Skip to content

About

Small app to bridge tally signals from Blackmagic cameras to Canon cinema-line cameras correctly.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

1 Commit

Folders and files

Repository files navigation

ATEM → Canon XC Tally Bridge

Listens to tally on a Blackmagic ATEM switcher and drives the tally lamps on Canon XC-protocol cameras (CR-N700, EOS C400, C70, C80, C500 Mk II, C300 Mk III, XF605, CR-N500/N300/N100, CR-X300).

No SDK install, no native modules, no runtime dependencies — just Node 18+.

ATEM Constellation 4K ──UDP/9910──▶ bridge ──HTTP control.cgi──▶ 8× Canon cameras
                        (tally)                                   (tally lamps)

Quick start

npm install && npm run build
cp config.example.json config.json

Edit config.json with your switcher and camera addresses, then check that every camera answers before you rely on it:

npm run check
npm start

Open http://127.0.0.1:8080 for a live dashboard.

Both scripts read ./config.json. To use a different file, call the CLI directly — it lives at dist/src/cli.js:

node dist/src/cli.js --config /path/to/other.json

How it works

Reading tally from the ATEM

The bridge speaks the ATEM's native UDP protocol directly. It completes the session handshake, acknowledges what the switcher sends, and decodes three commands: TlSr (tally by source), PrgI/PrvI (program/preview bus per M/E), and InPr (input names, for the dashboard).

It is receive-only. The only thing it ever transmits is a protocol acknowledgement — there is no code path that can change what is on air.

Two tally sources are available, set by atem.tallySource:

Mode Behaviour
switcher (default) The ATEM's own resolved tally, the same signal that drives its hardware tally outputs. A camera that is live inside an upstream/downstream key, a DVE box, SuperSource, or an M/E re-entry lights up correctly.
me Watches one M/E's program and preview buses only (atem.mixEffect, 1-based). Simpler, but a camera that is live only through a keyer stays dark.

switcher is the default because a camera operator with a dark tally who is actually on air is the failure mode that matters. Switch to me if you specifically want per-M/E tally.

Driving the Canon lamps

Tally is set with a single sessionless request:

GET /-wvhttp-01-/control.cgi?f.tally.mode=program&f.tally=on

Per Canon's XC spec, control.cgi is mandatory on every XC-protocol camera, needs no session, and needs no camera-control privileges — so the bridge never holds a session that could be pre-empted by an operator's PTZ controller, and nothing it does interferes with PTZ, focus, or menu control.

A few details that matter in practice:

  • Capabilities are detected, not hard-coded. At startup each camera is asked for f.tally.mode.list via info.cgi. The CR-N series reports preview,program; cinema bodies also report preview_program. New models work without a code change.
  • Program beats preview. When a source is on both buses, cameras that can show both do (combinedTally: "auto"); the rest show program red.
  • Mode is set before the lamp is switched on, so the lamp never briefly flashes the previous colour.
  • Requests coalesce. Rapid takes never queue up stale commands — only the newest state is ever sent, with Canon's recommended ≥30 ms spacing per camera.
  • Connections are kept alive, so a tally change costs one round trip.
  • Digest auth is negotiated automatically on the first 401 and then pre-applied to every later request.

Staying correct

  • Reconcile loop. Every reconcileIntervalMs (default 5 s) the desired state is re-asserted on every camera. This repairs a lamp after a camera reboot, a dropped request, or someone changing tally in the camera's own web UI.
  • Auto-reconnect. Five seconds without traffic from the ATEM tears the session down and re-handshakes.
  • Hold on disconnect. If the switcher goes away the lamps keep their last state by default. Mid-show, every tally going dark at once is worse than a few seconds of stale state. Set behaviour.onAtemDisconnect: "clear" to prefer dark.
  • A busy status port never stops tally. If the dashboard can't bind, the bridge logs it and carries on.

Configuration

See config.example.json — every option is documented inline. The only thing you must fill in is the ATEM address and the input → camera mapping.

{
  "atem": { "address": "192.168.1.240" },
  "cameras": [
    { "input": 1, "name": "Cam 1", "host": "192.168.1.101", "username": "admin", "password": "..." }
  ]
}

input is the ATEM source id — for physical inputs, the number printed on the switcher. Config errors are all reported at once at startup, so a show-day typo fails loudly instead of leaving one camera dark.

CLI

node dist/src/cli.js [options]

  -c, --config <path>   Config file (default: ./config.json)
      --log-level <l>   debug | info | warn | error
      --check           Validate config, probe every camera, exit
  -h, --help

Dashboard and health

Endpoint Purpose
/ Live dashboard — colour-coded per camera, updates over SSE
/api/status JSON snapshot
/api/events Server-sent events, one per change
/healthz 200 when the ATEM is connected and every camera is reachable, 503 otherwise

Testing without hardware

The repo ships simulators for both ends, so the whole chain can be exercised on a laptop.

Run everything at once — starts a fake switcher, three fake cameras (one behind digest auth, one cinema body with the combined tally mode), the real bridge, and asserts 16 behaviours end to end:

npm run selftest

Or drive it by hand. In one terminal:

node dist/tools/fake-canon.js --port 18081 --model "Canon CR-N700"

In another:

node dist/tools/fake-atem.js --port 19910 --inputs 8

Then point a config at 127.0.0.1:19910 / 127.0.0.1:18081 and type pgm 3, pvw 2, both 1, or off at the fake ATEM's prompt.

Unit tests:

npm test

Running it as a service

A launchd plist for macOS is in docs/. Install it with:

cp docs/com.tallybridge.plist ~/Library/LaunchAgents/ && launchctl load ~/Library/LaunchAgents/com.tallybridge.plist

Edit the paths inside it first. Logs go to /tmp/tally-bridge.log.

Network notes

  • The bridge must reach the ATEM on UDP 9910 and each camera on TCP 80.
  • Put the bridge on the same VLAN as the switcher and cameras if you can; tally latency is dominated by round-trip time.
  • Cameras only need their guest or a normal user account to have camera control enabled — administrator rights are not required.

Known limitations

  • Canon's XC spec has no push mechanism for setting tally, so each change is an HTTP request per affected camera. On a LAN this is a few milliseconds; over a routed WAN it will be visibly slower.
  • preview_program support is read from the camera. The EOS C400 postdates the published spec revision (BPE-7216-005, Sept 2023), so its modes are detected at runtime rather than assumed.
  • Sources above the physical inputs (colour bars, media players, M/E re-entry) can be mapped by their ATEM source id, but there is no lookup table for those ids in this tool — use the number the switcher reports.

Protocol references

  • docs/protocol-notes.md — the exact byte layouts this tool depends on, with the reasoning behind each design decision.
  • ATEMSwitchersSDKManual.pdf — Blackmagic's SDK manual. Documents the tally model (IsProgramTallied / IsPreviewTallied, DSK tally override), which is what TlSr carries on the wire.
  • canon-xc-control-protocol-specifications-005.pdf — Canon's XC spec. §3.2.3.1 covers control.cgi; the f.tally parameters are in Appendix iii.

About

Small app to bridge tally signals from Blackmagic cameras to Canon cinema-line cameras correctly.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages