ados-video unit supervises the encoder and the local
MediaMTX instance. On the ground station ados-groundlink owns the radio
receiver and the fan-out, and its --ground-video mode (the ados-mediamtx-gs
unit) runs a second MediaMTX instance that reads the radio stream and serves the
browser.
The hop map
This is the list to measure against. Numbers in brackets are the hop labels used byados diag video.
main is the canonical path name on both nodes. MediaMTX serves WHEP at
127.0.0.1:8889/main/whep, on loopback only; browsers reach it through
ados-control at :8080/whep. Nothing publishes to /ados, so a zero byte count on that
path is not evidence of a video fault.
Stage 1: Capture
The agent scans/dev/video* at boot and supports MIPI CSI cameras and USB UVC
cameras through V4L2. It sets resolution, framerate and pixel format before
starting the encoder. The shipped default is 1280x720 at 30 fps.
The encoder choice comes from the board’s HAL profile. Rockchip boards with a
media engine can use mpph264enc through GStreamer. Boards exposing a V4L2
hardware encoder use h264_v4l2m2m through ffmpeg. Everything else falls back to
libx264.
Stage 2: Encode
One process does the encode and both publishes. The default path (USB and IP cameras) is a single ffmpeg whose output is ffmpeg’stee muxer: branch A is the
RTSP publish into the local MediaMTX, branch B is the RTP copy the radio reads.
Parameter sets repeat before every IDR
The RTSP muxer advertisesAVFMT_GLOBALHEADER, which makes libx264 put SPS and
PPS only in extradata (the SDP) and never in the stream. A browser that loses
sync then has nothing in-band to re-bootstrap from and freezes on its last
decoded frame while the PeerConnection still reads connected.
Every encode arm repeats the parameter sets in-band per IDR to close that:
dump_extra=freq=keyframe on libx264 and h264_v4l2m2m, --inline on
rpicam-vid, h264parse config-interval=1 on the GStreamer arms plus
mpph264enc header-mode=1 at the encoder. The ground ingest re-applies the same
bitstream filter, which is a no-op against a current drone and a repair against
an older one.
There is no keyframe-on-demand, by design
The GOP is the recovery mechanism. A decoder that loses sync re-bootstraps at the next IDR, which is at most 500 ms away at 30 fps with-g 15.
There is no PLI or keyframe-request path.
The shipped encoder leaves (ffmpeg CLI, rpicam-vid, gst-launch-1.0) expose
no runtime control channel an IDR could be requested through, and MediaMTX does
not forward a WebRTC receiver’s PLI to an RTSP publisher. The most a request
could save is the 0 to 500 ms a decoder waits, so about 250 ms expected, on an
event that only happens after loss the FEC did not recover. Respawning the
encoder to force an IDR costs a 1 to 2 second blackout, which is strictly worse
than waiting.
H.264 is the default codec
H.264 is the shipped wire codec. H.265 saves bandwidth at the same quality, andvideo.camera.codec accepts it, but browser WebRTC support for H.265 is not
consistent enough to ship as a default and Mission Control’s player pins
avc1.640029. Test your playback path before switching.
Stage 3: WFB-ng transport
WFB-ng puts an RTL8812EU adapter in monitor mode and injects FEC-encoded packets on 5 GHz. This is not standard WiFi: no association, no handshake, no retransmission, no CSMA/CA backoff.
The framing between the encoder and the radio is RTP (RFC 6184), payload type 96,
SSRC
0xCAFE, pkt_size 1316, on UDP 5600. RTP is the contract, not a detail: a
lost RTP 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 in its MediaMTX path config (rtpSDP, shown in Stage 4).
On CSI boards the encoder is
rpicam-vid or gst-launch-1.0 publishing through a
shell pipeline, and there the RTP copy is still produced by a separate tap process
with its own watchdog. Check which encoder is running (the encoder field in
camera-state.json, or the Video row of ados status) before reasoning about
hop 4.rtl8812eu driver actually applies. Raising the cap without a range sweep is how
a link that looks perfect on a bench dies at 150 m.
Stage 4: Ground receive and serve
wfb_rx -p 0 decodes the radio packets and FEC-recovers gaps, then the fan-out
forwards them to the ground MediaMTX on UDP 5600 and to the on-box display on
5605. No ffmpeg sits in between. ados-groundlink --ground-video writes a
MediaMTX config whose main path has a native UDP RTP source and the SDP that
describes it:
MediaMTX does no transcoding. The H.264 stream passes through to WebRTC as it
arrived.
Both MediaMTX instances are rendered from one config generator, so the air and
ground documents cannot drift apart silently. The file is written to
/etc/ados/mediamtx.yml.
WebRTC and ICE
The relevant MediaMTX keys, with the values the LAN profiles get:Stage 5: Browser decode
The browser receives H.264 RTP over WebRTC and decodes with the platform hardware decoder. Mission Control’ssrc/lib/video/webrtc-client.ts handles WHEP
negotiation, ICE gathering, track attachment and connection health.
Video reaches a browser either from the ground station over the radio hop above,
or over the cloud relay described below.
Video transport modes
Mission Control’s transport switcher offers four modes:
Under
p2p-mqtt the browser and the agent’s ados-cloud service exchange SDP
over MQTT:
http://localhost:8889/main/whep) and publishes MediaMTX’s answer on that
session’s own answer topic, so concurrent viewers never see each other’s SDP.
A close message, or the browser’s MQTT last will when it vanishes, ends the WHEP
session. Once the exchange completes the media flows peer-to-peer through
STUN-negotiated NAT traversal; the broker carries signalling only, never video.
The full topic list is on
Cloud infrastructure.
p2p-mqtt needs internet access on both sides. Behind symmetric NAT, which some
cellular carriers use, ICE negotiation can fail outright. The transport switcher
reports the failing stage.What is measured, and what is not
One latency number in this stack has been measured, and it is not the number an operator means by “latency”. Publish-side, stamp to readback, sim bench, 2026-09-15. The SEI probe injects a timestamp after the encoder and reads it back from the drone’s ownrtsp://localhost:8554/main.
That number covers four things: the publish mux, the RTSP publish, MediaMTX on
the drone, and the readback demux.
It excludes capture, encode, the radio hop, the ground receive, the ground
MediaMTX and the browser. Those are most of what an operator experiences.
There is no glass-to-glass figure for ADOS, and this is not one. Do not read
the 74 ms as end to end, and do not add per-stage guesses to it to manufacture a
total. A real air-to-ground figure needs the same probe run on the ground node
against its own
rtsp://127.0.0.1:8554/main with clock-aligned nodes. A real
glass-to-glass figure needs a camera-side marker and a photograph of the viewer.
Neither has been done.
Two things to know before quoting any reading from the probe:
- It is off by default and gated on
video.wfb.sei_latency, because it splices a Python stage into the byte path. Bench profiles only. /api/video/latencyreads the tap’s sidecar file with no freshness gate, so a stale reading can look live. Confirm an injector is actually in the byte path before trusting a number.
ados diag video samples the reliable
cumulative counters at each hop twice and attributes a stall to the hop that
stopped. That is the supported first move on a dark feed. See
Diagnostics.
Recording
Recording happens inside MediaMTX, writing fMP4 segments with no extra encode pass. It is off by default;video.recording.enabled turns it on and
video.recording.retention bounds how long segments are kept. The segment set on
disk is the pre-roll ring, and clips are cut out of it after the fact with
ados record clip. See
Video pipeline.
Next steps
Video pipeline
Configure the camera, encoder and recording on a node.
Diagnostics
Run the per-hop video verdict with
ados diag video.Cloud infrastructure
The MQTT topics and the Convex status path.