Skip to content

fix(ts): carry a full-rate CBR feed at its own multiplex rate - #5414

Closed
t0ms wants to merge 4 commits into
moq-dev:quest/m1/ts-atsc-ac3from
t0ms:quest/m1/tstd/mux-overhead
Closed

t0ms wants to merge 4 commits into
moq-dev:quest/m1/ts-atsc-ac3from
t0ms:quest/m1/tstd/mux-overhead

Conversation

@t0ms

@t0ms t0ms commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #5306, which it targets until that lands.

Problem

A padded export ts could not carry a CBR contribution feed at the feed's own multiplex rate when
the feed fills that rate with no null packets (#5362). The export spent packets the source does not:

  • Video filling its level's leak was admitted below Rbx. The schedule charged it whole 188-byte
    packets at the lesser of Rx and Rbx, plus the clock packet, and MB held only a transport buffer's
    worth ahead of the leak.
  • Every 25 ms slot spent a packet on a clock of its own, where the source carries its PCR in the
    video's adaptation field.
  • Every audio frame took a PES of its own, where the source packs several.

At the source's rate the export fell behind until its lead ran out, and it stopped on missed a
decode deadline
on the video PID.

Approach

One commit per step, each retiring its quest:

  • fix(ts): admit leak-bound video at its full Rbx (quest/m1/tstd/video-leak.md). TB drains
    whole packets at Rx, the clock packet among them, and MB passes elementary bytes on into EB at
    Rbx. An unpadded slot carries the lesser of the two, with fractions carried from slot to slot and
    no one-packet slack.
  • fix(ts): let video's MB run BSmux ahead of its leak. MB may hold BSmux ahead of the leak
    (2.14.3.1), so audio taking a position between video packets no longer idles it.
  • fix(ts): count audio PES headers in their decoder buffer. Only video's MB discards the PES
    header before EB. For audio and system PIDs, B holds it until the access unit after it is removed
    (ISO 13818-1 2.4.2.3). This reverses a5640abee's accounting for PIDs without a leak and keeps
    video's. A 2,592-byte frame no longer fits a 2,592-byte B, so that test now pushes the largest
    frame that fits with its 14-byte header.
  • feat(ts): carry the PCR in the clock PID and pack audio frames per PES
    (quest/m1/tstd/mux-overhead.md).
    • Each slot's PCR rides the adaptation field of the PCR PID's first packet in the slot, reflowing
      that PES's payload. A slot without one keeps its clock packet.
    • With a --delay, consecutive frames of every audio codec share a PES while they run on without
      a gap and fit B with their header. The codecs are AAC, MP2, AC-3, E-AC-3, and AC-3 in a private
      stream. Each frame leaves B at its own decode time.
    • A PES starts wherever a frame's place on the media timeline is a multiple of the frames B
      holds, so exporters that joined at different frames cut the same PESs.

Tested with:

  • just test ts --leak: 1080p with no HRD at level 4.0's 24 Mb/s leak round-trips the whole 30 s
    clip. Before, the export stopped 12.7 s in.
  • just test ts --full, new, run in the interop workflow. The --leak video beside MP2 and DVB
    AC-3, muxed with its PCR in the video PID and several audio frames per PES, with the nulls
    dropped, exports at its own 24.75 Mb/s. It covers the whole 30 s and the strict T-STD check
    passes. The new overhead.py counts 8,026 packets spent on clocks and audio framing, against
    the source's 8,165.
  • just test ts, --hrd, and --headroom pass.
  • A 280 s replay of a 25 Mb/s CBR contribution feed with no nulls. On main at b581c27ed, the
    export stops 31 s in on the video PID. With the first two commits it runs through at 26 Mb/s,
    but stops 185 s in at 25 Mb/s. With all four it runs through at the source's own 25 Mb/s. The
    strict T-STD check, compliance.py and PCR accuracy all pass, and every frame of every track
    is the source's.
  • just check on macOS with host tools passes, apart from the moq-tokio flake below and the steps
    whose tools are missing here: the js module (no wasm-bindgen), nix flake check, and the Swift
    step (no Xcode).

Impact

  • No public API change. Schedule is private to moq-mux.
  • Wire: padded export ts output carries each slot's PCR in the PCR PID's adaptation field
    whenever that PID has a packet in the slot.
  • Wire: with a --delay, an exported audio PES carries several frames, up to its B. With no
    delay, it carries one frame, as before.
  • An audio frame that fills B with no room for its PES header is now refused.
  • Adds just test ts --full and its CI step, and test/ts/overhead.py.

Alternatives

  • Keep audio PES headers out of B, as a5640abee does. Then PESs packed up to B overflow it by
    their header, which compliance.py's strict check reports on --full.
  • Put the PCR in a stuffed adaptation field of a packet of its own. That costs as much as the clock
    packet. Reflowing a PES the slot already carries costs at most one more packet, and only when its
    last packet has no room left.
  • Pack a fixed number of frames per PES. The number that fits depends on the codec and bitrate,
    so the PES size follows B instead.

Follow-ups

  • A unit may go out its window, which is --delay, plus a slot and its drain ahead of its decode.
    So at --delay 1s or more, the first audio PES after startup can sit in B for longer than the
    T-STD's 1 s. --full runs at 900 ms for this reason. This belongs with the send-ahead quest.
  • With per-frame removal, a PES could exceed B, provided its frames never overflow B as they
    arrive. On the 25 Mb/s feed above, the source packs about 9.3 AC-3 frames per PES, while B caps
    the export at seven. So the export spends 2.5 % more packets than the source on AC-3, and 1.0 %
    more on MP2. It still fits, since it repeats PAT and PMT less often than the source.
  • moq-tokio's a_refused_tcp_dial_keeps_retrying flakes under load. Within its 200 ms budget it
    sometimes sees timed out connecting instead of Connection refused. It is unrelated to this PR.

(Written by Claude Opus 5.5)


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

t0ms and others added 4 commits October 13, 2026 11:59
The schedule charged video whole 188-byte packets at the lesser of Rx and
Rbx, plus the clock packet, so a source filling its level's leak, as a
full-rate feed with no HRD does, fell behind and missed a decode deadline.
The transport buffer now drains whole packets at Rx, the clock packet among
them, while MB passes only elementary bytes on into EB at Rbx. Unpadded, a
slot carries the lesser of the two, fractions carried from slot to slot,
without the one-packet slack.

`just test ts --leak` round-trips 1080p video with no HRD at level 4.0's
24 Mb/s leak. Before, its export stopped 12.7 s into the 30 s clip.

Retires quest/m1/tstd/video-leak.md.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Held to a transport buffer's worth ahead of the leak, MB emptied whenever
audio due sooner took a position or two between video packets, and the leak
idled. On a feed whose video fills Rbx, the export carried about 1 % less
video than the source and stopped 105 s in. H.222.0 2.14.3.1 gives MB BSmux
for this, 4 ms of the larger of Rbx and 2 Mb/s, and BSoh for PES headers.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The T-STD discards a video PES header in MB, before EB, but an audio or
system PID's transport buffer feeds B directly: B holds the PES header
until the access unit after it is removed (ISO 13818-1 2.4.2.3).

The schedule counted only elementary bytes in every decoder buffer, so an
audio PES could fill B with its frame and then overflow it by its header.
Count the whole payload for PIDs without a leak, and keep video's EB
elementary-only. The admission check now rejects a frame that fills B
with no room for its header.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A padded export spent a whole clock packet on every 25 ms slot and a PES
on every audio frame. A source multiplex with no null packets has no room
for either, so an export at the source's own rate fell behind it until
its lead ran out and it missed a deadline (moq-dev#5362).

- Each slot's PCR rides the adaptation field of the clock PID's first
  packet in the slot when one goes out there, reflowing that PES's
  payload, and falls back to a clock packet of its own otherwise.
- Consecutive frames of every audio codec (AAC, MP2, AC-3, E-AC-3 and
  AC-3 carried in a private stream) share a PES while they run on without
  a gap and fit B whole with their header. Each frame leaves B at its
  own decode time. PESs start on a grid of the media timeline, so
  exporters that joined at different frames cut the same PESs.
- Frames and grid slots follow each other in decode order however often
  the exporter is polled, and tables muxed while frames are still packed
  go out with the next unit, in the order they were written.

`just test ts --full` exports a generated 30 s clip at its own rate with
no nulls: an x264 video at its level's leak beside MP2 and DVB AC-3,
with the PCR in the video PID and several audio frames per PES. The new
`overhead.py` fails the export if it spends more packets than the
source on clocks and audio framing; CI runs it in the interop workflow.

Retires quest/m1/tstd/mux-overhead.md.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants