diff --git a/doc/lib/rs/moq-mux.md b/doc/lib/rs/moq-mux.md index 85dba3b09c..6ac125f6bf 100644 --- a/doc/lib/rs/moq-mux.md +++ b/doc/lib/rs/moq-mux.md @@ -104,6 +104,26 @@ real idle gap. fMP4 passthrough rewrites each fragment's `tfdt` to match. Use it for a live feed with its own zero; publish verbatim only when the catalog's clock (`Config::with_clock`) already names the source's zero. +An application running its own demuxer gets the same mapping from +`clock::Anchor`: one per source, plus one `clock::Lane` per track. A single +`SourceMap` per track would let tracks drift apart by their first-PTS +difference, and one `SourceMap` shared across tracks reads their interleaving +as a reset. Each lane detects its own restarts, and the anchor moves once for +all of them. Call `Lane::restart()` before a frame when the demuxer sees a +discontinuity out of band. + +```rust +use moq_mux::clock; + +let mut anchor = clock::Anchor::new(catalog.clock()); +let mut video = clock::Lane::default(); +let mut klv = clock::Lane::default(); + +// Both tracks keep their source spacing on the broadcast clock. +let video_ts = anchor.translate(&mut video, video_pts)?; +let klv_ts = anchor.translate(&mut klv, klv_pts)?; +``` + ```bash cargo add moq-mux ``` diff --git a/quest/m1/README.md b/quest/m1/README.md index ecc7b1ac58..fee0f21d4e 100644 --- a/quest/m1/README.md +++ b/quest/m1/README.md @@ -78,7 +78,6 @@ QUIC studies there on that rule. - [Data consumer timestamps](/quest/m1/data-consumer-timestamps.md) - json and binary consumers return each value's timestamp, in Rust and every binding - [Nested data configs](/quest/m1/data-config-nesting.md) - docs nest `BinaryConfig`/`JsonConfig` in an application section instead of flattening it - [Omit empty catalog sections](/quest/m1/catalog-omit-empty.md) - a Rust catalog with no video or audio leaves those keys out, as JS does -- [Public clock anchor](/quest/m1/clock-anchor-public.md) - an application demuxer shares one source offset across its tracks through `Anchor` and `Lane` - [Delete SourceMap](/quest/m1/source-map-removal.md) - on dev, `Anchor` with its `Lane`s is the one way onto the broadcast clock - [Draft-20 FETCH](/quest/m1/ietf-fetch-location.md) - a draft-20+ FETCH within one group is served from its LOCATION_FILTER, as older drafts are - [Fetch without SUBSCRIBE](/quest/m1/ietf-fetch-only.md) - a relay fetches from an IETF upstream without subscribing, finished tracks included, with End of Track always reported diff --git a/quest/m1/clock-anchor-public.md b/quest/m1/clock-anchor-public.md deleted file mode 100644 index d491aa6098..0000000000 --- a/quest/m1/clock-anchor-public.md +++ /dev/null @@ -1,29 +0,0 @@ -# [S] moq-mux exports the per-source clock anchor - -## Goal - -An application running its own demuxer puts several tracks of one source on -the broadcast `Clock` with one shared offset, as the built-in TS, fMP4, and -FLV importers do, without going through a built-in importer. - -## Plan - -Requested by an external consumer (OneTooMany, Discord): their MPEG-TS -ingest publishes video, KLV, and sometimes audio from one program. One -`SourceMap` per track lets tracks drift apart by their first-PTS difference, -and one shared `SourceMap` reads interleave (beyond `MAX_REORDER`) as a reset. - -Decided: make `clock::Anchor` and `clock::Lane` public with the API the -importers already use: `Anchor::new(clock)`, `translate(&mut lane, pts)`, -`translate_at`, `extend`, and `Lane::restart`, with `Lane: Default`. Additive, -so it lands on `main` for a 0.10 patch. - -Open for the PR: the export path. Proposal: `pub mod clock` with -`moq_mux::clock::{Anchor, Lane}`, keeping the root `Clock` re-export. Ask the -maintainer with alternatives. - -Document both in `doc/lib/rs/moq-mux.md`, with a two-track example. - -Their other `ts::Import` gaps (MPEG-2 video, KLV reassembly) stay unplanned -until requested directly. The H.265 per-packet error is -[TS damaged units](/quest/m1/ts-damaged-units.md). diff --git a/quest/m1/source-map-removal.md b/quest/m1/source-map-removal.md index f4eddc12de..a3a60899fe 100644 --- a/quest/m1/source-map-removal.md +++ b/quest/m1/source-map-removal.md @@ -11,7 +11,3 @@ with its `Lane`s is the one way to map a source onto the broadcast clock. it. A single-track source uses `Anchor` with one `Lane`. Migrate the `SourceMap` tests in `rs/moq-mux/src/clock.rs` onto `Anchor`, and update `doc/lib/rs/moq-mux.md`. - -## Required - -- [Public clock anchor](/quest/m1/clock-anchor-public.md) - `Anchor` and `Lane` must be public first diff --git a/rs/moq-mux/src/clock.rs b/rs/moq-mux/src/clock.rs index ee2e0694b8..ac813e0edf 100644 --- a/rs/moq-mux/src/clock.rs +++ b/rs/moq-mux/src/clock.rs @@ -15,6 +15,9 @@ //! A source with its own zero (a file, a restarted encoder) is translated onto the mapping with //! [`SourceMap`]: the first frame anchors onto the live edge, and a reset re-anchors forward, //! preserving the real idle gap measured on this monotonic clock. +//! +//! A source that muxes several tracks (an MPEG-TS program, an fMP4 file) shares one [`Anchor`] +//! across them, with one [`Lane`] per track, so every track keeps the same offset. use std::time::{Duration, Instant, SystemTime}; @@ -240,7 +243,21 @@ impl SourceMap { /// video step back further than [`SourceMap::MAX_REORDER`], which would read as a reset. So /// each track keeps its own [`Lane`] that detects its own backwards steps, and a restart any /// lane detects moves the anchor once; the other lanes adopt that mapping when they restart too. -pub(crate) struct Anchor { +/// +/// ``` +/// use moq_mux::{Clock, clock}; +/// +/// let mut anchor = clock::Anchor::new(Clock::new()); +/// let (mut video, mut klv) = (clock::Lane::default(), clock::Lane::default()); +/// +/// // Both tracks of the program keep the 800ms between their source timestamps. +/// let v = anchor.translate(&mut video, moq_net::Timestamp::from_micros(10_800_000)?)?; +/// let k = anchor.translate(&mut klv, moq_net::Timestamp::from_micros(10_000_000)?)?; +/// assert_eq!(v.as_micros() - k.as_micros(), 800_000); +/// # Ok::<(), Box>(()) +/// ``` +#[derive(Debug)] +pub struct Anchor { clock: Clock, /// Broadcast micros minus source micros for the current generation; `None` until the first /// frame anchors it. @@ -256,8 +273,8 @@ pub(crate) struct Anchor { } /// One track's position on its source's [`Anchor`]. -#[derive(Default)] -pub(crate) struct Lane { +#[derive(Debug, Default)] +pub struct Lane { /// The offset this lane translates with, in micros; `None` until it adopts one. offset: Option, generation: u64, @@ -270,13 +287,14 @@ pub(crate) struct Lane { impl Lane { /// The next frame starts a new source timeline, however its PTS compares to the last. - pub(crate) fn restart(&mut self) { + pub fn restart(&mut self) { self.restart = true; } } impl Anchor { - pub(crate) fn new(clock: Clock) -> Self { + /// A source mapping onto `clock`, unanchored until the first frame on any lane. + pub fn new(clock: Clock) -> Self { Self { clock, offset: None, @@ -288,12 +306,15 @@ impl Anchor { } /// Translate one of `lane`'s timestamps, sampling the arrival time. - pub(crate) fn translate(&mut self, lane: &mut Lane, pts: moq_net::Timestamp) -> crate::Result { + pub fn translate(&mut self, lane: &mut Lane, pts: moq_net::Timestamp) -> crate::Result { self.translate_at(lane, pts, self.clock.now().value()) } /// Translate one of `lane`'s timestamps, arriving at monotonic `now` micros. - pub(crate) fn translate_at( + /// + /// The deterministic core behind [`translate`](Self::translate): `now` is what + /// [`Clock::now`] would read, pinned for synthetic sources and tests. + pub fn translate_at( &mut self, lane: &mut Lane, pts: moq_net::Timestamp, @@ -366,7 +387,9 @@ impl Anchor { } /// Record that the broadcast has published up to `end`, e.g. a fragment's last sample end. - pub(crate) fn extend(&mut self, end: moq_net::Timestamp) { + /// + /// A restart then lands after `end` even when no frame's start reached it. + pub fn extend(&mut self, end: moq_net::Timestamp) { self.extend_micros(end.as_micros()); } diff --git a/rs/moq-mux/src/lib.rs b/rs/moq-mux/src/lib.rs index b128bf22de..9722b5ac7a 100644 --- a/rs/moq-mux/src/lib.rs +++ b/rs/moq-mux/src/lib.rs @@ -12,6 +12,8 @@ //! raw bitstream to a broadcast. //! - [`catalog`] publishes and subscribes to the broadcast catalog, //! the JSON manifest listing every track and how to decode it. +//! - [`clock`](mod@clock) holds the broadcast [`Clock`] and the translators +//! that move a source's own timestamps onto it. //! - [`import`](mod@import) is the front door for callers who only have //! a format string. It picks the right concrete importer for you. //! - [`select`] picks which renditions of a broadcast to keep, on either @@ -28,7 +30,7 @@ pub mod binary; pub mod catalog; -mod clock; +pub mod clock; pub mod codec; pub mod container; mod error;