Repository navigation
Dev Camera Relay Bridge Protocol
A source bridge is a separate process that receives a mower's camera from a vendor's media cloud with that vendor's SDK and hands the encoded frames to camera-relay (ADR 0045). There is one bridge process per source session (per channel join), and it talks to the relay over its standard streams only; when the bridge runs in a container of its own, a socket transport carries the same bytes. The protocol is vendor neutral: agora-bridge is the first implementation, and any other SDK can be wrapped the same way. Version 1 is described here.
| Stream | Direction | Content |
|---|---|---|
| stdin | relay → bridge | Control lines: one JSON object per line, UTF-8, terminated by \n
|
| stdout | bridge → relay | Binary records: frames and events |
| stderr | bridge → relay | Plain log lines for the relay's log, never a secret |
Secrets travel only on stdin: never in arguments, environment variables, files or logs. The bridge holds them in memory only and writes no file. Closing stdin means leave: the bridge leaves the channel and exits.
Every line has an op. Unknown fields are ignored; an unknown op is answered with an error event and otherwise ignored.
op |
Fields | Meaning |
|---|---|---|
join |
app_id, channel, token, uid, encryption ({mode, key, salt} or null), optional profile
|
Join as a receive-only member with the session's credentials. Sent once, first. |
renew |
token, optional uid
|
Renew the token on the running connection. When the bridge cannot (the SDK refuses, or the uid differs and the SDK needs a rejoin), it reports renew_failed and the relay joins again with the fresh session. |
subscribe |
uids (list) |
Subscribe to these publishers' encoded video. Publishers that join later are subscribed when listed. |
keyframe |
uid |
Ask the publisher for a key frame. Best effort: some publishers ignore it. |
leave |
Leave the channel, flush, exit with code 0. |
Example (values shortened):
{"op": "join", "app_id": "…", "channel": "…", "token": "…", "uid": 123456, "encryption": null}
{"op": "subscribe", "uids": [1, 2, 3]}Every record is a 4-byte big-endian length L followed by L bytes: one type byte and the body. L is at most 4 MiB + 64; a longer record is a protocol error and the relay stops the bridge.
| Field | Size | Meaning |
|---|---|---|
uid |
u32 BE | Publisher uid (one camera) |
codec |
u8 |
1 H.264, 2 H.265 (Annex-B) |
flags |
u8 | bit 0 keyframe, bit 1 capture_ts present |
recv_mono_ns |
u64 BE | When the bridge received the frame, on its monotonic clock |
capture_ts |
u64 BE | Sender's capture time in ms, if bit 1 is set (else 0) |
bytes |
rest | One access unit in Annex-B; a key frame carries its parameter sets |
capture_ts is the sender's own clock, not wall time (on the LUBA a monotonic clock in 100 ms steps): it is informational and never used for latency. The relay takes presentation timestamps from recv_mono_ns (the true intervals between frames as they arrived), continued monotonically across bridge processes.
A UTF-8 JSON object with an event field:
event |
Fields | Meaning |
|---|---|---|
hello |
protocol (1), bridge, sdk
|
First record after start |
joined |
elapsed_ms |
The connection is up |
publisher |
uid, state (published, unpublished) |
A camera started or stopped publishing |
video_state |
uid, state (starting, decoding, frozen, stopped), optional dropped
|
The SDK's view of a camera's stream and frames dropped since the last report, for metrics |
token_will_expire |
The SDK warns that the token runs out soon | |
renewed / renew_failed
|
reason (on failure) |
Outcome of a renew
|
error |
code, detail
|
A failure; fatal ones are followed by exit |
left |
The channel was left; the bridge exits next |
Events never carry a token, key or salt.
- The bridge never blocks the SDK's callback thread: it copies each buffer into a bounded queue. When the queue is full it drops delta frames until the next key frame of that uid and reports the count in the next
video_stateevent (dropped). - It sends nothing it did not receive: no repeated or synthetic frames.
- Exit codes:
0afterleaveor closed stdin,2bad input,3SDK failed to load,4join failed,1anything else. The relay treats any exit outsideleaveas a source failure (stateFAILED, retry with backoff). - The bridge makes no network calls besides the SDK's own.
The relay image never contains a vendor SDK, so a bridge built as an image of its own (agora-bridge) is reached over TCP: a small server in the bridge's container listens on the relay's loopback (the containers share a network namespace) and starts one bridge process per connection.
- The connection carries exactly the bytes above: what the relay writes is the bridge's stdin, what the bridge writes to stdout comes back. stderr stays in the server's log.
- The server starts the bridge at the first control line; a connection that closes before is a reachability probe and starts nothing (the relay answers
unsupported_providerwhile nothing listens). - When the bridge process ends, the server sends one more event record,
{"event": "exit", "code": N}with the exit code above, and closes the connection. A server that runs too many bridges answerserror(codebusy) andexit1. - The relay closing the connection closes the bridge's stdin, which means
leave.
How camera-relay uses a bridge (services/camera-relay, adapters/media/bridge_source.py):
-
joinwith the vendor session, thensubscribewith the uids of the leased cameras; a camera whose uid never publishes never gets media and blocks nothing. - Per camera, from its first key frame on, the access units go into an MPEG transport stream with presentation times from
recv_mono_nsand are published to the media server as H.265 (r/<rid>/<cam>/src); the generic transcoder makes the H.264 viewer stream. Nothing is repeated or invented; a muxer that falls behind drops delta frames until the next key frame. - Renewal about five minutes before the session expires: a fresh session from the gateway; the same channel gets
renewon the running connection, otherwise (or onrenew_failed, no answer within 10 s, ortoken_will_expireafterwards) a second connection joins with the fresh session, each camera switches to it at its first key frame, and the old connection getsleave. - The active connection ending without
leave(anexitrecord, a closed connection, a protocol error) fails the source: the session goesFAILEDwith its backoff.
The relay's tests run against a fake bridge that replays a recorded stream through this protocol, so CI needs no vendor SDK.
Mirrored from docs/dev/camera-relay-bridge-protocol.md at commit a46c22cf085b9be11366cd38887abd045c842630. Edit the file in the repository, not this wiki page.
Mirrored from the docs/ directory at commit a46c22cf085b9be11366cd38887abd045c842630. Edit the files in the repository, not this wiki.
MowDeck documentation
- Home
- Phases
- Architecture
-
Decisions (ADR)
- Architecture decisions
- ADR 0001: OpenFGA instead of self-built authorization
- ADR 0002: Self-built Identity service in FastAPI instead of Keycloak or Zitadel
- ADR 0003: SvelteKit for the web app
- ADR 0004: Monorepo with a uv workspace
- ADR 0005: AGPL-3.0 for services and apps, Apache-2.0 for libraries
- ADR 0006: PostgreSQL for all persistence, no SQLite
- ADR 0007: NATS JetStream as event bus
- ADR 0008: Traefik as gateway
- ADR 0009: Command and event envelopes, subject naming and de-duplication
- ADR 0010: Python 3.13 for all services and libraries
- ADR 0011: Separate GPL-3.0 package for the PyMammotion adapter
- ADR 0012: Mower control interface and safety rules
- ADR 0013: Device-Gateway registry, sharding and per-mower concurrency
- ADR 0014: Authorization model, tuple ownership and the mowdeck-authz client
- ADR 0015: Tokens, sessions and key rotation
- ADR 0016: Fleet commands and authorization per command kind
- ADR 0017: Telemetry storage and retention
- ADR 0018: Realtime WebSocket protocol
- ADR 0019: Mammotion adapter transports, manual drive and secrets
- ADR 0020: Web app architecture
- ADR 0021: Browser authentication flow
- ADR 0022: Notifications: rules, recipients and delivery
- ADR 0023: Map rendering and projection
- ADR 0024: Scheduler: recurrence, rain pause and command issuing
- ADR 0025: Home Assistant integration as a pure API client
- ADR 0026: Manual drive channel over the Realtime WebSocket
- ADR 0027: Health history and theft alarm
- ADR 0028: Entitlements: plans as data and enforcement
- ADR 0029: Helm chart and release pipeline
- ADR 0030: Mowing patterns as plug-ins and the Planner
- ADR 0031: Billing with a Stripe adapter
- ADR 0032: Live camera over Agora, joined directly by the browser
- ADR 0033: Vision-assisted recovery with human approval
- ADR 0034: Linked cloud accounts and device discovery
- ADR 0035: Server-side camera frames and automatic AI analysis on errors
- ADR 0036: Native app shells with Capacitor, loading the web app from the server
- ADR 0037: Camera panorama calibration in the browser, with the AI supplying points only
- ADR 0038: Dual link per mower and firmware updates
- ADR 0039: Spot mowing by MowDeck-driven paths
- ADR 0040: Link heartbeat for online status
- ADR 0041: Spot runs from the dock: undock, transit and return
- ADR 0042: Drive calibration
- ADR 0043: Turn styles
- ADR 0044: Drive view with camera
- ADR 0045: Camera relay
- Development
- Operations
- Spikes
- Guides
- Original plan (German)