1What part 1 left open
Part 1 covered import/export techniques and ideas for H.264, H.265 and AAC, application catalog extensions, and verbatim SCTE-35 ingest. It treated timing in the abstract: “the frame timestamp represents arrival on the program timeline.” It did not touch what happens once that timeline has to survive reconstruction into a transport stream a real broadcast decoder locks to.
That is one gap this report closes. Not with another diagram of what could work, with a MoQ-sourced feed decoded live by a real Integrated/Multi-channel Receiver-Decoder (MRD), driven to SDI, and confirmed on real broadcast gear downstream.
2What is already true in main
Before the new work, two pieces of the timing story were already merged upstream, and this report leans on both. PR #1843 made the reconstructed decode timestamp (DTS) monotonic across B-frame reordering and made the PCR follow that DTS instead of the presentation timestamp. PR #1915 / #1918 exposed a timestamp on the Frame the caller receives and shipped the SRT gateway that paces on it.
Monotonic is not the same as uniform, and that distinction is the entire subject of this report.
3Measure first, then follow the clock
Before touching any code, we did the unglamorous thing: we captured a real feed to a file and measured it, with the same conformance tool broadcast engineers already trust1. The point was to replace opinion with numbers before proposing a fix.
The numbers were blunt. Nothing was corrupt: no clock reference ever ran backward, no packet continuity broke. What was wrong was the timing. Almost a quarter of the clock references arrived too late to meet the broadcast limit, and seven of every ten sat a hair apart, almost on top of each other. A stream can be perfectly ordered and still tick unevenly, and that unevenness was the whole problem.
| Metric | Dirty-start (2.9s, 133 PCR) | Clean capture (30s, 937 PCR) | Verdict |
|---|---|---|---|
| PCR going backward | 0 | 0 | passing |
| Continuity errors | 0 | 0 | passing |
| PCR intervals > 40 ms | 16.5% | 23.7% (222/936), max 133 ms | failing |
| Micro-jumps < 0.1 ms | 69% | 70.0% (655/936) | degenerate |
The same seven-in-ten ratio showed up on two unrelated captures, which rules out a bad start and points at something systemic. A static file cannot tell you whether a real decoder will reject the delivery, but it measures the clock's evenness faithfully, and that is exactly where it came back failing.
We also ruled out the obvious suspect. Our first guess was a too-small timing safety margin; we cranked it up five-hundred-fold and the identical pattern came back, only shifted. The cause was structural.
So what's happening? The original file already carries a clean, evenly spaced decode clock. When the media crosses MoQ, that clock is dropped: MoQ carries a single timestamp per frame, not the decode clock. On the way out, the exporter has to invent a decode clock again so decoders have one, and it does it frame by frame with a simple rule (author_dts). For roughly seven of every ten frames (the reordered B-frames) that rule cannot place them cleanly and nudges them one tick forward instead. The broadcast clock is then copied straight off this clock (pcr = dts.unwrap_or(pts)), so it inherits every one of those tiny nudges. That reconstruction was only ever meant to keep decoders happy, never to be a real uniform timebase, and it should never have become the source of the broadcast clock. Now that is measured, not assumed.
It worked on our Android app. But let's test it on real hardware.
4Feeding a real IRD/MRD
File analysis proves repetition and monotonicity; it cannot prove that a real decoder accepts the delivery. For that we built a bench:
- A development box.
- Publishing Big Buck Bunny as MPEG-TS, converted to 1080i 59.94, ATSC compliant.
- Publishing an ffmpeg test pattern, to avoid cuts in the stream.
- A MoQ relay. Dual-homed: one leg on the LAN to ensure no packet loss, but with internet access to publish to the Android client too.
- An egress client. Inside the same Gigabit Ethernet LAN, it pulls the TS back out and also acts as the groomer (tested with and without it), regulating to CBR and recomputing the PCR.
- A Sencore MRD. Same as in Part 1; it receives the stream over UDP/IP multicast, the exact protocol it expects in production, and decodes to SDI.
- An SDI broadcast switcher. Really just for fun, a Videoassist or any other monitor would have worked just as fine.
- A SDI capture device. We used one of our EDIS SDI-SRT bridges as viewer.
- A TV. For the Multiview output.
The whole bench ran on a single Gigabit Ethernet LAN to take packet loss off the table, so any defect the decoder flags is the stream's own timing, not a dropped packet.
| Case | Sencore | Sencore logs |
|---|---|---|
| Baseline (direct CBR source) | clean lock | none |
| MoQ raw (no groomer) | glitches, near-noise audio path | continuous alarms every 1-2s: TS Sync Loss, Transport Error Indicator, Video Output Auto Format flapping |
| MoQ + groomer | clean lock, stable 24 hours | none |
MoQ + --pace alone (PR #1973) | glitches, errors | PCR values untouched by pacing, same degenerate pattern as raw |
MoQ + --pace + -muxrate 11M | clean lock, pristine (1 min) | none |
MoQ + --pace + -muxrate 2M | no video lock (expected) | not a PCR fault: 2 Mbps is under the ~9 Mbps encoded rate, bandwidth-starved |
| Offline decode of the raw MoQ export | n/a | 0 bitstream errors (not Sencore, raw analysis) |
A second, independent check closes the loop on where the fault actually lives: the same publish, watched live over the internet on a native Android MoQ client with no TS and no UDP in the path, played back with zero errors. Publish and transport are sound end to end. The fault is isolated to the TS egress→MRD path, not to MoQ itself.
After adding the ffmpeg-based groomer, Sencore stayed silent for more than 24 hours straight.
Before (raw, no groomer):
moq-cli subscribe --url https://<relay> --client-tls-disable-verify \
--broadcast <name> --format ts \
| ffmpeg -i - -c copy \
-f mpegts "udp://<multicast>:<port>?pkt_size=1316"
After (with groomer):
moq-cli subscribe --url https://<relay> --client-tls-disable-verify \
--broadcast <name> --format ts \
| ffmpeg -i - -c copy -muxrate 11M -pcr_period 20 \
-f mpegts "udp://<multicast>:<port>?pkt_size=1316"
-muxrate 11M forces a constant output bitrate, so the muxer pads with null packets and derives PCR from byte position at that fixed rate instead of the incoming frame timing. -pcr_period 20 caps the interval between PCR values at 20 ms, half the 40 ms TR 101 290 limit. Together they replace the degenerate author_dts-derived PCR with one recomputed from a constant-bitrate clock.
After, simpler (requires the unmerged --pace flag from PR #1973):
moq-cli subscribe --url https://<relay> --client-tls-disable-verify \
--broadcast <name> --format ts --pace \
| ffmpeg -i - -c copy -muxrate 11M \
-f mpegts "udp://<multicast>:<port>?pkt_size=1316"
Both work today. --pace hands the delivery pacing to moq-cli itself instead of ffmpeg's buffering, and -pcr_period drops out because it was already ffmpeg's own default. Either way, -muxrate is the flag doing the real work.
The ffmpeg mpegts muxer discards the degenerate PCR on the way through and generates a new one, uniform, from a constant-bitrate byte position. That is the whole fix: regulate to CBR, recompute the clock. Do it. It works today, and we are not going to leave it as the final answer, but you do not have to wait for us, before you put MoQ in front of a real decoder.
5Not really MoQ's fault
MPEG-2 Systems rests on one invariant: a decoder's PLL slaves its 27 MHz clock to the PCR values it receives, and every PCR has to be faithful to the axis on which the decoder actually recovers time. On IP delivery, that axis is the wall-clock instant each packet leaves the sender, not the packet's position in a file.
MoQ transports timed objects, not a byte-clock, and it does that on purpose: the relay stays media-agnostic, and that is a real strength, not an oversight. Reconstructing a conformant transport stream, PCR included, is application work that belongs at the boundary where MoQ meets a legacy decoder, not inside the transport MoQ's maintainers own. Luke Curley's own framing of MoQ2 is exactly this: a lean transport that lets the application layer decide what a stream means. We are not asking the core to grow broadcast-specific machinery. We are building the egress-side piece that any protocol meeting a real decoder eventually needs.
[update] Not MoQ's fault, but fixing it anyway on moq-dev (PR #1973).
6Wait, is this a hack?
No, not a hack, a downstream process regenerating the PCR sounds like a workaround until you look at what the rest of the industry ships for the identical problem. Zixi Broadcaster's UDP and RIST outputs offer a documented remultiplex/pad mode that converts a variable-bitrate transport stream to CBR and adjusts the PCR, with a configurable PCR interval3. Zixi documents CBR remux and PCR adjustment at output, the same operational role as a groomer3. TSDuck, the same tool we used to measure the problem, ships pcradjust and regulate as first-class plugins for PCR smoothing and clock-paced delivery4, and its own documentation is explicit that useful PCR smoothing on a live stream requires exactly the real-time discipline a groomer provides.
[update] But wait, can MoQ-dev do better? Stay tuned for PR #1973…
7What is next
- In-tree PCR fix (follow #1973). PR #1973 is a work in progress; Luke just pointed us to it, and we took the chance to test it against the real Sencore bench. Pacing alone still glitched: it only gets halfway there, the PCR values themselves are still untouched. No PR exists yet for the other half, forcing CBR at the muxer, and it is being discussed on Discord; failing that, the groomer workaround already ships today regardless.
- Extensive testing. We are now close to 24 hours of continuous running, zero alarms via the ffmpeg clock-fixing way. Next is polishing #1973 to close the other half. More extensive testing is still needed, but the trend so far is promising.
- Formal TR 101 290 capture. The numbers in this report come from TSDuck's own plugins, not from a P1/P2 conformance report.
- Broader than one vendor. Every hardware result here is against one Sencore unit. We will validate against as many MRDs as we can reasonably get our hands on; we are not claiming universal IRD compatibility from a sample of one.
- End to end SDI to SDI, yeah baby! We are building a full-cycle MoQ-driven device, leveraging the fact that we are already AJA-certified developers. And yes, MoQ-driven bridges for IRDs are needed too.
8Acknowledgments
Thomas (t0ms) shared his own tests with us and pointed us toward finding a solution, which we found via ffmpeg. He also opened Issue #1839. Luke is already half way to fixing it with --pace at PR #1973.
✓In one line
A broadcast IRD/MRD has already consumed video from MoQ, but not before we detected a clock defect and groomed it away downstream. The fix that works today ships in this report with the commands. The fix that belongs upstream is in progress. But you don't really have to wait for it.
Glossary
- PCR
- Program Clock Reference, timestamps embedded in an MPEG-TS that let a decoder's PLL recover a stable 27 MHz clock.
- MRD / IRD
- Multi-channel/Integrated Receiver-Decoder, broadcast hardware that decodes a compressed transport stream back to baseband (SDI). This report uses MRD for the Sencore unit tested; the two terms describe the same class of device.
- TR 101 290
- The ETSI measurement guideline broadcast engineers use to classify transport stream conformance faults, including PCR accuracy and repetition.
- Groomer
- A downstream process that regulates delivery to constant bitrate and recomputes the PCR before handing a stream to a hardware decoder.
- DTS / PTS
- Decode and Presentation Timestamps: DTS orders frames for the decoder, PTS orders them for display; they diverge whenever B-frames are present.
- CBR / VBR
- Constant and variable bitrate: CBR transport streams pad with null packets to keep byte rate uniform, which stable PCR delivery depends on.
References
- TSDuck, MPEG transport stream toolkit, User Guide. tsduck.io/docs/tsduck.html.
- Luke Curley, "MoQ: Not Another Tech Demo", a talk on Media over QUIC, YouTube. youtube.com/watch?v=BluV8WBGnHY.
- Zixi Broadcaster, Adding UDP Outputs and Adding RIST Outputs (remultiplex/pad: "adds null packets and adjusts the stream clock (PCR) in order to make the stream CBR"), and Fix (configurable PCR interval, DVB standard 40 ms / ATSC standard 100 ms; padding bitrate; timeline normalization). docs.zixi.com/.../adding-udp-outputs, docs.zixi.com/.../adding-rist-outputs, docs.zixi.com/.../fix.
- TSDuck,
pcradjustandregulateplugins, source and issue tracker. github.com/tsduck/tsduck.
Engineering report for technical reference. Status current as of June 30, 2026 and may change. Last updated July 1st. This report was updated once we knew about #1973 and further investigated it. Hardware validation in this report covers one MRD (Sencore); PCR tolerance may vary across vendors and has not been generalized. Codec, container and standards names are used for identification.