Skip to main content

Multi-Camera

One airframe can carry several cameras: an EO, a wide EO, a thermal, a down-facing landing cam. Each one is a leg, and each leg is served as its own mediamtx path. Exactly one leg is the primary. It is the leg that rides the radio and the cloud relay. Every other leg is served over the LAN only.

One camera, or legs

The legacy single-camera shape is still the whole config for a one-camera node:
Declaring video.cameras replaces it. When video.cameras is absent the agent resolves the single video.camera block verbatim as one primary leg with the id main, so an existing config is byte-identical after the multi-leg support landed.
Do not hand-edit the generated mediamtx.yml. Change video.* in /etc/ados/config.yaml (or write the roster through the API) and let the agent regenerate it.

Per-leg fields

Identity and transport

A source starting rtsp://, rtsps://, http:// or https:// is a network capture URL. Everything else is a local device.

Encode

These are read by the encode pipeline and reach the encoder command builder.

Roster metadata

These are surfaced through the camera roster and consumed by extensions. The encode and radio pipeline reads none of them. role and purpose are complementary, not alternatives. role is the transport plane (which leg rides the radio). purpose is the consumer plane (what an extension binds to). A leg can be the primary transport and also be the detect source.

What primary means

Selection rules, in order:
  1. The first leg whose role is exactly primary wins, wherever it sits in the list.
  2. If no leg declares role: primary, the first leg in the list is the primary.
The primary’s path id is rewritten to main so the radio, the cloud relay and every downstream consumer keep one fixed path name. Its declared role string is preserved for the Mission Control label map, so a primary declared as role: eo still renders as an EO leg while being served at main. A secondary with a network source becomes a mediamtx sourceOnDemand pull: no agent-owned encoder process, and mediamtx only connects upstream when a viewer asks. A secondary on a local device does get its own agent-owned encoder, using the same command builder as the primary.

Airtime and the thumbnail profile

One fleet shares one 20 MHz channel, so exactly one aircraft streams full video at a time. The primary leg’s top-level width / height / fps / bitrate_kbps are the hero profile. The thumbnail sub-block is the non-hero profile the drone falls back to while the operator’s attention is on a different aircraft:
Those four values are the defaults. Only the primary leg rides the shared channel, so only the primary is attention-switched; the thumbnail block is unread on a secondary leg. Mission Control drives the switch through POST /api/video/profile.

The camera roster

GET /api/video/roster is the one place an operator sees every camera the node knows about. It reconciles three sources into one list:
  • the legs declared in video.cameras (or the legacy video.camera block, synthesised as main),
  • the devices the hardware enumeration discovered (/run/ados/cameras-discovered.json),
  • the live stream state (/run/ados/video-streams.json).
Each row carries the leg’s logical identity, its management metadata and a state: The read is guaranteed 200 and degrades to {"cameras": []} when the sidecars are absent. A ground station carries no onboard camera, so it serves an empty roster rather than a phantom main.
/api/video/roster is not /api/video/cameras. The latter is the ground station’s switchable-camera enumeration ({cameras, assignments}) that the camera-switch control uses, and it is a different surface.

Writing the roster

PUT /api/video/roster persists the operator’s leg list. It validates before it writes:
  • ids must be unique and path-safe,
  • orientation and purpose must be known values,
  • no non-primary leg may claim the reserved id main.
A validation failure is a 400. The write then forwards a video.cameras.set op to the supervisor’s video command socket (/run/ados/video-cmd.sock), which is the config-write and restart authority. An unreachable supervisor is a 503; a saved-but-not-restarted result is a 502. The persist is a merge by owner. An operator write preserves the legs an extension declared and an extension write preserves the operator’s, so a driver extension that declares a pod’s legs does not lose them the next time you edit the roster in Mission Control. A client-supplied owner on a leg is ignored; the supervisor stamps operator on an operator write. A ground station returns 404 for the write: there is no onboard camera to manage there.

Ports