Skip to main content
The video pipeline takes a camera feed, encodes it to H.264 (or H.265), serves it through a local MediaMTX, and transmits it over WFB-ng for long-range video or hands it to the cloud relay for remote viewing. The pipeline runs as the ados-video systemd service (a native Rust binary) on the drone profile.

Pipeline flow

Camera (V4L2 or CSI) feeds the encoder, which encodes H.264 once and writes it to two muxers through a tee. Branch A is the RTSP publish into the local MediaMTX, which serves WebRTC and HLS through the agent’s port 8080 and feeds the cloud relay. Branch B is an RTP copy on UDP 5600, which is what wfb_tx reads for the radio. The RTP framing matters. wfb_tx needs each datagram to survive loss on its own, and RTP (RFC 6184) gives that: a lost packet costs at most one NAL fragment, where a lost chunk of a raw byte stream corrupts everything up to the next start code. The receiver depayloads against the SDP at /etc/ados/wfb/video.sdp. When radio video goes dark, those are the two things to check on the drone: that something is producing on UDP 5600, and that /etc/ados/wfb/video.sdp matches what the encoder is emitting. ados diag video does both and names the hop that stopped.

Camera detection

The camera manager scans for cameras at startup:
  1. Checks /dev/video* devices (V4L2)
  2. Queries each with v4l2-ctl for capabilities
  3. Filters for actual video capture devices (skips metadata-only nodes)
  4. Sorts by priority: CSI first, then USB
If no camera is found, the video service stays in idle state and retries on USB hot-plug events.
There is no test-pattern source value. To exercise the pipeline with no camera attached, point source at an RTSP URL served by a local generator.

Hero and thumbnail profiles

A radio-linked drone boots at 1 fps thumbnail until selected. One fleet shares one WFB channel, so only one aircraft streams full video at a time. The ground station or Mission Control promotes one drone to hero with POST /api/video/profile, and that drone switches to its full settings. A one-drone fleet is promoted automatically. Only the primary leg is switched. See Airtime and the thumbnail profile for the configuration block.

Encoding

The encoder builds one ffmpeg command line tuned for low latency, with the radio’s RTP copy as a second tee branch off the same encode:
Key encoding flags: On boards with hardware encoders the encoder switches automatically based on the board profile’s hw_video_codecs field: h264_v4l2m2m through ffmpeg on the Raspberry Pi (the V4L2 M2M device is trial-probed before use), mpph264enc through GStreamer on Rockchip, and omxh264videoenc through GStreamer on Allwinner. Each path repeats the parameter sets per IDR with its own mechanism, so the recovery behaviour is the same everywhere. To force a path, set video.camera.encoder to auto (the default), omx, v4l2m2m or software. A bitrate change does not respawn the encoder unless it is a large one. The adaptive ladder applies small rung moves in place, and only a change of 25% or more restarts the process, because a respawn is a 1 to 2 second blackout.

Recovering from a decoder desync

The GOP is the whole mechanism. Parameter sets in-band per IDR plus a 500 ms GOP means a decoder that loses sync re-bootstraps within half a second. There is no keyframe-on-demand path. The encoders expose no runtime control channel an IDR could be requested through, and MediaMTX does not forward a browser’s PLI to an RTSP publisher. The most a request could save is the 0 to 500 ms wait, and forcing an IDR by respawning the encoder costs several times that in blackout.

H.264 vs H.265

H.264 is the default because it works everywhere. Browser players (MSE and WebRTC) expect it. H.265 saves bandwidth at the same quality, but WebRTC H.265 support in browsers is not consistent, so H.265 is not the default.
If you switch to H.265, test your player first. Mission Control’s WebRTC client expects avc1.640029 (H.264). H.265 streams may need a different playback path.

MediaMTX

MediaMTX is a lightweight RTSP/WebRTC server bundled with the agent. The agent manages its lifecycle: starts it on boot, monitors it and restarts it if it crashes. How a player reaches the stream:
  • RTSP at rtsp://<node>:8554/main for LAN players (VLC, ffplay, GStreamer) and offload nodes.
  • WHEP (WebRTC, browsers and other off-box players) at http://<node>:8080/whep for the primary leg main. A secondary leg is http://<node>:8080/whep?camera=<id>. The request carries the pairing key like any other API call.
  • LL-HLS fallback at http://<node>:8080/hls/main/index.m3u8.
MediaMTX’s own WebRTC and HLS listeners bind loopback (127.0.0.1:8889 and 127.0.0.1:8888) and are not reachable off-box. Media flows on UDP 8189. The agent writes the whole MediaMTX document itself, from one generator shared by the air and ground profiles:
UDP ICE is pinned to one port, 8189, so a firewall rule is one line. STUN is configured only on the cloud-relay profile: on a LAN session the browser connects on host candidates, so a STUN entry would be pure handshake latency before the first frame. The generated paths keep useAbsoluteTimestamp: false on both profiles, so a reader gets frames as soon as the encoder publishes. With it on, MediaMTX would drop every packet until the encoder’s first RTCP sender report, about 5 s of blank video after each encoder start, camera recovery or hero and thumbnail switch. Frame capture time for latency measurement travels in the wall-clock SEI the agent injects (video.wfb.sei_latency), not in RTP timestamps.

Source recovery

Every recovery path in the pipeline retries on one fixed 2 s tick, with no backoff and no attempt limit: the encoder respawn after its source drops, the cold start from the error state, and the consumers of main (cloud push, WFB tap, vision tap, SEI tap, secondary-leg encoders). A camera that comes back is republished on main within one tick plus the encoder’s spawn and connect time, and every consumer restarts on the same tick.
Do not hand-edit /etc/ados/mediamtx.yml. MediaMTX rejects an unrecognised key outright, so a typo does not degrade the node, it leaves the node with no media server at all. Change video.* in /etc/ados/config.yaml and let the agent regenerate the file.

Transports

The same encoded stream leaves the drone on these paths:

Recording

Recording is continuous and segmented, handled inside MediaMTX with no extra encode pass:
Segments land under /var/ados/recordings/<path>/%Y-%m-%d_%H-%M-%S-%f, one minute each. There is no whole-file capture and no maximum duration: the segment set on disk is the pre-roll ring, so an arbitrary window before any event stays extractable after the fact. On a drone, start and stop a node-local capture with POST /api/video/record/start and POST /api/video/record/stop; recorded segments sit under /var/ados/recordings/main/. On a ground station, cut a clip with ados record clip --path <stream> --start <rfc3339> --duration <seconds> -o <file>.
retention is the time bound; the supervisor’s storage janitor also deletes the oldest recordings under disk pressure. Size retention to your card and bitrate anyway, especially on a node you cannot reach.

Snapshots

Grab one JPEG from the live feed:
GET /api/video/snapshot returns one JPEG grabbed live from the main path, with the capture time in the X-Captured-At header. Nothing is written to disk. When no frame is available it answers 503 with E_NO_FRAME.

Pipeline health monitoring

The service checks that bytes keep arriving at MediaMTX’s main path and that the radio tap keeps making progress (15 s window). It restarts a stalled leg and drops to software x264 after three hardware-encoder failures.

Video status

From the terminal:
From the REST API:
The response includes camera details, encoder state, MediaMTX status, recording state and the playback URLs. For a per-hop verdict rather than a summary, ados diag video samples the cumulative counters at each hop twice and names the hop that stopped. See Drone Agent diagnostics.

More than one camera

Everything above describes the single video.camera block. A node with several cameras declares a video.cameras list instead, one entry per video leg, each served as its own MediaMTX path and WHEP leg. See Multi-camera.

Next steps

Multi-camera

Declare several legs and choose which one rides the radio.

WFB-ng

The long-range radio link that carries the primary leg.

Video stack

End-to-end architecture and latency figures.

Drone Agent diagnostics

Find the hop where video stopped.
Last modified on October 16, 2026