EDIS Interactive Puebla, MX · --:--:--
EDIS Interactive · Engineering report

MPEG-TS from MoQ into a Broadcast Decoder Demo

The bitstream is correct, the clock needed to be slightly fixed.
Broadcast · MoQ · MPEG-TS · PCR · TR 101 290
The Sencore MRD hooked into the bench, decoding a MoQ-sourced MPEG-TS to SDI.

Part 1 ended with a promise: “taking a real MPEG-TS feed onto MoQ without losing the bytes that matter is the part nobody demos.” We are not going to be one more player who says it and never shows it. Today we drove a MoQ-sourced MPEG-TS into a real broadcast MRD, watched it decode clean to SDI, and confirmed it downstream on real gear. What almost broke the demo was not the bytes. It was the clock. And we got a fix.

In brief

MoQ's export already produces a valid bitstream. What a real broadcast decoder rejects is the clock riding inside it, and that is fixable today without touching MoQ's core.

  1. The MPEG-TS MoQ exports decodes with zero bitstream errors offline. It was never a corruption problem.
  2. The PCR MoQ writes today is monotonic but not uniform, because it is derived from author_dts, a decoder-compatibility reconstruction, not a real media clock: 23.7% of PCR intervals exceed the 40 ms TR 101 290 limit.
  3. On a real MRD (Sencore), that degeneration produces a continuous cascade of TR 101 290-class alarms. Not theoretical, not a corner case.
  4. A downstream groomer, regulate to CBR and recompute the PCR, is proven to fix it, live, against real hardware.
  5. This is not a MoQ-specific workaround: it is the same pattern Zixi ships in production3 and TSDuck ships as first-class tooling4. MoQ is not the outlier here, it just has not built its own groomer yet.
  6. [update] PR #1973 is already half way to an in-tree fix: it paces delivery, but the PCR values themselves are still untouched, and that might belong at egress or at the core.
The bottom line: MoQ's transport is sound; a real broadcast decoder needs a faithful PCR, and that might belong at egress or at the core. The fix that works today is a groomer, commands included. The fix that belongs upstream is in progress, not merged yet.

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.

The Sencore IRD/MRD decoding a MoQ-sourced feed on the EDIS bench, driven to SDI.
Fig. 1. The MoQ way up to a real IRD/MRD, a real demo.

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.

Time matters Monotonic is not the same as uniform, and that distinction is the entire subject of this report.
MetricDirty-start (2.9s, 133 PCR)Clean capture (30s, 937 PCR)Verdict
PCR going backward00passing
Continuity errors00passing
PCR intervals > 40 ms16.5%23.7% (222/936), max 133 msfailing
Micro-jumps < 0.1 ms69%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.

bench topology · the path a signal actually walked, 2026-06-30
SOURCE bunny ATSC MoQ MoQ Relay 192.168.99.6 EDIS MoQ Client groomer -muxrate -pcr_period UDP multicast SENCORE MRD 0 errors, groomed SDI switcher EDIS SDI Viewer HDMI PGM Output TV / MULTIVIEW
Fig. 2. A development box published a MoQ stream to our relay; our egress read it back from the relay, applied the groomer, and forwarded it to the Sencore in its native UDP multicast format. With the groomer in place, everything worked cleanly.
CaseSencoreSencore logs
Baseline (direct CBR source)clean locknone
MoQ raw (no groomer)glitches, near-noise audio pathcontinuous alarms every 1-2s: TS Sync Loss, Transport Error Indicator, Video Output Auto Format flapping
MoQ + groomerclean lock, stable 24 hoursnone
MoQ + --pace alone (PR #1973)glitches, errorsPCR values untouched by pacing, same degenerate pattern as raw
MoQ + --pace + -muxrate 11Mclean lock, pristine (1 min)none
MoQ + --pace + -muxrate 2Mno video lock (expected)not a PCR fault: 2 Mbps is under the ~9 Mbps encoded rate, bandwidth-starved
Offline decode of the raw MoQ exportn/a0 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.

Fig. 3a. Before (top) and after (bottom) groomer. Seen after Sencore to SDI and feeding to a broadcast switcher.
Sencore error log table showing continuous TR 101 290-class alarms on the raw MoQ feed.
Fig. 3b. Sencore's log when the feed is raw: continuous alarms, silent once the groomer is added.

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.

Not a hack Every serious egress path that reconstructs a transport stream for a hardware decoder ends up with a component that regulates delivery and re-derives the PCR. MoQ needing the same piece is not a defect in MoQ, it is the field being consistent with itself.

[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.
The first end to end SDI to SDI! We are building a full-cycle MoQ-driven device, leveraging the fact that we are already AJA-certified developers.

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

  1. TSDuck, MPEG transport stream toolkit, User Guide. tsduck.io/docs/tsduck.html.
  2. Luke Curley, "MoQ: Not Another Tech Demo", a talk on Media over QUIC, YouTube. youtube.com/watch?v=BluV8WBGnHY.
  3. 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.
  4. TSDuck, pcradjust and regulate plugins, 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.