A universal camera-control hub. One software interface — a broadcast-style RCP (paint controls) and a touch PTZ panel modelled on the Panasonic AW‑RP150 — plus a normalizing command bus that drives cameras from many vendors over their native protocols. The same commands also reach every camera from a Bitfocus Companion surface or a USB control panel.
Status: the bridge, protocols and UI are real and build clean; a committed unit-test suite covers the protocol framing. Where a family was verified against public docs or an official reference it is marked verified below. Items marked tuning need a first on-camera test to confirm value encodings. Nothing ships as a fake/demo device.
One installation is the whole tool. The installer for Windows and macOS
carries the control surface and the bridge; the bridge starts with the
application, on ws://localhost:9700, and stops with it.
https://github.com/larszu/lz-camera-bridge/releases
That was not always true, and the difference matters if you read an older note: until 2026-09-16 the installer shipped the panel alone. It came up looking complete and reconnected every three seconds against a bridge that nobody had started — you were expected to run a second process out of a clone of this repo, and nothing in the application said so. Now the panel finds its bridge because the application brought it.
A separate bridge is still perfectly legal, and for a control room it is the normal case: run the bridge where the cameras are and point tablets at it (see Other devices on the same network). If port 9700 is already taken the application says so and uses the bridge that is already running, instead of failing silently.
Every push to the default branch builds this repo's page from
.github/workflows/pages.yml and publishes it:
https://larszu.github.io/lz-camera-bridge/
The workflow asks the Pages API before it configures anything. With no Pages site it still builds — that is a real check — and skips only the publishing step, with a warning and the one missing step in the run summary. A run that must stay red for a click nobody made teaches people to ignore red.
Measured 2026-09-16: published. The Pages site exists, and the deploy job
now runs through — the switch that no workflow can flip (GITHUB_TOKEN may not
create a site) has been thrown. Every push to main publishes by itself.
What the page carries is the README and docs/, rendered — not the UI
itself. That is deliberate and not an omission: web-rcp is a client that
needs the bridge, and on a Pages address it would look like the application
while operating no camera. The running program comes from the installer below.
| Family | Mode | Transport | Status |
|---|---|---|---|
| Sony CCU (HXC/HDC, BRC) | tcp / serial |
700PTP over TCP :7700 / RS‑422 | paint verified; AWB/ABB deliberately disabled¹ |
| Sony FX / Alpha (FX3, FX6, A7 …) | sony-usb |
PTP vendor extension over USB (libusb) | verified (codes from libgphoto2); handshake 3.00 and confirmed writes ported³ |
| Sony FX / Alpha over Wi-Fi/LAN | sony-ptpip |
PTP/IP TCP 15740, SSDP discovery; Access Authentication through SSH (port 22, aes128-ctr, host-key fingerprint) | protocol **verified on an FX3 (fw 7.00)**³; this bridge path read-only on that FX3 |
| Sony Monitor & Control (FX WiFi) | sony-mnc |
HTTP :10000 + SSDP discovery | unverified (undocumented app protocol) — for FX3/A7 prefer sony-ptpip |
| Canon EOS (R5/R6/R7/R8/R10 …) | canon-ccapi |
CCAPI HTTP REST :8080 | verified (official CCAPI) |
| Panasonic Lumix (S1/S5/GH5/GH6 …) | lumix-http |
cam.cgi HTTP |
verified |
| Blackmagic (Pocket/Studio/URSA, FW 8.6+) | blackmagic |
/control/api/v1 REST + WS |
verified |
| Z CAM (E2 family) | zcam |
/ctrl/set HTTP |
verified |
| Panasonic AW PTZ (AW‑UE/HE) | panasonic-ptz |
aw_ptz / aw_cam CGI |
verified (AW protocol) |
| VISCA over IP (PTZOptics, Marshall, AVer, Sony BRC/SRG) | visca |
UDP :1259 (raw) / :52381 (Sony header) | verified |
HTTP-CGI PTZ (Vissonic/PTZOptics ptzctrl.cgi, Sony SRG/BRC /command/) |
http-cgi |
HTTP GET, Referer + Digest; power via VISCA TCP :5678 (Vissonic) or main.cgi (Sony) |
verified live on a Vissonic clone and a Sony SRG-A40 |
| JVC ConnectedCam / KY‑PZ | jvc |
Digest login + /cgi-bin/api.cgi |
verified vocabulary (some steps tuning) |
| BirdDog NDI PTZ | birddog |
VISCA :52381 + REST :8080 | verified endpoints (tuning) |
| VISCA on a serial line (same heads, RS‑232/422 wiring) | visca-serial |
RS‑232 8N1, 9600 default, daisy‑chain address 1–7 | framing unit‑tested; not yet on a camera |
| DJI Ronin RS 2 / RS 3 Pro | dji-ronin |
DJI R SDK over CAN 1 Mbit/s via USB SLCAN adapter | framing unit‑tested; not yet on a gimbal² |
| DJI Osmo Pocket 3 / 4 | dji-osmo |
DUML over a serial (CDC) device | framing unit‑tested; not yet on a gimbal³ |
³ Sony PTP, shared by sony-usb and sony-ptpip (SonyPtpClient), follows
Sony's Camera Control PTP 3 Reference: handshake with protocol 3.00 and
retry while the camera has no version yet; every write waits until the camera
reports the value (a real FX3 shows it ~250 ms late); 500 ms after an
exposure-mode change; the clock (0xD223) as an ISO 8601 string. The protocol
code is the one lz-camera-sync
runs against an FX3 (fw 7.00) since 2026-10-07 — reading 369 properties,
writing and restoring values, setting the clock, over Wi-Fi with SSH. In this
bridge the sony-ptpip path was checked read-only against that FX3 (SSDP
found it and an A7 IV; SSH with fingerprint, handshake, 369 properties). The
SSH password is never written to site.json; after a bridge restart it is
entered again.
¹ Sony's 700 protocol is NDA-only; no public source documents the auto‑setup
command codes, so those buttons stay disabled rather than guessing at a
broadcast CCU. See packages/web-rcp/src/capabilities.ts.
² RS 2 and RS 3 Pro only. RS 3 and RSC 2 look the same and do not speak the SDK. Needs a USB SLCAN adapter (CANable, USBtin, Lawicel) on the focus‑wheel port — not SocketCAN, which is Linux‑only.
³ DJI publishes no control interface for the Pocket series, and its USB‑C port
serves storage and UVC webcam — there is no USB control path. The one
publicly reverse‑engineered route is Bluetooth LE; what ships here is DUML over
a serial device, and the BLE seam is prepared but not filled. Both gimbals move
the head only: a gimbal carries a camera, it is not one, so paint controls stay
greyed out. The full picture, including which constants to turn first if a
device stays silent, is in docs/dji-gimbal.md.
-
Web RCP — Sony-style paint panel (iris, master black/gamma, R/G/B white & black balance, gain, ND, shutter, bars). Buttons are gated per backend so unsupported functions are disabled, not silently failing.
-
Touch PTZ panel — AW‑RP150-style joystick (pan/tilt with speed), zoom/focus rockers, push‑AF, iris and a paged preset grid with a store mode.
-
Soft presets — on heads that position absolutely and report their pose (VISCA IP/RS‑232, BirdDog, Panasonic AW, HTTP‑CGI, robotic heads) a stored preset keeps the pose in the bridge, and a recall glides there with ease‑in/out: all axes start and land together, move time per preset, Soft switch per preset. Other paths recall the camera's own preset, and the panel says so.
-
Bitfocus Companion — module in
packages/companion-module-lz-camera-bridge(v0.2.0): paint, pan/tilt/zoom/focus and the elevation column, soft presets (move time, Soft per preset), studio shots (cut/fade, shot sets, seats, moves, move anyway, on-air lock per camera), panorama aim and the switcher, with dropdowns, feedbacks, variables and ready-made button pages built from the bridge's cameras and studio shots. It talks to the Companion API (9701/9702) and the panel's port (9700). See itsHELP.md. -
USB HID control panel — a data-driven adapter maps a hardware panel's axes/buttons onto the command bus, so one panel controls any brand.
-
Keyboard — on the PTZ panel the arrow keys drive while held,
1–9recall presets,Homegoes home; a window that loses focus stops the head. -
Planned shots — the MultiCam Planner's camera list (v3) carries each camera's heading and its shots as pan/tilt/focal length in the room frame. The bridge turns them into head angles, drives them (
ptzAbsoluteon VISCA, Sony CGI, Panasonic AW and the Vissonic heads via VISCA TCP), stores them in the head's own preset memory, and takes a one-time offset on site: steer the head onto one known shot, click Head is here, and the mounting error is applied to every shot. From then on any panel or Companion recalls them by number. Zoom from a focal length is estimated between the ends of the lens until a zoom curve is measured: drive the zoom to 0/25/50/75/100 %, read the focal length off the lens or the camera menu, Record point. Details indocs/camera-plan.md. -
Aim by frame — on the PTZ panel, drag a 16:9 frame in the camera's live picture: the bridge turns it into pan/tilt from the pose the head reports and into a zoom position from the field of view per zoom (datasheet wide/tele angles, sensor width + focal length, or measured by Measure: the head pans a little at five zoom positions and the bridge measures how far the picture moved). Heads that take a stream of positions glide there like a soft preset; the robotic head plays its own eased keyframes; the others drive their own absolute move. Needs a head that reports its pose and a stream address.
-
Panorama map — Scan panorama steps the head through a pan/tilt grid at wide zoom, takes a picture at every stop and stitches an equirectangular map from the known poses (no feature matching). It is stored in the config directory (
panorama/cam-<n>.jpg) and served at/panorama/<n>.jpg. A frame drawn on the map drives the head there, also while it stands at full tele; the live picture is shown in its place on the map. The map is a still — scan again when the room has changed. -
Studio shots (view Shots) — for every camera on the bridge:
- Cut or Fade: cut recalls at the camera's own top speed; fade is a timed move with ease-in/out, pan, tilt and zoom arriving together — the same preset glide as above, with the move time of the recall. A planned shot glides from the pose the head reports; robotic heads play the time themselves. Where neither works the shot cuts, and the panel says so.
- Shot sets — one button recalls a shot on several cameras with one common move time.
- On-air lock — a camera whose switcher input is on programme does not move on a shot recall (sets, seats, moves, preset keys, Companion presets, planned shots); the banner offers Move anyway. Per camera, on by default.
- Seats — a seating chart (rows × seats); each seat lists shots on one or more cameras, best first. A tap takes the first camera that is connected and not on air. Optionally a room plan instead of the grid: seats are dragged to where they are (a lectern, a sofa, a panel table), and Place grid lays out rows × seats as a quick start.
- Moves — shots of one camera in a row, each with its own move time and an optional hold; any joystick touch stops the move.
Shot sets, seats and moves live in the site file. Untested on real heads: so far all of it has only run against the demo camera.
The bridge also drives the room around the cameras. This came over from the
former av-control-center application (2026-09-28) and replaces it.
- Switcher — a Vissonic VIS-CATC (one output, twelve multi-image
layouts) operated the way a panel works: a programme row over a preview row
and one CUT. The preview bus lives in the bridge, so every screen shows
the same pre-selection. Keys:
1–6preview,Shift+digit take,EnterorSpacecut — by key code, so it works on every keyboard layout. Two routes to the device, chosen per site: its network page (POST /) or the RS-232 verb list from the manual, through a TCP-serial gateway or a port on the bridge computer. Layouts, audio, freeze (serial only), a crosspoint list for multi-window layouts. Protocol indocs/switcher.md. - Tally per camera — each camera names its switcher input; the bridge
derives programme/preview from the switcher and shows it on the camera list,
the wall, the PTZ panel and the video tiles. Companion gets
switcherPreview,switcherTake,switcherCut. - Video — live tiles from each camera's stream address (RTSP, SRT or
RTMP). The bridge runs one ffmpeg per stream and serves
GET /video/<n>.mjpegon its own port; the page shows it in an<img>, no player. Click a tile to preview its input, double-click to take. Own window opens the tiles alone for a second screen. The stream address never leaves the bridge. ffmpeg is looked for inLZ_BRIDGE_FFMPEG,ffmpeg-portable/, thenPATH; the desktop app brings its own. SRT needs an ffmpeg built with libsrt — the bundled one and Homebrew's default formula have none (RTSP and RTMP work with both); the tile says so instead of staying black. - Scopes — Scopes on a video tile opens waveform (luma) and
vectorscope under it; each panel's header switches to RGB parade,
histogram, CIE and more, 4 scopes fills the window with waveform,
vectorscope, parade and histogram. They measure raw R'G'B' frames from
WS /scope/<n>(LZ Scopes frame protocol), not the JPEG of the tile: the bridge starts its own ffmpeg per open scope and stops it when the panel closes. Details indocs/live-video.md. - Site — cameras and switcher as one file (
lz-sitev1). The desktop app andnpm run bridgekeep it inLZ_BRIDGE_CONFIG_DIR/site.json(default~/.lz-camera-bridge) and restore it on start, connecting what was connected. Export downloads it; import replaces the room — also from the settings export of the old av-control-center, so an existing room moves with one file. The file carries the camera logins; keep it out of repositories. - FreeD tracking — every camera that reports its pose (VISCA, Panasonic
AW, HTTP-CGI, robotic heads incl. elevation column) streams pan, tilt, zoom
and focus as FreeD D1 over UDP to one or more receivers, e.g. Unreal Live
Link: one switch per camera, targets and rate under Site, interpolated
between pose reads, silent when a camera stops answering. Not yet tested
against Unreal or real heads. Details in
docs/freed-tracking.md.
![]() |
![]() |
| Switcher — PGM over PVW, CUT, layouts, audio | Video — live tiles from RTSP/RTMP/SRT, tally on the top edge |
![]() |
![]() |
| Wall — every camera's panel at once | Site — the room as one file |
Taken 2026-09-29 against demo cameras, a switcher stand-in and ffmpeg test patterns streamed over RTMP — no real room, so nothing to blank out.
Input surfaces Bridge :9700 (command bus) Devices
──────────────── ────────────────────────── ───────
Web panel ─┐ ┌─ Sony CCU (700PTP) · Sony USB (PTP)
Cameras/Wall ┼─ WebSocket ────▶ BridgeServer ├─ Canon CCAPI · Lumix · Z CAM · Blackmagic
Switcher ┤ camera slots ──(RCP verbs)───────▶├─ VISCA (IP / RS-232) · Panasonic AW · JVC · BirdDog
Video tiles ─┼─ GET /video/n ─▶ RtspHub (ffmpeg → MJPEG) ◀────────┤ RTSP streams
Scopes ┼─ WS /scope/n ──▶ ScopeStream (ffmpeg → raw RGBA) ◀─┤
Site ─┘ GET /site.json site file (persisted) ├─ HTTP-CGI PTZ (Vissonic/PTZOptics, Sony SRG)
Companion ── HTTP/WS :9701-2 switcher slots ──(route/cut)─────▶└─ Vissonic VIS-CATC (HTTP or RS-232)
HID panel ── USB └─ tally per camera ──▶ every client
Every camera backend exposes the same handleRcpCommand(cmd, params) contract,
so a single command vocabulary (setIris, setMasterGain, ptz,
recallPreset, home, setCameraPower, …) fans out to all of them. A switcher
is not a camera and has its own slot family and verbs (preview, cut, take,
route, setLayout); the bridge derives the tally per camera from both. A visual walk-through lives in
docs/architecture.html.
Paint values can be trimmed relatively (cmd: 'nudge') instead of only jumped
to: one resolution for every path — WebSocket, Companion buttons, the RCP's
up/down selectors — against the value the bridge has actually read. Where it has
read none, the trim is refused with a reason rather than started from a guessed
128. The bus scale of every value, and what each backend makes of it, is
documented in docs/paint-nudge.md.
Every paint value on the panel says where it comes from: read back from the
device, or merely the last thing the bridge sent. Which is which is decided per
field and per connection mode by a table that was read out of this repository's
own backends — five of the twelve paths read nothing back at all, and the panel
says so once at the top instead of marking twenty knobs. See
docs/value-origin.md.
Live-video feasibility per camera family — which cameras can show a live
picture in the panel, at what cost — is documented in
docs/live-video.md.
A USB control surface drives the bridge instead of the other way round: a
gamepad, or a panel you build yourself. A PlayStation controller is recognised
by vendor/product id and gets a drive mapping (left stick pan/tilt, analog
triggers zoom, face buttons presets); anything unknown keeps the paint
mapping it always had. That split is a safety boundary and not a convenience —
setIris is a 9-byte VISCA packet whose bytes 6/7 the DigitalBird DB3 decoder
reads as a pan/tilt direction, so a stick on iris makes that head drive off.
The parts list for a panel of your own, and the compatibility check against the
DB3 head, are in docs/ptz-pult-hardware.md; the
two things that still need a real device are in
docs/ptz-pult-messprotokoll.md.
The bridge reads the camera-list the AV Planner Suite's MultiCam Planner
exports and holds it against the cameras on the bus, so the control wall can
label a slot the way the show calls it ("CAM 3 — Bühne links") instead of by
number. Every match carries evidence — a measured model, a mere convention, or a
human — and where there is none, there is no match. See
docs/camera-plan.md.
A B4 lens reports its name, open f-number, focal lengths and MOD over the
serial protocol. Those are readings from one piece of glass, not
capabilities of this repo, so they stay out of the caps:parity contract — the
reasoning, and where they live instead, is in
docs/b4/lens-facts-vs-capabilities.md.
A zoom or focus demand wired to the B4 interface becomes an input on the same
bus: its wiper drives setZoom/setFocus on a chosen camera, through a table
measured on that demand, and an unplugged demand stops the axis instead of
holding its last value. Without the bridge the same firmware can appear as a
USB gamepad. See docs/b4/demand.md.
| Path | What it is |
|---|---|
packages/bridge |
Node/TypeScript bridge server, camera clients, protocols, HID input, switcher (src/switcher), video hub (src/multiview), site file (src/site) |
packages/web-rcp |
React UI: Cameras (RCP + PTZ panel, wizard), Wall, Switcher, Video, Site |
packages/companion-module-lz-camera-bridge |
Bitfocus Companion module |
packages/electron-app |
Desktop application: the web UI and the bridge, bundled into one installer |
packages/firmware |
WIZ108SR serial↔TCP bridge firmware (C) |
Source language: en. New user-facing text goes in English. That is not a
preference but a measurement: the UI already carries roughly 140 English spots
against 32 German ones, and its vocabulary is the technical one anyway — Iris,
Gain, ND, Paint. Rewriting the English into German to then put „Blende" next to
WB costs work and makes the result worse. Decided 2026-09-08 (E-17);
multicam-planner is English-source as well, while cable-planner and
light-planner are German-source. The machine-readable copy of this
declaration sits in package.json under avplan.sourceLanguage.
The German spots that are still there are a mixed-language product, not a
translation backlog — the same dialog shows both. npm run lang:check counts
them and holds the count at the level measured when the check was introduced:
while that cleanup is open, the existing ones may stay, but the mix must not
grow. Translating some means lowering the limit in the same commit.
npm install # workspaces install
./dev.sh # Linux / macOS — bridge + web UI, demo camera preselected
npm run dev # the same via npm (bridge backgrounded with `&`)
npm test # protocol framing unit testsPick the Demo (no camera) tab in the connection panel and connect. The panel then talks to a state held in the bridge: move a control and the value follows, so the surface — RCP, joystick, layout — can be tried on a laptop.
Before this, npm run dev always started but could not show anything:
every backend in backendFactory needs a real address (a CCU on TCP, a VISCA
port, a USB device in PC-Remote mode). Without one the panel came up empty
and every control was inert.
It is not a camera simulator. Nothing drifts, nothing is measured, and
every value moved because a command moved it. The state carries an isDemo
flag all the way to the dashboard, so a demo value can never be mistaken for
a reading from a real device — that is the same line valueOrigin.ts draws
between commanded and confirmed, and test/demoKamera.test.ts holds it:
one case asserts that nothing changes over 600 ms with no command, another
that isDemo survives mapState.
dev.sh also cleans up after itself. npm run dev appends the bridge with
& and leaves it: a Ctrl-C ends only the web UI, the bridge stays on port
9700, and the next start fails on it.
The bridge binds every interface and now prints the addresses you can hand
out, not just localhost:
[BridgeServer] WebSocket listening on ws://localhost:9700
[BridgeServer] ws://192.168.1.42:9700 (same network)
All detected addresses are listed rather than one being guessed: on a machine with a Docker or VPN bridge the first one is often the wrong one.
Build individually:
npm run build --workspace=packages/bridge
npm run build --workspace=packages/web-rcpOptional native modules (loaded lazily, not required to build/run):
npm install usb --workspace=packages/bridge # Sony USB (PTP) discovery/control
npm install node-hid --workspace=packages/bridge # USB control-panel inputOpen the web UI and follow the setup wizard: pick a camera family, enter its address (the wizard shows the right defaults per protocol), and connect. PTZ cameras open the joystick panel automatically; paint cameras open the RCP.
The header switches between five views: Cameras (one camera, its connection and panel), Wall (every camera's panel at once), Switcher, Video and Site. Under Cameras every camera also takes a name, its RTSP stream address and its switcher input — that is all the Video and Switcher views need. A room that already exists as a site file goes in under Site → Import; the bridge then connects everything by itself.
- Sony BRC/SRG PTZ: choose the VISCA over IP tab and set port 52381 —
the Sony transport header is applied automatically. Where the room blocks
the VISCA ports, the HTTP-CGI PTZ tab drives the same head over its web
CGI (family Sony, login required; power is read back). Over this CGI the
Sony also takes manual white balance: R and B gain (
imaging.cgiWhiteBalanceCrGain/CbGain, read back frominquiry.cgi?inq=imaging). It has no G gain and, over the CGI, no hue, saturation, black or gamma. - Vissonic / PTZOptics PTZ: HTTP-CGI PTZ, family Vissonic. No login
for control; power on/standby goes over VISCA TCP 5678 and is read back.
Stream address
rtsp://<ip>:554/1. - VIS-CATC switcher: the Switcher view, + Switcher, address of the device page. Give each camera its switcher input in the connection panel.
- Sony FX/Alpha USB: put the camera in PC Remote mode; install the
usbmodule on the host. - Canon: activate CCAPI once via Canon's tool, then enter the shown IP/port.
- VISCA on a cable: the VISCA RS‑232 tab. Most heads ship at 9600 baud, address 1. Not the same as the 8‑Pin RS‑422 tab next to it — that is Sony's 9‑pin protocol on a different cable with different framing (odd parity).
- DJI gimbals: the DJI Ronin and DJI Osmo Pocket tabs move the head
only. Read
docs/dji-gimbal.mdfirst — it says what is proven and what is not, and there is no USB control path for the Pocket.
Proprietär — © 2026 Lars Zumpe, alle Rechte vorbehalten. Nutzung der veröffentlichten Builds ist kostenlos; Weiterverbreitung und abgeleitete Werke sind es nicht. Siehe LICENSE.




