Skip to content

feat(moq-mux,moq-srt): PTS-exposing TS Export API + PCR-paced SRT egress#1845

Merged
kixelated merged 2 commits into
devfrom
claude/ts-export-frame
Jun 21, 2026
Merged

feat(moq-mux,moq-srt): PTS-exposing TS Export API + PCR-paced SRT egress#1845
kixelated merged 2 commits into
devfrom
claude/ts-export-frame

Conversation

@kixelated

Copy link
Copy Markdown
Collaborator

Summary

Implements step 1 of #1839: an additive media-timing API on moq_mux::container::ts::Export, and uses it to fix the SRT egress tune-in burst that #1828 explicitly deferred.

ts::Export now yields Frame, not Bytes

  • Export::next() / poll_next() return Option<moq_mux::container::Frame>. Each frame's payload holds the TS packets, stamped with the source timestamp and keyframe flag, so a transport can pace delivery on the media clock. duration is always None (the muxer never tracked it).
  • PAT/PMT are buffered, then flushed with the first media timestamp. Previously the leading header went out as its own timestamp-less chunk and write_frame re-emitted PAT/PMT on the first keyframe (a startup double-PSI). Now step 3 only builds the PSI; write_frame prepends it to the first frame, so the header inherits that frame's real timestamp and timestamp is always valid (no Option). Removed the now-unused write_psi.

SRT egress paces on the media clock

  • serve_request stamps each SRT payload with anchor + (timestamp - base) instead of Instant::now(). That Instant is the packet's origin time feeding SRT's TSBPD, so the receiver reconstructs the original inter-frame spacing and the tune-in keyframe burst is released at the media rate instead of bursting. One Instant::now() total (the anchor), no per-packet wall-clock stamps.

Why this is the whole SRT fix

SRT already has congestion control and a receiver jitter buffer (TSBPD). The burst wasn't a missing-pacing problem; it was that we fed Instant::now() as the packet origin time, collapsing all spacing into "simultaneous." Feeding real media time uses TSBPD as designed. The standalone PCR-pacing loop #1839 describes is still needed for the future raw UDP/RTP backends (no TSBPD there), but not for SRT.

Notes

  • Export::next() returning Frame is a breaking moq-mux API change, hence targeting dev.
  • duration was not removed: it's load-bearing in fmp4 export (trun sample durations) and the consumer's group-advance, and isn't needed for Export to return Frame (it just sets None, like the legacy/flv/ts-import paths).
  • No cross-package sync: Rust-only muxer API (no JS equivalent), and the moq-cli CLI surface is unchanged (same TS bytes to stdout).

Test plan

  • cargo test -p moq-mux ts:: (181 passed) — covers the PSI-buffering change and TS round-trips
  • cargo test -p moq-srt
  • cargo clippy -p moq-mux -p moq-srt -p moq-cli --all-targets (clean)
  • E2E: SRT m=request tune-in no longer bursts the opening GOP (manual, ffmpeg/VLC loopback)

Part of #1839; builds on #1828.

(Written by Claude)

kixelated and others added 2 commits June 20, 2026 17:38
…gress on it

`ts::Export::next()` now yields a `moq_mux::container::Frame` instead of bare
`Bytes`: each frame's `payload` is the TS packets, stamped with the source
`timestamp` and `keyframe` flag so a transport can pace delivery on the media
clock. PAT/PMT are no longer emitted as a separate timestamp-less chunk; the
muxer buffers them and prepends them to the first media frame, so the leading
header inherits a real timestamp and the previous startup double-PSI is gone.

The SRT request egress (#1828) now stamps each SRT payload with
`anchor + (timestamp - base)` rather than `Instant::now()`. That Instant is the
packet origin time feeding SRT's TSBPD, so the receiver reconstructs the
inter-frame spacing and a tune-in keyframe burst is released at the media rate
instead of all at once, the pacing #1828 deferred.

Part of #1839.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The deleted write_psi left its doc comment attached to pick_next_track;
replace it with an accurate one. Document that SRT egress paces on the
presentation timestamp (the only clock the muxer exposes) while frames
transmit in decode order, so B-frame reorder leaves send_at slightly
non-monotonic, which is harmless.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@kixelated kixelated enabled auto-merge (squash) June 21, 2026 02:02
@kixelated kixelated merged commit 87fe2ff into dev Jun 21, 2026
1 check passed
@kixelated kixelated deleted the claude/ts-export-frame branch June 21, 2026 04:02
kixelated added a commit that referenced this pull request Jun 21, 2026
#1845 landed PTS-paced SRT egress + a Frame-returning ts::Subscriber::next on
dev while this branch moved the egress path out of listen.rs into the new
server.rs. Resolution: keep the Server/Request-based listen.rs (run() on top of
Server) and port #1845's media-clock pacing into server.rs::serve_subscribe,
which now consumes ts::Subscriber's Frame (timestamp + payload) and stamps each
SRT payload with the frame's media time instead of Instant::now().

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
kixelated pushed a commit that referenced this pull request Jun 21, 2026
dev advanced 3 commits while this PR was open (#1845 PTS-exposing TS Export +
PCR-paced SRT egress, #1847 moq-lite-05 wire sync, #1819 always-on hardware
encoders). Re-merged dev and reconciled two conflicts:

- nix/overlay.nix: kept this PR's cargo+sccache CI direction (dev's tip still
  carries the crane `moqChecks`; the cargo switch lives on main, #1821).
- rs/moq-mux/src/container/ts/export.rs: combined dev's #1845 Frame-returning
  `Export::next` (PTS/keyframe-stamped) with this PR's generic `catalog::Catalog`
  trait (main's #1815 mpegts rename). Updated the moq-cli drain helper to read
  `frame.payload` and refreshed two stale `scte35::Ext` comments.

Verified: moq-mux (279), moq-cli, moq-srt tests pass; workspace clippy -D
warnings and fmt clean (excluding the crates that need libva/gstreamer system
libs unavailable in this sandbox: moq-video/libmoq/moq-boy/moq-gst); JS
type-checks and biome clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BQ9o9paZnRLpYRgbyjUwFV
@kixelated kixelated mentioned this pull request Jun 21, 2026
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