Wire the web app to your actual ATEM.
The web UI lives at /app/ and runs anywhere — phone, laptop, tablet. To make it actually control your ATEM, you need to run a small Node.js backend on a computer that's on the same Ethernet network as the ATEM. Takes about 10 minutes.
The ATEM lives at a fixed IP on your network.
Open ATEM Software Control on your computer. Top-left dropdown shows the connected ATEM — click the gear icon next to it. Note the "IP Address" field. Write this down — you'll need it in Step 4.
If you've never set the ATEM's network: by default it tries DHCP. Most home routers will assign it something like 192.168.1.50. To make it predictable, set a static IP from ATEM Software Control → Settings → ATEM → Network.
The backend runs on Node.js v18 or newer.
Download from nodejs.org — pick the LTS version. Works on macOS, Windows, Linux, and Raspberry Pi. After installing, verify in a terminal:
$ node --version v20.11.0 # or higher
One file. Two dependencies. No magic.
Create a folder for the backend (anywhere — Desktop, Documents, doesn't matter). Inside it, save these two files:
package.json
{
"name": "atem-web-bridge",
"version": "1.0.0",
"main": "server.js",
"scripts": { "start": "node server.js" },
"dependencies": {
"atem-connection": "^3.5.0",
"ws": "^8.16.0"
}
}
server.js
// Bridges a websocket between the VSG web UI and a Blackmagic ATEM const { Atem } = require('atem-connection'); const { WebSocketServer } = require('ws'); const ATEM_IP = process.env.ATEM_IP || '192.168.1.50'; // <-- your ATEM's IP const PORT = parseInt(process.env.PORT || '9001', 10); const atem = new Atem(); const wss = new WebSocketServer({ port: PORT }); let latestState = null; atem.on('connected', () => { console.log('[ATEM] connected'); broadcast({ event: 'info', model: atem.state?.info?.productIdentifier || 'ATEM Mini', firmware: 'connected' }); }); atem.on('error', err => console.error('[ATEM] error:', err)); atem.on('stateChanged', (state, paths) => { latestState = state; const me = state.video.mixEffects[0]; if (!me) return; broadcast({ event: 'state', state: { program: me.programInput, preview: me.previewInput } }); }); function broadcast(msg) { const data = JSON.stringify(msg); wss.clients.forEach(c => c.readyState === 1 && c.send(data)); } wss.on('connection', (ws) => { console.log('[WS] client connected'); if (latestState) { const me = latestState.video.mixEffects[0]; ws.send(JSON.stringify({ event: 'state', state: { program: me.programInput, preview: me.previewInput } })); } ws.on('message', async (raw) => { try { const { cmd, payload } = JSON.parse(raw.toString()); if (cmd === 'cut') await atem.changeProgramInput(payload.input); if (cmd === 'preview') await atem.changePreviewInput(payload.input); if (cmd === 'take' && payload.type === 'cut') await atem.cut(); if (cmd === 'take' && payload.type === 'auto') await atem.autoTransition(); } catch (e) { console.error('[WS] cmd error:', e); } }); }); console.log(`[ATEM] connecting to ${ATEM_IP}...`); console.log(`[WS] listening on ws://0.0.0.0:${PORT}`); atem.connect(ATEM_IP);
Three commands.
- Open a terminal in the folder where you saved the two files above.
- Install dependencies:
$ npm install - Start the backend, replacing
192.168.1.50with your ATEM's actual IP:# macOS / Linux: $ ATEM_IP=192.168.1.50 npm start # Windows (PowerShell): $ $env:ATEM_IP="192.168.1.50"; npm start
You should see:
[ATEM] connecting to 192.168.1.50... [WS] listening on ws://0.0.0.0:9001 [ATEM] connected
Find this computer's IP, then plug it into the web app.
Find this machine's local IP:
# macOS / Linux: $ ipconfig getifaddr en0 192.168.1.42 # Windows: $ ipconfig | findstr IPv4 IPv4 Address. . . . . . . . . . . : 192.168.1.42
Open the web app at videoswitchguide.com/app/ on any device on the same network. Click the connection status pill in the top-right (or the Connect · Settings button), and enter:
ws://192.168.1.42:9001
Hit Connect. The status pill turns green and starts pulsing. The web UI now controls your real ATEM — from the browser, on any phone, laptop, or tablet on your network.
Your ATEM data never leaves your network. The web UI runs at videoswitchguide.com (over HTTPS), but the control connection is a direct WebSocket to your local machine. Nothing is proxied through this site.
Run the backend as a service.
If you want the backend to run automatically without keeping a terminal open:
- —macOS: use
launchdwith a .plist in~/Library/LaunchAgents/. - —Linux: use
systemdwith a unit file in/etc/systemd/system/. - —Windows: use
node-windowsor NSSM to wrap it as a Windows service. - —Cross-platform: use
pm2(recommended) —npm i -g pm2 && pm2 start server.js --name atem-bridge.
If something doesn't work.
- —Backend says "ATEM connecting..." but never connects. Wrong IP. Re-check from ATEM Software Control. Or the ATEM is powered off / on a different network.
- —Web UI shows "Connection failed". Two reasons: the backend isn't running, or your computer's firewall is blocking port 9001. Allow the port or move to a different one (set
PORT=9002in the env vars). - —The web UI is on HTTPS but tries to connect to
ws://. Most browsers block insecure WebSocket from a secure page. Workarounds: visit the web UI over HTTP (http://videoswitchguide.com/app/), or terminate TLS in front of the backend with Caddy / nginx so you can usewss://. - —It works on the same machine but not from my phone. Phone needs to be on the same Wi-Fi as the backend computer. Some routers have "client isolation" that blocks devices from talking to each other — turn it off.
What this backend doesn't do (yet).
The skeleton above handles cuts and transitions — the most common operations. To extend it for audio mixing, recording control, streaming, keyers, or media graphics, see the full atem-connection library docs. The library exposes essentially every command Blackmagic's official ATEM Software Control sends.
For a more comprehensive solution that handles many devices (including the ATEM Mini), see our deep dive on Bitfocus Companion — an OSS controller that already covers the ATEM exhaustively, and saves you from writing your own backend.