blackmatrix

blackmatrix

Docker app from Stoatworks Labs' Repository

Overview

Crosspoint router matrix for Blackmagic ATEM switchers and Videohubs — one grid across the fleet, with Videohub protocol emulation so hardware router panels drive it. Note: Two reasons for host networking, both of which fail silently in bridge mode: the device scan sweeps the local /24s and would sweep Docker's bridge instead, and the Videohub emulation LISTENS on a port per switcher (9990 upward) for panels to connect to. Devices can still be added by IP without the scan. This image is published from a private repository, so its GHCR package is private too. Run 'docker login ghcr.io' on the server before installing.

BlackMatrix

AI-assisted project. This codebase was created with Claude Code (Anthropic), directed and reviewed by a human author. It was developed against a built-in simulated switcher fleet (--mock) and a real TCP client speaking the Videohub protocol, then checked against a real ATEM Mini Extreme ISO, which corrected the routing rules rather than confirming them — the availability masks were probed on the hardware and this app's matrix was served from it as a 29x39 router. What has never happened is a real Videohub control panel driving it, and no real Videohub has ever been one of its devices. Prove it on your own kit before it goes anywhere near a show.

A crosspoint router matrix for a fleet of Blackmagic ATEM switchers — sources across the top, destinations down the side, one click to route — that also pretends to be a Blackmagic Videohub so hardware router panels, Companion and Blackmagic's own software can drive the same crosspoints — and, for a redundant rig, so a media server can switch to its backup machine through it.

Watch it running — 53 seconds

Fifty-three seconds, filmed against a released build and its simulated fleet. The failover at the end fires because the heartbeat genuinely stopped and the app's own state machine decided it had — the crosspoints move because a salvo was taken, not because a video was edited. Two of the switcher's cameras are renamed to stand in for the media servers; that is the only staging.

An ATEM has no single "router", so this treats every bus that takes one source at a time as a destination, grouped into sections:

Section Destinations Routed with
Outputs Aux 1..n setAuxSource
Program / Preview Program and Preview per ME changeProgramInput / changePreviewInput
Keyer sources USK fill and key per ME, DSK fill and key setUpstreamKeyer*Source / setDownstreamKey*Source
SuperSource Box 1..n, art fill, art key setSuperSourceBoxSettings / setSuperSourceProperties
Multiview Every multiviewer window setMultiViewerWindowSource

Which sources are legal on which destination is read from the switcher, not from a model table: every ATEM input reports a sourceAvailability mask (aux, multiviewer, SuperSource art, SuperSource box, key source) and a meAvailability mask. Illegal cells are drawn hatched and refused server-side — so an aux output cannot be routed back onto an aux bus, and ME 1's output cannot be routed onto ME 1.

Quick start

npm install
npm run dev:mock

That serves http://localhost:8533 with three simulated switchers of deliberately different shapes (1 M/E, 4 M/E, compact) and a Videohub server per switcher. No hardware, no network switchers, nothing to break.

Against real switchers, write blackmatrix.config.json (see config/blackmatrix.config.example.json) and:

npm run build && npm start

Download

v0.2.0 — prebuilt for macOS, Windows and Linux. Pick your platform:

macOS — Apple Silicon, Intel
Build Download Size
Apple Silicon · .dmg disk image BlackMatrix_0.2.0_aarch64.dmg 46 MB
Intel · .dmg disk image BlackMatrix_0.2.0_x64.dmg 48 MB
Apple Silicon · .pkg installer blackmatrix-0.2.0-macos-aarch64.pkg 47 MB
Intel · .pkg installer blackmatrix-0.2.0-macos-x86_64.pkg 49 MB
Windows — x64
Build Download Size
x64 · .exe installer BlackMatrix_0.2.0_x64-setup.exe 29 MB
Linux — x64
Build Download Size
x64 · .deb package (Debian/Ubuntu) BlackMatrix_0.2.0_amd64.deb 56 MB
x64 · .rpm package (Fedora/RHEL) BlackMatrix-0.2.0-1.x86_64.rpm 56 MB

All builds, checksums and release notes: github.com/stoatworks-labs/blackmatrix/releases.

macOS builds are signed and notarised and open normally. The Windows builds are unsigned, so SmartScreen warns once.

Videohubs, not just switchers

A real Blackmagic Videohub can be a device in the same fleet:

{ "id": "hub", "name": "Machine room", "type": "videohub", "address": "192.168.1.60" }

Its inputs become sources and its outputs become destinations in the same grid, so one screen covers the switchers and the router between them, and a salvo can span both. Locks on a Videohub are the router's own and shared with every other client on it. Details, and the ties that make an ATEM bus and a router output follow each other: docs/videohub.md.

Capture a switcher before you lose it

npm run capture -- 192.168.10.240 --name "Stage"

The availability masks this whole project depends on exist only in the protocol — not in an ATEM autosave, not in any file a switcher leaves behind. A capture puts the real shape of a real switcher on disk, and "capture": "<file>" on a device replays it with no hardware present. See docs/capture.md, including the opt-in probe that tests the masks against the hardware.

Adding devices

The Devices page (top right) is where switchers and routers are added, edited, reconnected and removed, with no config file and no restart. Scan network sweeps the local /24s for both kinds — a switcher answers on UDP 9910, a router on TCP 9990, and neither answers the other's probe — and offers what it finds.

Ports for the Videohub emulation are assigned from basePort upward and written back to the config, so they stay put when devices are added or removed. A panel is configured against a port number; that number is a fact about the device, not about where it sits in a list.

A device id cannot be changed once made — salvos, ties and label overrides are all keyed on it. Removing a device leaves salvos and ties that reference it alone rather than rewriting them, and says which ones: an operator who pulls a switcher out for an hour should get their salvos back when it returns.

On a phone

The web UI is responsive: below 800px the grid is replaced by an X-Y panel — a destination list showing what each is taking, then the sources that destination will accept. Not a smaller grid, because a grid does not shrink; this is what a hardware panel does for the same reason. Preset is the default at that width, so a mis-tap stages instead of cutting.

That works in any phone browser pointed at the server. There is also a native iOS app in mobile/, which adds the one thing a browser cannot do — finding the server without typing an address:

cd mobile
npm install
npm run ios:init      # once, generates the Xcode project
npm run ios:build     # or: npx tauri ios build --debug --target aarch64-sim

It is a shell, deliberately: it sweeps the local /24s for BlackMatrix servers, remembers the ones you use, and then shows the server's own UI. It does not speak the ATEM or Videohub protocols — a phone has no business holding a switcher connection open while the OS suspends it, and the panel emulation needs a listening socket. Both stay on the server.

Android builds too (npm run android:init, then npm run android:build). It needs ANDROID_HOME, NDK_HOME and a JAVA_HOME pointing at JDK 21 — Gradle cannot use JDK 25, and says so only as a bare > 25.0.4.

Neither mobile app has run on a physical device: that needs an iOS development profile and an Android release key, both of which are credentials rather than code. docs/signing.md has the steps, and the build wiring for both is already in place.

The hosted simulator

npm run sim          # locally, on :5183
npm run sim:build    # static build in packages/web/dist-sim

A build of the same UI that simulates its devices in the browser tab, with a model list covering the ATEM and Videohub ranges. It is a demo and says so on every screen: nothing is on a network, nothing is being controlled, and no web page can reach this hardware — the ATEM protocol is UDP, the Videohub protocol is raw TCP, and the emulation this app offers panels has to listen on a port. A browser can do none of those things.

It is not a reimplementation. The simulator builds a real AtemState and runs it through the same matrix model, the same legality rules and the same grid as the live app, so what it shows is what the app would show. What it cannot be is accurate about a model: the names are Blackmagic's, and only the entries marked as read off hardware carry real numbers. Everything else is an approximate shape for that class of device, and a capture replaces it with the truth.

Configuration

{
  "port": 8533,                                   // UI + REST
  "videohub": { "enabled": true, "basePort": 9990, "host": "0.0.0.0" },
  "devices": [
    { "id": "stage", "name": "Stage", "address": "192.168.10.240" },
    { "id": "studio", "name": "Studio", "address": "192.168.10.241", "videohubPort": 9995 }
  ],
  "labels": { "stage": { "aux.0": "FOH screens" } },  // your names for destinations
  "salvos": []
}

Each switcher gets its own Videohub server: the first on basePort, the next on basePort + 1, unless you set videohubPort. If something else on the host already listens on 9990 — Bitfocus Companion's Videohub panel surface does — the affected switcher logs the clash, starts without it, and everything else keeps working. Give it another port.

Controlling it from a router panel

Point any Videohub client at <host>:<port for that switcher>:

  • Bitfocus Companion — the blackmagic-videohub module.
  • A Smart Videohub / Universal Videohub hardware panel, or Blackmagic's Videohub Control software.
  • Anything else that speaks the Videohub Ethernet Protocol.

Input numbers are the switcher's sources in ATEM source-id order; output numbers are the destinations in the section order of the table above. Both orderings are stable for a given switcher, so a panel's buttons keep meaning the same thing.

Full mapping, what is and is not implemented, and a worked telnet session: docs/videohub.md.

Locks

A destination can be locked, from the browser or from a panel. Locks are held per IP address, which is how a real Videohub does it — so a second panel on the same machine shares the lock, and everyone else is refused. Shift-click the padlock (or send F on the wire) to force one open.

Locking is enforced in one place, so a lock taken from a panel refuses a route made from the browser, and the other way round.

Salvos

A salvo is a named set of crosspoints across the whole fleet — one press sets them all. Build one in the UI by capturing what destinations are currently taking (Build new, then + on each row you want), or write them into the config file.

Every crosspoint in a salvo is attempted; the ones the switcher will not accept come back named rather than silently dropped. Salvos are this app's own idea — they are not part of the Videohub Ethernet Protocol — so they exist in the UI and the REST API only.

Redundant systems

A redundant media server rig is two machines playing the same show and a router downstream deciding which one reaches the screens. This can be that router, in either of the two shapes the industry uses.

The media server drives it. disguise's understudy sends matrix routing itself the moment it takes over a failed machine, and PIXERA fires a control action at a matrix switcher from its System Lost trigger. Both can point at the Videohub emulation — disguise has a Blackmagic Smart Videohub driver, and PIXERA can send anything over TCP. Because an ATEM re-syncs its inputs, the main and backup feeds do not have to be genlocked to each other for a clean cut, which an SDI router does require.

Or this app decides. A failover watch polls a machine — a TCP port, a URL, or a heartbeat it must be sent — and fires a salvo when it stops answering. It will not fire before it has seen the machine working once, it starts disarmed, it fires once, and it does not switch back unless you say what "back" means. What it fires is an ordinary salvo, so the failover can be rehearsed by pressing Take on it.

And a plain line protocol on TCP and UDP (ascii.enabled, port 9995) for anything that can send a string but not speak Videohub — disguise's generic Telnet Matrix, PIXERA's TCP module, 7thSense, a show controller:

ROUTE 2 1          route output 2 to input 1
1*2!               the same thing, Extron style
SALVO backup       fire a salvo by name
3.                 fire the third salvo (Extron preset recall)
FAILOVER main      fire a watch's lost salvo

One lock trap is worth knowing before you rely on any of it: a refused route is answered with ACK and an unchanged status, as the Videohub spec requires, which means a media server cannot see a refusal. A locked destination is a failover that silently does not happen. videohub.failoverClients names the addresses whose routes walk through locks.

All of it, including how to fill in disguise's and PIXERA's fields, is in docs/failover.md.

REST API

Method Path Body
GET /api/fleet
POST /api/devices/:id/route { "destination": "aux.0", "source": 3 }
POST /api/devices/:id/lock { "destination": "aux.0", "action": "lock|unlock|force" }
POST /api/devices/:id/label { "destination": "aux.0", "label": "FOH" } or { "source": 3, "label": "Wide" }
GET POST /api/salvos a salvo
DELETE /api/salvos/:id
POST /api/salvos/:id/take
GET POST /api/failover a failover watch
DELETE /api/failover/:id
POST /api/failover/:id/arm { "armed": true }
POST /api/failover/:id/trigger — take over now
POST /api/failover/:id/restore — go back, clear the latch
POST /api/failover/:id/heartbeat — for a heartbeat probe

The browser gets the same state pushed over ws://<host>:8533/ws.

Renaming a source renames the input on the switcher itself. Renaming a destination is local to this app — the ATEM has no name for "Aux 2".

Layout

packages/
  videohub   @av/videohub      Videohub Ethernet Protocol v2.3, server AND client. Knows nothing about ATEMs.
  ascii      @av/ascii-matrix  The plain-text line protocol. Also knows nothing about ATEMs.
  matrix     @av/atem-matrix   Turns an AtemState into destinations, legal sources, and routing calls.
  server     @blackmatrix/server  Fleet, locks, salvos, failover, REST + websocket, one Videohub per switcher.
  web        @blackmatrix/web     The grid.

The three libraries are portable on purpose: @av/videohub will put a Videohub front end on anything with crosspoints, @av/ascii-matrix will put a line protocol on it, and @av/atem-matrix has no I/O in it at all.

Known limits

  • No real Videohub panel has driven it, and no real Videohub has ever been one of its devices — the protocol side is verified against the published specification, a TCP client, and an ATEM's own Videohub server, not a panel.
  • No media server has driven the failover support. It is written from disguise's and PIXERA's documentation, and tested against this repo's own clients. The numbering is the first thing to check on real kit.
  • The first two multiview windows are program and preview on most switchers, which usually means the switcher ignores a route sent to them. Those rows are marked, not hidden.
  • Media player still/clip selection is deliberately not a destination. It takes from the media pool, not from the video sources, so it does not belong in a matrix whose columns are sources.
  • The failover probes answer "is that machine there", not "is it playing the right thing" — a TCP handshake, an HTTP answer, or a heartbeat. Knowing whether a media server's output is correct means speaking its protocol, which is a much larger promise than this makes.
  • Streaming and recording follow program and are not routable, so they are absent.

License

MIT — see LICENSE.

Install Blackmatrix on Unraid in a few clicks.

Find Blackmatrix in Community Apps on your Unraid server, review the template, and click Install. Unraid handles the Docker app or plugin setup from the published template.

Open the Apps tab on your Unraid server Search Community Apps for Blackmatrix Review the template variables and paths Click Install

Related apps

Details

Repository
ghcr.io/stoatworks-labs/blackmatrix:latest
Last Updated2026-08-23
First Seen2026-08-22

Runtime arguments

Web UI
http://[IP]:8533/
Network
host
Shell
sh
Privileged
false

Template configuration

ConfigPathrw

Configuration files.

Target
/app/config
Default
/mnt/user/appdata/blackmatrix/config
Value
/mnt/user/appdata/blackmatrix/config