Skip to content

Latest commit

 

History

History
242 lines (203 loc) · 12.2 KB

File metadata and controls

242 lines (203 loc) · 12.2 KB

video

Streams a local video file to a relay as a CMSF broadcast (draft-ietf-moq-cmsf-01: CMAF packaging on top of MSF draft-ietf-moq-msf-01 and MoQ Transport draft-ietf-moq-transport-20), and measures what comes back out.

It exists to answer one question about a suspected delivery fault: is the transport at fault, or the encoder feeding it? Every byte the publisher sends is known in advance, so the subscriber can say exactly what arrived, in what order and how late — and can reassemble the Objects it received into a file that either matches the source byte for byte or does not. A clean run points at the capture and encode path; a dirty one points at the transport.

Usage

video [flags] publish | subscribe

Start the relay, then the two halves in either order:

go run ./cmd/relay                                    # :4433, self-signed cert
go run ./cmd/video -out /tmp/recv.mp4 subscribe
go run ./cmd/video -in clip.mp4 publish
Flag Default Mode Description
-addr localhost:4433 both Relay address, host:port or a moqt:// URI
-ns moq-example/video both Track namespace
-in — publish Video file to stream (required)
-rate 1 publish Pacing multiplier; 0 sends as fast as the transport takes it
-loop 1 publish Passes over the file; 0 repeats until interrupted
-gop 0 publish Minimum Objects per Group; 0 starts a Group at every sync sample
-delay 2s publish Pause between the catalog and the first frame
-out — subscribe Where to reassemble the received media; - sends it to stdout
-wait 30s subscribe How long to wait for a publisher on the namespace
-packaging cmaf subscribe cmaf for a broadcast this tool published, legacy for moq-lite/hang

Any MP4 the file reader understands works — progressive or already fragmented, AVC / HEVC / AV1 / VP9. Only the first video track is published; audio, subtitle and data tracks are ignored.

Connections use InsecureSkipVerify: true so they work against the relay's ephemeral self-signed cert. SIGINT / SIGTERM ends either side cleanly, and the subscriber writes its report either way — a run cut short still says what arrived before it was.

Reading the report

=== delivery report ===
objects   180 received, 0 missing (groups 6 received, 0 missing)
bytes     1516520 over 5.967s
order     0 objects arrived after a later one
latency   p50 939µs  p90 1.6ms  p99 3ms  max 3.7ms  (180 objects)
          slowest 0/25 3.7ms
          slowest 5/7 3.1ms
spacing   p50 33.4ms  p99 36.9ms  max 39.5ms  (mean 33.2ms)
digest    211deb6c63d4dcaf14bfa31e5b62b6682d0c2b0b085c54253df1118cff7c96d0
          MATCHES the source: every byte arrived, in order
  • objects / groups — anything missing is loss, counted only between the first and last that arrived, so joining mid-broadcast is not reported as a failure.
  • order — Objects that arrived after one the publisher sent later. One Group is one subgroup stream and streams are read concurrently, so a Group can overtake the tail of the one before it. That is inherent to MoQ, not a fault; it is here because a player that ignores it shows artefacts.
  • latency — send to receive, per Object, from a producer-defined Object Property the publisher stamps (type 0x3800, in the range §2.5 reserves for application-specific use, which IANA will never allocate and a relay must forward unchanged). Only meaningful with both ends on one machine, since it subtracts two clocks. The slowest lines name the worst Objects by group/object, because a spike is a handful of Objects and an average hides it.
  • spacing — gaps between successive arrivals in send order. A stall shows here even when latency cannot be measured.
  • digest — SHA-256 of the CMAF header plus every Object payload in order, against the same value the publisher declared in the catalog. Needs -out, which is the only thing that retains payloads: without it a run keeps one small record per Object and not the media, which is what makes -loop 0 soak testing practical.

The file -out writes is a playable fragmented MP4. If the digest matches and it still shows artefacts, the artefacts were encoded into the source.

How the file maps onto MoQ

CMAF MoQ Draft
Init header (ftyp+moov) catalog initDataList, inline base64 CMSF §3.1
One chunk (moof+mdat, one frame) one Object CMSF §3.3
Fragment starting at a sync sample one Group, one subgroup stream CMSF §3.4

One frame per Object rather than one GOP is deliberate: an Object that spans a GOP reports one arrival time for two seconds of video and hides exactly the spikes this is looking for. It is also what a low-latency CMAF publisher emits, so nothing about the shape is diagnostic-only.

-gop N widens Groups without breaking §3.4 — a sync sample reached before the open Group holds N Objects becomes an ordinary mid-Group Object rather than starting a new one. Use it to compare "many short streams" against "few long ones".

The catalog declares packaging: "cmaf" (§3.5.1) and both SAP fields (§3.5.2) as 2. That is the only conformant value: both fields are maxima, §3.4 pins a Group's first Object to SAP type 1 or 2 on a cmaf track, and pkg/moqt/msf rejects anything outside that. Input that may not satisfy §3.4 is flagged at publish time instead — see Limitations.

Subscribing to someone else's broadcast

-packaging legacy reads the bare-bitstream tracks the moq-lite/hang stack serves, which is how you point this at a publisher that is not moq-go:

go run ./cmd/video -addr moqt://cdn.moq.pro/demo -ns bbb.hang \
    -packaging legacy -out /tmp/bbb.mp4 subscribe

That produces a playable 1280x720 file and a delivery report. Such a track's Objects are a QUIC varint timestamp in microseconds followed by one Annex-B access unit — no container — and its catalog declares packaging: "legacy", which is not an MSF §5.2.4 value, so msf rejects it and this mode treats validation as advisory. There is no initDataList entry either: the codec is avc3, so the parameter sets are in the bitstream and the SPS/PPS opening the first Group become the decoder configuration.

Two things are dropped on the way into the file, and neither is dropped from the report: the frames ahead of the first keyframe, which a live join lands in the middle of and which reference a picture that was never sent; and the occasional access unit carrying no picture at all — one in a few hundred on bbb.hang holds a single reserved-type NALU. Both decode as errors, which in this tool would read as corruption.

Playing it

-out - sends the media to stdout instead of to a file, so it can go straight into a player. The report moves to stderr to keep the pipe clean:

go run ./cmd/video -addr moqt://cdn.moq.pro/demo -ns bbb.hang \
    -packaging legacy -out - subscribe | mpv --vo=tct -   # ASCII, in the terminal
go run ./cmd/video ... -out - subscribe | ffplay -        # a window

Decoding is the player's job rather than this tool's: drawing frames would mean carrying an H.264 decoder, and every terminal player already has one. mpv --vo=tct and chafa both render into a terminal — brew install mpv if neither is present.

Each segment goes out headed by a styp and written in a single call. Neither is cosmetic: mp4ff encodes a fragment box by box, and a player reading the other end of a pipe can wake on half a moof, at which point ffmpeg's demuxer sanity-checks the trun against how much input it thinks remains, gets a negative answer, and stops — which mpv reports as end of file a few seconds in. The same bytes read from a file always parsed perfectly, which is what made it look like a timing fault.

Fragments carry half a second of samples rather than one frame. The Objects are still one frame each, and that is what the report measures — this is only how they are framed on the way out. ffmpeg's demuxer sanity-checks every trun against how much input it believes remains, and on a growing non-seekable stream that estimate goes negative from time to time; at twenty-four truns a second the odds catch up within half a minute, which is what "plays about twenty-five seconds and stops" was.

It is better rather than perfect. mpv still occasionally complains, though it now recovers instead of stopping, and ffplay fares better than mpv because it does not attempt the backward seek mpv does on a stream that cannot seek. If playback still stops early, -out to a file and play that — the file is always intact, and the report says whether the transport was at fault.

A run that receives nothing says so: no objects for a while in the log means the publisher or relay stopped sending on the subscription. Nothing is reported as lost, because nothing was — it was never sent. That is a broadcast-side problem, not a delivery one, and no player can do anything with it.

In this mode the timing figures are consumer-bound. A player that pauses stops reading the pipe, the writer waits, and the Objects behind it are stamped when they are read rather than when they landed — so latency and spacing describe the player as much as the transport. That is the right trade here: the alternative, dropping whatever the player was too slow to take, hands it a stream with holes and playback stops. Measure with -out to a file, or with no -out at all.

What the report loses: latency needs the publisher's send stamp and the digest comparison needs the source counts, and a foreign broadcast declares neither. Ordering, loss, spacing and the playable file all still work — and they are measured against a publisher that shares no code with this one, which is the only way the numbers mean anything beyond moq-go agreeing with itself.

Tracks

Both tracks live under -ns:

Track name Packaging Contents
catalog (MSF catalog) The broadcast description, plus the CMAF header and the source's digest. One Object per Group.
video cmaf One CMAF chunk per Object, one Group per GOP.

The catalog is published once at start (§5: a catalog Object SHOULD be published only when track availability changes), so a subscriber that connects later backfills it with a fill fetch stream (§5.1.3). The publisher closes with the §11.3 terminator catalog, which is what ends the subscriber's run and triggers its report.

Limitations

  • Open-GOP input is warned about, not rejected. A file whose Groups open on an access point with leading pictures may be starting them at SAP type 3, where CMSF §3.4 requires 1 or 2 — and it would then break up at every Group boundary for reasons belonging to the file while reporting a perfect digest, which is the exact false positive this tool exists not to produce. Whether those leading pictures are RASL (undecodable, SAP 3) or RADL (decodable, SAP 2) cannot be read from the container at all, only from slice headers, so the publisher flags the input rather than judging it. Re-encode with closed GOPs to rule the encoder out; if the artefacts survive that, the result means something. Detection reads composition times, not the is_leading field ISO/IEC 14496-12 defines for it — ffmpeg writes is_leading as 0 ("unknown") on every sample of both open- and closed-GOP output, so a check reading it never fires on the files this is pointed at.
  • Video only. Audio would double the tracks for no extra signal about video artefacts, and would complicate the byte comparison.
  • The whole file is held in memory by the publisher, and by the subscriber too when -out is set. These are debug clips; streaming chunks off disk would put file I/O inside the loop being measured.
  • Latency needs one machine. Across two, the send stamp and the receive stamp come from different clocks and only spacing, order and the loss counts mean anything.
  • The publisher lingers a second after its last write. Closing a QUIC connection abandons whatever it has not had acknowledged, and without the pause the terminator catalog is exactly what gets dropped.