Skip to content

fix(moq-mux): author DTS for B-frame MPEG-TS export#1843

Open
kixelated wants to merge 5 commits into
mainfrom
claude/nice-chandrasekhar-3a58a6
Open

fix(moq-mux): author DTS for B-frame MPEG-TS export#1843
kixelated wants to merge 5 commits into
mainfrom
claude/nice-chandrasekhar-3a58a6

Conversation

@kixelated

@kixelated kixelated commented Jun 21, 2026

Copy link
Copy Markdown
Collaborator

Problem

The MPEG-TS exporter (rs/moq-mux/src/container/ts/export.rs) set dts: None on every PES, so B-frame streams carried no decode timeline. Players saw PTS-only video with out-of-order decode times and needed ffplay -fflags +igndts to play. Reproduced on a real CNN 1080i contribution feed through moq pub | relay | moq sub. Closes #1836.

Fix

MoQ groups and frames are already delivered in decode order, so the only job is keeping DTS strictly increasing — no need to reconstruct decode order. author_dts runs a decode clock a hair (DTS_RESERVE, 16 ticks ≈ 0.18 ms) behind the PTS and never lets it go backwards: a reordered frame whose PTS dips below the clock is nudged one tick past the last DTS. That monotonic DTS is exactly what removes the +igndts requirement.

  • Continuous-tick domain. The clock runs in unwrapped 90 kHz ticks (source timestamps never wrap), and the 33-bit wire wrap is applied only at emission in write_pes, so PTS and DTS wrap together and the clock stays monotonic across the ~26.5 h boundary (caught in review).
  • PCR follows the decode clock (it was non-monotonic for B-frames before, since it tracked PTS in decode order).
  • None when DTS == PTS, so the PES only grows when a DTS is actually present.

Scope of the guarantee

This authors a monotonic DTS, which is what the issue is about. The 16-tick reserve is deliberately near-zero (no added decode latency), so it does not guarantee DTS ≤ PTS: a B-frame's DTS can sit above its own PTS by up to the reorder span. Lenient decoders (decode in stream order, present by PTS) are unaffected and no longer need +igndts; strict DTS ≤ PTS conformance is the faithful follow-up — the source TS already carries the exact DTS, which the importer currently discards, and carrying that PTS − DTS offset end-to-end (over the wire, to survive pub → relay → sub) is the dev-targeted change noted in #1836.

Notes for reviewers

  • No public API or wire/format change (author_dts and the per-track last_dts are private), so this targets main.
  • The PES optional header grows by 5 bytes when it carries a DTS; write_pes sizes it accordingly.
  • Re-import is unaffected: the TS importer reads pts only and ignores dts (verified); byte-exact round-trip tests pass.

Test plan

  • author_dts unit tests: strictly increasing across reordered (B-frame) decode order; the no-reorder case trails PTS by exactly the reserve; a 33-bit wrap-boundary test.
  • Integration test imports the real kyrion_dirtystart.ts (H.264 1080i, ~86 B-frames), exports, and asserts the video PES carry a strictly increasing decode timeline.
  • Full moq-mux suite (247 tests), cargo clippy, and cargo fmt clean (via nix).

(Written by Claude)

The TS exporter set `dts: None` on every PES, so B-frame streams carried
no decode timeline. Players saw PTS-only video with out-of-order decode
times and needed `ffplay -fflags +igndts` to play (reproduced on a real
CNN 1080i contribution feed through `moq pub | relay | moq sub`).

MoQ groups and frames are delivered in decode order, so the only job is
keeping DTS monotonic. A forward-only decode clock runs DTS_RESERVE ticks
(0.25 s) behind the PTS and never goes backwards: a reordered frame whose
PTS dips below the clock is nudged one tick past the last DTS. The reserve
gives the reorder room to land under each frame's own PTS (DTS <= PTS) for
broadcast B-frame spans; it is decode lead time, not presentation latency
(frames still show at their PTS), so it is effectively free. No buffering,
no estimation, no startup latency. PCR follows the decode clock too.

The PES optional header grows by 5 bytes when it carries a DTS. No public
API or wire/format change.

Closes #1836

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 439bc2c1-cb83-40bf-b9a5-ba4f8c3f714a

📥 Commits

Reviewing files that changed from the base of the PR and between 0103929 and b0bc241.

📒 Files selected for processing (2)
  • rs/moq-mux/src/container/ts/export.rs
  • rs/moq-mux/src/container/ts/export_test.rs

Walkthrough

The MPEG-TS muxer's export path gains DTS (Decode Timestamp) authoring for reordered video frames. Each Track stores per-frame authored decode-timestamp state (last_dts) for video only. PesUnit gains an optional dts field to carry an authored decode timestamp when it differs from PTS. write_frame converts each frame timestamp to continuous 90 kHz ticks and calls author_dts() to generate strictly monotonic DTS using a fixed reserve offset (DTS_RESERVE) and the stored last_dts. write_pes wraps optional unit.dts into the 33-bit MPEG-TS timestamp field, includes DTS in the PES header when present, computes variable optional header length accordingly, and selects PCR from the decode clock (DTS if available, otherwise PTS). First-packet header sizing is updated to account for the expanded variable PES optional header region. Unit tests validate monotonic DTS generation under frame reorder, correctness across the 33-bit wrap boundary, and exact reserve trailing for non-reordered streams. An integration test using a B-frame reordered fixture verifies the exporter produces strictly increasing effective decode timelines.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: authoring DTS for B-frame MPEG-TS export, which directly addresses the core issue of missing decode timestamps.
Description check ✅ Passed The description thoroughly explains the problem (B-frame streams with no decode timeline requiring workarounds), the fix (monotonic DTS clock with fixed reserve), implementation details, and scope, all directly related to the changeset.
Linked Issues check ✅ Passed The PR implements the exporter-side fix from issue #1836 by authoring valid, monotonic DTS values. The implementation maintains a decode clock (#1836 objective), applies 33-bit wrap at emission, handles B-frame reordering, and eliminates the need for +igndts workaround.
Out of Scope Changes check ✅ Passed All changes focus on authoring DTS within the exporter: per-track last_dts tracking, monotonic DTS generation via author_dts, PES optional header expansion, and corresponding test coverage. No public API or wire format changes introduced; secondary follow-up (faithful composition-offset carriage) is correctly deferred to dev branch.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch claude/nice-chandrasekhar-3a58a6

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@rs/moq-mux/src/container/ts/export.rs`:
- Around line 939-949: In the `author_dts` function, add wrap-around detection
before the monotonic check at line 942. When `pts` is significantly smaller than
the stored previous DTS (indicating a 33-bit wrap-around has occurred), reset or
adjust the monotonic state to prevent DTS from exceeding the newly wrapped PTS
value. Use a signed distance comparison similar to the `PtsUnwrap` logic
mentioned in the comment (comparing against a HALF threshold) to detect when the
PTS wraps from a high value to a low value, then handle this case by resetting
the `last` state or adjusting the monotonic bump calculation to maintain the DTS
≤ PTS invariant across the wrap boundary.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a89082ce-6974-46b3-9fbd-0b197bdf09a1

📥 Commits

Reviewing files that changed from the base of the PR and between 4c3ac0b and bf4e26b.

📒 Files selected for processing (2)
  • rs/moq-mux/src/container/ts/export.rs
  • rs/moq-mux/src/container/ts/export_test.rs

Comment thread rs/moq-mux/src/container/ts/export.rs
kixelated and others added 4 commits June 20, 2026 18:54
Run the decode clock in the continuous (unwrapped) 90 kHz tick domain that
the source timestamps already use, and wrap into the 33-bit wire field only
at emission. Previously the clock ran in wrapped 33-bit space, so a 24/7
feed broke at the ~26.5 h wrap: PTS dropped to ~0, the monotonic bump kept
DTS high, and DTS > PTS. The clock now never wraps mid-stream, so DTS <= PTS
holds across the boundary; PTS and DTS still wrap together on the wire.

Also bump DTS_RESERVE from 0.25 s to 1 s so the reserve covers any broadcast
B-frame structure. It is decode lead time, not presentation latency.

Adds a wrap-boundary unit test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the fixed 1s DTS_RESERVE with a frame-based reserve so the advertised
decode buffer scales with the frame rate instead of a flat wall-clock slab.
DtsClock learns the frame duration from the smallest PTS gap (adjacent frames
are one frame apart) and runs the decode clock RESERVE_FRAMES frame-durations
behind the PTS.

The reserve must exceed the stream's reorder depth, which open-GOP / B-pyramid
structures push past the raw consecutive-B count: the kyrion 1080i contribution
feed reorders 5 frames deep with only 3 consecutive B-frames. RESERVE_FRAMES = 8
covers that plus typical pyramids; a deeper stream still gets a monotonic DTS
(the +igndts fix), just without a guaranteed DTS <= PTS.

The reserve is decode lead time, not presentation latency (frames still show at
their PTS).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ead)

Revert the frame-scaled reserve to a fixed 16-tick (90 kHz) DTS_RESERVE: just a
strict-monotonic nudge so the decode timeline stays strictly increasing (the
+igndts fix) with negligible decode lead. Drops the frame-duration tracking.

This trades the DTS <= PTS guarantee: at 16 ticks the reserve cannot cover the
reorder span, so a B-frame's DTS sits above its own PTS. Monotonic DTS is what
players need to schedule decoding; exact DTS <= PTS is the faithful wire-DTS
follow-up. Tests assert monotonicity accordingly.

Co-Authored-By: Claude Opus 4.8 <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.

fix(moq-mux): author DTS for B-frame MPEG-TS export (decode timeline not emitted)

1 participant