Skip to content

test(ts): grade the full T-STD buffer model - #4643

Merged
kixelated merged 6 commits into
quest/m1/tstd/READMEfrom
quest/m1/tstd/check
Oct 1, 2026
Merged

kixelated merged 6 commits into
quest/m1/tstd/READMEfrom
quest/m1/tstd/check

Conversation

@kixelated

@kixelated kixelated commented Oct 1, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

tstd in test/ts/compliance.py modelled only the transport buffer, with fixed leak rates, so it could not say whether moq export ts output decodes in an ISO 13818-1 receiver. The questline needs a full T-STD verdict to grade the fixed-delay export against.

Approach

  • Tool search first. TSDuck 3.44 has no T-STD analyzer (tsp --list has nothing buffer-model related), nothing in nixpkgs does, and no maintained open-source verifier turned up. So the model is hand-rolled, with every parameter transcribed from a spec: H.222.0 10/2014 (free from the ITU), its Amd.6 for ADTS, H.264 Table A-1/A-2, H.265 Table A.8, ATSC A/52 and A/53 Part 5. TSDuck does the parsing: tstables decodes the PMT, and tsp -P pes --avc-access-unit decodes the AVC/HEVC SPS.
  • Model. Video (AVC 2.14.3.1, HEVC 2.17.2): TB -Rx-> MB -Rbx leak-> EB, access units removed at DTS. The SPS's NAL HRD sets Rx (1.2x BitRate for AVC, CpbBrNalFactor/CpbBrVclFactor x BitRate for HEVC) and EB (CpbSize), MB takes the level's CPB less the declared one, and Rbx stays the level's. The level limits stand in only without a NAL HRD; a VCL-only HRD does not describe the byte stream, so it counts as absent. A *_timing_and_HRD_descriptor with hrd_management_valid is refused, since its MB-to-EB schedule is not modelled. Audio (ADTS, MPEG-1/2, AC-3, E-AC-3, Opus): TB -Rx-> B, frames split from the ES and removed at their PTS. Graded on 2.4.2.6: TB/MB/EB/B overflow, TB occupied over 1 s, EB/B underflow, STD delay (1 s, 10 s for AVC/HEVC). Each signalled PCR discontinuity gets fresh buffers.
  • Opus is graded with borrowed parameters. The Opus-in-TS draft gives Rx but no buffer size, so Opus uses ADTS's buffers for the same channel count. The stream label says so.
  • Packets and access units. Every packet on an ES PID enters TB, including adaptation-only packets and legal duplicates (2.4.2.3, 2.4.3.3); only first-copy PES bytes reach MB/B. Video AUs are read from the NAL stream (H.264 7.4.1.2.3, H.265 7.4.2.4.4), and a PES carrying several AUs is refused. Video decode times must strictly increase. These three came from the automated review, and each has a synthetic control.
  • Refused, not skipped. MPEG-1/2 video, DVB E-AC-3, HEVC beyond Main/Main 10, and ADTS with a PCE fail the check by name. SCTE-35 and private data are listed as not graded.
  • Controls (just test ts-tstd, wired into interop.yml).
    • A real Kyrion broadcast capture from the moq-mux test data passes against its own declared buffer (1.935 Mb/s CBR NAL HRD, 755 kbit CPB, EB peaking at 100%). So does the same capture with its PCRs restamped at its own rate.
    • Restamping with tsp -P pcradjust (PES and timestamps untouched) to deliver at 0.7x gives EB/B underflow, 4x gives TB/B overflow, and 15x gives TB/B overflow.
    • MB can no longer overflow on this fixture: it is ~3.6 MB once the declared CPB is subtracted from the level's, more than the 4 s capture carries.
  • Gate. tstd stays a shape check, so just test ts only reports it. delay.md now says to promote it to hard in that PR.
  • Cleanup (approved by the maintainer). Deletes checks that TSDuck or pcr-timing.py already cover, for a net -286 lines.
    • compliance.py drops pcr-jitter, pcr-repetition, inter-arrival, bitrate-consistency, burstiness and null-ratio. pcr-timing.py's pcr-value-interval and pcr-schedule grade the same PCRs while honouring signalled discontinuities, tstd's TB grades the bursts a receiver cannot absorb, and tsanalyze reports the null count.
    • pcr-timing.py runs sync/continuity only under --live. On a file, compliance.py grades both through tsanalyze, which also flags a payload-less packet advancing the counter: verified by mutating one PCR-only packet's CC, which tsanalyze and tsp -P continuity both report.
    • pcr-timing.py drops pcr-single-pid. It failed a legal stream with one PCR per program, so every check now grades the busiest PCR PID instead.

Does current moq export ts pass?

No. 30 s round-trip of the generated clip, steady-state time base (29 s):

PID Result
256 AVC High@3.1 EB underflow on 70/722 access units, worst 16 ms late; MB/EB peaks 2%/1%
257 AAC mono TB overflow x258 (bursts of 4+ audio packets, 176% peak); B overflow x3383 (audio arrives ~180-240 ms ahead, 121% peak)

The media fits its buffers easily, so every failure is timing:

  • Video has no decoder lead. Each frame's last byte arrives a median 15 ms before its DTS: the PCR runs at media time minus the DTS reserve, which puts it about level with the anchor's DTS.
  • B-frame DTS sits on the anchor's. B-frames are authored 1-3 ticks after the anchor's DTS (see the PCR verdict below), so they must be in EB within microseconds of the anchor.
  • Audio runs far ahead. It arrives ~200 ms early, in bursts.

Fixed-delay release targets the first and third. This is evidence that the line is feasible, not that it isn't.

Delay branch (#4645) under this model

origin/quest/m1/tstd/delay (dbb4e1f04), built and run through its own run.sh --strict with this branch's compliance.py. The generated 20 s clip passes strict tstd at both rates, with no violations:

Mux rate Video AVC (PID 256), 500 AUs Audio AAC (PID 257) pcr-schedule
10 Mb/s TB 0%, MB 1.6%, EB 1.0% TB 29.4%, B 21.6% 743/743
2 Mb/s TB 0%, MB 1.6%, EB 1.0% TB 27.0%, B 41.2% 744/744

PCR verdict, with TSDuck as the reference

PCR values are correct. TSDuck agrees with pcr-timing.py on every point. The only divergence came from compliance.py's pcr-jitter.

  • Steady state (after packet 1200): tsp -P pcrverify --bitrate 10000000 -j 160 reports 1,121 PCR OK and 0 over 160 us, so every PCR is within one packet of the 10 Mb/s byte grid.
  • The 99 ms is two signalled time-base changes. pcrextract shows the PCR stepping back at packets 124 (-94.84 ms) and 350 (-54.98 ms). Both carry discontinuity_indicator, and tsanalyze counts them as "Expected discontinuities: 2" on PID 256. pcrverify does not honour the indicator. Its two jitters over 30 ms are exactly those deltas plus the byte term: -2,560,800 - 115,761 = -2,676,561, and -1,484,400 - 12,862 = -1,497,262.
  • Both sides of each rewind are consistent 25 ms grids, offset by exactly 119.84 ms and 79.98 ms. That is the video DTS reserve growing: Export::pcr_at in rs/moq-mux/src/container/ts/export.rs computes slot_ticks - max reserve, sets the indicator when the reserve rises, and writes the flag into the packet. This is legal under 2.4.3.5. But PTS/DTS are not rebased, so the decoder lead jumps from ~11 ms to ~146 ms across the first one, a ~120 ms decode stall at start-up.
  • The 24.7 ms in pcr-jitter is an artifact of the start-up rate guess. pcrextract puts all 78 off-grid intervals in the first ~1000 packets, at 2-5 packets per 25 ms PCR interval. Before the importer publishes mpegts.muxRate (its meter needs a 2 s window), the exporter has no rate to pad to. pcr-jitter divides total bytes by PCR span to get a nominal rate (9.47 Mb/s), and that start-up drags it low. It also silently skips the negative deltas that pcrverify counts.
  • A DTS issue found along the way (not PCR). author_dts in export.rs gives each reordered B-frame the previous DTS + 1 tick, so an anchor and its three B-frames decode 11 us apart. For example, packets 499/534/551/567 have PTS 11.161/11.081/11.041/11.121 and DTS 10.961 + 0/1/2/3 ticks. In steady state, 519 of 722 video access units sit within 3 ticks of the previous one. This is monotonic, but not a frame-spaced decode schedule. Routed separately, since rs/moq-mux is out of scope here.

Impact

  • No public API or wire changes. Test harness only.
  • compliance.py drops --tb-size-bytes, --video-leak-bps, --audio-leak-bps (and the unused data_leak_bps): parameters now come from the stream and the spec.
  • compliance.py drops --pcr-repetition-ms, --pcr-jitter-us, --null-ratio-max, --bitrate-cov-max and --burstiness-max along with their checks. pcr-timing.py --repetition-ms remains the PCR spacing knob. Nothing in the repo passed the removed flags.
  • run.sh now requires tstables (already in the TSDuck the dev shell ships).
  • New recipe just test ts-tstd and an interop.yml step.

Alternatives

  • TSDuck pcrverify instead of the hand-rolled pcr-jitter. Not swapped, because the discontinuities are legitimate and pcrverify grades across them. pcr-jitter is deleted instead: pcr-schedule covers it.
  • The ffmpeg clip as the positive control. Rejected. ffmpeg's muxer sends audio 0.7 s ahead by default, which overflows the 3,584 B ADTS buffer, and even at -muxdelay 0.1 one 4-packet audio burst overflows TB.
  • MPEG-2 video parameters, so kyrion_mpeg2av_ac3.ts could be a second positive. Skipped because the exporter never emits MPEG-2 video. It is refused by name instead.

Follow-ups

  • Export (routed separately): a frame-spaced B-frame DTS in author_dts, and choosing the DTS reserve before the first PCR so start-up needs no time-base rewinds.
  • An ADTS broadcast capture as a second positive control. The current positive exercises AVC + MPEG audio, while the export emits ADTS.

🤖 Generated with Claude Code

(Written by Claude Opus 5.5)

kixelated and others added 2 commits September 30, 2026 16:46
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replace the transport-buffer-only tstd check with ISO 13818-1 2.4.2: TB, MB
and EB (leak method) for AVC/HEVC, TB and B for ADTS, MPEG audio, AC-3 and
E-AC-3, each access unit removed at its DTS. Parameters come from the
stream's SPS level and the H.222.0/ATSC tables; streams without published
parameters (Opus, MPEG-2 video) are refused by name. No maintained tool
implements T-STD, so TSDuck only decodes the PMT.

tstd-controls.py (just test ts-tstd) proves it discriminates: a Kyrion
broadcast capture passes, and the same capture with its PCRs restamped
to deliver at 0.7x, 4x and 15x fails with underflow, B overflow and
TB/MB overflow. tstd stays report-only until fixed-delay release.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated

Copy link
Copy Markdown
Collaborator Author

Outcome of the quest run (paper trail):

  • Tool vs hand-rolled. No maintained T-STD implementation exists: none in TSDuck 3.44 or nixpkgs. So the model is hand-rolled from cited spec tables, and TSDuck decodes the PMT (tstables). No new tools in the dev shell.
  • Validation. A Kyrion broadcast capture passes. The same capture restamped to 0.7x/4x/15x delivery fails with underflow, B overflow, and TB/MB overflow respectively (just test ts-tstd).
  • Export verdict. The current moq export ts output fails. Video: EB underflow on ~10% of AUs, up to 16 ms late in steady state. Audio: TB and B overflow, arriving ~200 ms ahead in bursts. All of it is delivery timing, not media size.
  • Open decisions (kept as a draft for the maintainer):
    1. Opus streams are refused, since the spec sets no buffer size, so they fail the strict gate. Recommend keeping it refused.
    2. Keep the hand-rolled pcr-jitter over TSDuck pcrverify, which grades across signalled discontinuities. Recommend keeping it.
    3. The positive control borrows rs/moq-mux test data across the tree. Recommend accepting this over duplicating a 1.4 MB fixture.

(Written by Claude Opus 5.5)

The Opus-in-TS draft gives Rx but no buffer size, so borrow ADTS's for the
same channel count instead of refusing the stream, and say so in the label.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated

Copy link
Copy Markdown
Collaborator Author

Follow-up on the maintainer's review:

  • Opus: now graded against ADTS's buffers for its channel count, labelled as borrowed. The Opus-in-TS draft gives Rx but no buffer size.
  • PCR verdict, with TSDuck as the reference: the PCR values are correct.
    • In steady state, pcrverify --bitrate 10000000 puts every PCR within 160 us of the byte grid.
    • The 99 ms is TSDuck's pcrverify measuring across two signalled time-base rewinds at packets 124 and 350, which tsanalyze counts as expected discontinuities. They come from Export::pcr_at backing the clock off when the DTS reserve grows (export.rs 1921-1943).
    • The hand-rolled 24.7 ms comes from pcr-jitter estimating its nominal rate from a capture whose first ~1.2 s is unpadded, because the mux rate is not yet known.
    • pcr-timing.py's value, schedule and continuity results match TSDuck on this capture.
  • Separate finding, routed: author_dts (export.rs 2451-2460) gives B-frames the anchor's DTS + 1 tick, so 72 % of steady-state video access units decode 11 us after the previous one.
  • Not swapped to pcrverify, since the discontinuities are legitimate. The cleanup PR scope is in the description: delete pcr-jitter, pcr-repetition, inter-arrival, bitrate-consistency/burstiness, null-ratio, file-mode sync/continuity in pcr-timing.py, and pcr-single-pid.

(Written by Claude Opus 5.5)

kixelated and others added 2 commits September 30, 2026 17:29
…over

compliance.py loses pcr-jitter, pcr-repetition, inter-arrival,
bitrate-consistency, burstiness and null-ratio, with their flags:
pcr-timing.py's value and schedule checks grade the same PCRs while
honouring signalled discontinuities, and tstd's TB grades the bursts a
receiver cannot absorb. pcr-timing.py runs sync and continuity only under
--live, since tsanalyze grades a file, and grades the busiest PCR PID
instead of failing a stream with one per program.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
H.222.0 2.14.3.1 and 2.17.2 take Rx and EB from the SPS's NAL HRD, with
the level limits only standing in when it is absent. Read the SPS through
TSDuck's pes plugin (AVC and HEVC alike) instead of a hand-rolled parser,
and refuse a stream whose timing_and_HRD descriptor puts MB-to-EB transfer
on the HRD schedule. The Kyrion reference still passes against its own
1.935 Mb/s, 755 kbit buffer; its 4x and 15x restamps now overflow the
video TB, and none can overflow its 3.6 MB MB.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated
kixelated marked this pull request as ready for review October 1, 2026 00:35

@kixelated kixelated left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review by review (OpenAI)

Reviewed #4643 at 1e4ca79.

  • [P2] Derive video access-unit boundaries from the ES, not timestamped PES boundaries: compliance.py:814–817. AVC permits multiple AUs in a PES, and its timestamp refers to the first commencing AU (H.222.0 §§2.14.2, 2.4.3.7). Here all bytes until the next timestamped PES must arrive by that first DTS. A synthetic two-AU PES with DTS 50/90 ms and the second AU arriving at 60 ms falsely reports EB underflow; supplying the real AU boundaries passes. Parse AU boundaries and derive unstamped decode times, or explicitly refuse layouts the model cannot grade.

  • [P2] Keep adaptation-only packets in TB input: compliance.py:543–545. This skips them before recording a packet, so PCR-only/stuffing packets on the elementary PID consume no TB capacity. TB must receive them even though they deliver no PES bytes (H.222.0 §2.4.2.3). Four such packets at 0.1 ms intervals with Rx=2 Mb/s produce zero model input; retaining them produces the expected TB overflow. Record their arrival with zero downstream bytes.

  • [P2] Suppress duplicate payload after TB: compliance.py:565–569. No continuity-counter/duplicate handling precedes these appends. Repeating one legal TS packet turns 170 ES bytes into 340 and adds a second timestamped PES, corrupting AU boundaries and occupancy. The same buffering rule keeps duplicates in TB but excludes them from MB/B. Detect legal repeats and retain only their TB cost.

Direction: a spec-based diagnostic with positive/negative controls is useful, and keeping it report-only during exporter work is appropriate. Add packetization controls alongside PCR-rate mutations before relying on its compliance verdict. No production API or wire change.

Verification: inspected all ten changed files and the cited standard; Python compilation and direct synthetic reproductions passed. TSDuck/Nix are unavailable here, so the broadcast controls and export round-trip were not independently run. Interop, Check, and WASM CI were running when checked.

(Written by OpenAI)

…y packet

From the automated review on #4643:

- Video access units open where H.264 7.4.1.2.3 / H.265 7.4.2.4.4 say,
  not at PES boundaries. A PES carrying several is refused, since only
  the first gets a timestamp, and video decode times must strictly rise.
- Adaptation-only packets (PCR, stuffing) enter TB with nothing for MB/B
  (H.222.0 2.4.2.3); they were skipped before TB.
- A legal duplicate packet (2.4.3.3) costs TB but is not delivered on.

Each has a synthetic control in tstd-controls.py that fails without it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated

Copy link
Copy Markdown
Collaborator Author

Re the automated review (OpenAI): I checked all three P2s against H.222.0 and agree with each. They're fixed in the latest push, and each has a synthetic control in tstd-controls.py that fails without its fix.

  1. Access units from the ES. AU boundaries now come from the NAL stream (H.264 7.4.1.2.3, H.265 7.4.2.4.4): a new AU opens at the first delimiter, parameter set or SEI after the previous picture's slices. H.222.0 2.14.1/2.17.1 require a delimiter in each AU, so a stream with none is refused. A PES carrying several AUs is refused rather than graded, because only the first gets a timestamp and deriving the rest isn't modelled. Video decode times must now strictly increase. Following the NAL rule rather than delimiters alone matters for the export, which puts the SPS/PPS ahead of the delimiter in keyframe PES.
  2. Adaptation-only packets. They now enter TB and deliver nothing downstream (2.4.2.3: "all bytes that enter TBn"; only PES bytes go on). Control: four adaptation-only packets after an AU give a TB overflow; before the fix, it passed.
  3. Duplicates. A packet with the same CC and payload as its predecessor (2.4.3.3) now costs TB but stays out of MB/B. Control: duplicating an AU's first packet passes; before the fix, it gave "decode time does not advance".

The real streams grade the same as before: the Kyrion controls are unchanged, and the export capture still shows 75 video underflows.

(Written by Claude Opus 5.5)

@kixelated kixelated left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review by review (OpenAI)

Reviewed 4895b9d, the one-commit delta from 1e4ca79.

The three original reproductions are addressed: multi-AU PES is refused, adaptation-only packets enter TB, and ordinary duplicates no longer enter MB/B. The four new synthetic layouts give their intended parser/model outcomes.

  • [P2] Reset duplicate detection at discontinuities: test/ts/compliance.py:550–555. Matching only the last payload and CC silently discards a genuine new PES when a signalled clock/counter reset reuses them. H.222.0 §§2.4.3.3–2.4.3.4 permit that reset; legal duplicates instead require consecutive same-PID packets with identical bytes except PCR. The cache also survives intervening adaptation-only packets. Reproduced with a real AAC silence frame in each of two time bases: the second packet carries discontinuity_indicator, the same CC/PES payload, and arrives 11 ms late. The model reports PASS with only one AU graded; changing only the new packet's CC grades both and exposes B underflow. Scope the duplicate state to the legal packet sequence and honour discontinuity resets; add this alongside the ordinary-duplicate control.

Direction: keep the narrow, report-only diagnostic and explicit refusal of layouts whose decode times cannot be derived. The new packetization controls improve confidence; include discontinuity coverage before relying on a hard gate. No production API or wire change.

Verification: Python compilation passed; exercised the new synthetic layouts directly through read_pes/video_units/simulate and reproduced the AAC regression through check_tstd with fixture PMT metadata supplied locally. Rechecked the cited standard. TSDuck/Nix are unavailable, so the complete controls and exporter round-trip remain unrun here. Check, Interop, and WASM CI were still running when checked.

(Written by OpenAI)

@kixelated

Copy link
Copy Markdown
Collaborator Author

Reviewed 4895b9d6332865c1aaaf61893a6a323a2a42c083 (full review vs base; no prior Grok review).

Verdict: MERGE

Solid full T-STD model with Kyrion/restamp/synthetic controls; stays shape/report-only until fixed-delay. The three OpenAI follow-ups (NAL AU boundaries, adaptation-only in TB, duplicates TB-only) look correct against H.222.0 and each has a control.

Non-blocking

  1. Interop red on this head; T-STD controls skipped. Failure was unrelated Media output and lifecycle (~31s vs --timeout 30). Re-run Interop (or just test ts-tstd) so the new packetization controls are green on this SHA. Earlier Interop on this PR at 1e4ca797 did pass T-STD controls.
  2. video_units only requires any AUD in the stream (compliance.py ~837–852), not one per AU. A stream that drops AUDs after the first can collapse pictures into one access unit and mis-grade instead of refusing. Require a delimiter per AU (or also open on first-slice flags).
  3. Declared CPB larger than the level term can make mb ≤ 0 and break peak-fill math. Refuse non-positive buffer sizes.

CI

Check/Test/WASM pass. Interop fail (media flake); T-STD controls skipped on this head.

This is an automated review, not the maintainer's decision
(Written by Grok)

@kixelated
kixelated merged commit 70e7383 into quest/m1/tstd/README Oct 1, 2026
5 of 6 checks passed
@kixelated
kixelated deleted the quest/m1/tstd/check branch October 1, 2026 03:49
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.

1 participant