From 968709fb7742e02834bf58e71fda7470045d2abc Mon Sep 17 00:00:00 2001 From: OneTooMany Date: Mon, 5 Oct 2026 12:29:15 +0200 Subject: [PATCH 1/7] quest: plan a final catalog clock from the first snapshot Co-Authored-By: Claude Opus 5.5 --- quest/m1/README.md | 1 + quest/m1/final-clock.md | 70 ++++++++++++++++++++++++++++++++++++++++ quest/m2/shared-clock.md | 8 +++-- 3 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 quest/m1/final-clock.md diff --git a/quest/m1/README.md b/quest/m1/README.md index 1a17f595ea..013880020c 100644 --- a/quest/m1/README.md +++ b/quest/m1/README.md @@ -72,6 +72,7 @@ QUIC studies there on that rule. - [Audio capture without ALSA link](/quest/m1/capture-alsa-link.md) - moq-audio capture and playback build on Linux without linking libasound - [Capture by default](/quest/m1/capture-default.md) - moq-video and moq-audio build `capture` by default, so pre-merge checks test it and the capture gate goes away - [Ship capture and playback](/quest/m1/cli-packaging.md) - a released moq binary can capture and play, which no distribution currently enables +- [Final catalog clock](/quest/m1/final-clock.md) - the catalog root clock is final from its first snapshot; fMP4 and MKV imports publish at their first frame, not their init segment - [Mux data timestamp](/quest/m1/mux-data-timestamp.md) - moq-mux JSON and binary producers take a broadcast-clock timestamp, with no ahead-of-now refusal - [Publishing never invents a timestamp](/quest/m1/publish-timestamp.md) - no Rust or binding publish API fills in a timestamp; an untimed payload goes out untimed - [JS publishing never invents a timestamp](/quest/m1/js-publish-timestamp.md) - the same in @moq/json, @moq/flate, and @moq/net diff --git a/quest/m1/final-clock.md b/quest/m1/final-clock.md new file mode 100644 index 0000000000..ebbf437668 --- /dev/null +++ b/quest/m1/final-clock.md @@ -0,0 +1,70 @@ +# [S] The catalog clock is final from its first snapshot + +## Goal + +Every catalog snapshot a consumer can read carries the final root `clock`, +and nothing re-anchors it after a snapshot is out. Today fMP4 and MKV imports +publish the catalog when their init segment (`moov`, `Tracks`) resolves the +reservation, on a provisional clock, then re-anchor it on the first frame +(`catalog::Producer::anchor`). Readers that copy the clock once (moq-hls +export's `EXT-X-PROGRAM-DATE-TIME` and `availabilityStartTime`, derived +broadcasts) keep the provisional one, and the hang draft says the mapping is +"fixed for the broadcast". + +Non-goals: per-importer offsets for sources sharing a catalog (that is +[shared-clock](/quest/m2/shared-clock.md)), and JS publish. + +## Plan + +The anchor is unreleased, and this breaks an in-tree path (an fMP4 or MKV +import read by moq-hls export), so it lands before the next release cut. +Requested by an external consumer (OneTooMany); scope and milestone are +proposals for the maintainer. + +Decided (2026-10-05): + +- fMP4 and MKV hold their initial reservation until their first frame + anchors, as FLV already does (`flv/import.rs`, "anchored before the + reservation below publishes"). The first snapshot then carries the anchored + clock. PTS stays verbatim for a single importer, so a passthrough fragment's + `tfdt` still agrees with its frame timestamp. +- The first catalog publish fixes the clock (sets `anchored`), whatever + triggered it. A snapshot on the wire is a mapping a reader may have copied. +- An importer whose first frame arrives after a snapshot is out keeps its + PTS verbatim (the anchor is a no-op), the same as on a `with_clock` catalog + today. No in-tree path reaches it: every importer takes a `Reserved`. + shared-clock replaces this with its per-importer offset. +- Rejected: fixing the clock at catalog creation and offsetting every + importer. Every fMP4 passthrough would carry a frame timestamp that + disagrees with its `tfdt`, which needs + [cmaf-frame-timestamp](/quest/m2/cmaf-frame-timestamp.md) first, and it + subsumes most of shared-clock. +- Rejected: omitting `clock` until anchored. Adding it later is still a change + copy-once readers miss. + +Guidance: + +- An fMP4 `moov` declares every track at once, and MKV's `Tracks` likewise, so + the first frame of any track releases the hold. Several importers on one + catalog (moq-hls import's renditions) each hold their own reservation, so the + catalog still waits for all of them. +- Tests (fail on `main` for fMP4 and MKV): for each of fMP4, MKV, FLV, and TS, + a catalog consumer's first snapshot carries the anchored clock and later + snapshots never change it. Plus an end-to-end fMP4 import into moq-hls export + whose `EXT-X-PROGRAM-DATE-TIME` matches the anchored clock. +- Docs: fix `Config::with_clock`, `Producer::clock`, and `Producer::anchor` + comments, `doc/lib/rs/moq-mux.md`, and add an `Unreleased` line in + `doc/setup/upgrade.md`: the catalog now appears at an importer's first frame, + not its init segment. If [#4821](https://github.com/moq-dev/moq/pull/4821) + has landed, align its `Clock` docs, which say a copy goes stale when an + importer re-anchors. + +Public API: no new items. A catalog fed by an fMP4 or MKV importer is +published at the first frame instead of the init segment, and its root clock +never changes after. Wire: none; the hang draft already says the mapping is +fixed. + +## Related + +- [Shared clock](/quest/m2/shared-clock.md) - per-importer offsets once the clock is fixed before an importer's first frame +- [Mux data timestamp](/quest/m1/mux-data-timestamp.md) - data producers stamp on the catalog clock this fixes diff --git a/quest/m2/shared-clock.md b/quest/m2/shared-clock.md index 80800597cb..fb7b3b6ad8 100644 --- a/quest/m2/shared-clock.md +++ b/quest/m2/shared-clock.md @@ -19,7 +19,9 @@ data tracks onto the anchored clock. The anchor is unreleased. Decided (2026-10-01): - The clock never moves once something stamps with it. The public - `catalog::Producer::clock()` fixes the mapping (sets `anchored`). Crate-internal + `catalog::Producer::clock()` fixes the mapping (sets `anchored`), as the + first catalog publish already does after + [final-clock](/quest/m1/final-clock.md). Crate-internal readers, like #4668's `Listing`, read the state without fixing it. - An importer anchors once, on its first frame, and gets back an offset: zero if it placed the mapping (PTS stays verbatim), `clock.now() - first_pts` @@ -46,7 +48,8 @@ Guidance: - The offset is signed: a stream starting at 3600 s on a clock reading 10 s shifts down. Refuse a frame that would land below zero rather than clamp it. - A `with_clock` catalog is fixed from the start, so its importers offset too. - Today they publish verbatim PTS on a clock they didn't place. + Today they publish verbatim PTS on a clock they didn't place, as does an + importer whose first frame follows the first catalog publish. - Document on `catalog::Producer::clock` that taking the clock fixes it. - Test: start a synthetic capture, then import an fMP4 starting at PTS 0. Both tracks advance from the capture's timeline with no rewind. Also cover the @@ -60,6 +63,7 @@ Wire: none. ## Required +- [Final catalog clock](/quest/m1/final-clock.md) - the first catalog publish fixes the clock, and fMP4 and MKV anchor before it - [CMAF frame timestamp](/quest/m2/cmaf-frame-timestamp.md) - decoders honour an offset frame timestamp on passthrough tracks ## Related From 1dd5013a2af0035656a240a72fd8d7fbdef121d2 Mon Sep 17 00:00:00 2001 From: OneTooMany Date: Mon, 5 Oct 2026 12:33:48 +0200 Subject: [PATCH 2/7] quest: hold the TS reservation too; correct the HLS claim Co-Authored-By: Claude Opus 5.5 --- quest/m1/final-clock.md | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/quest/m1/final-clock.md b/quest/m1/final-clock.md index ebbf437668..dc0ca013bd 100644 --- a/quest/m1/final-clock.md +++ b/quest/m1/final-clock.md @@ -5,7 +5,8 @@ Every catalog snapshot a consumer can read carries the final root `clock`, and nothing re-anchors it after a snapshot is out. Today fMP4 and MKV imports publish the catalog when their init segment (`moov`, `Tracks`) resolves the -reservation, on a provisional clock, then re-anchor it on the first frame +reservation, and an Opus-only MPEG-TS import when its PMT does, on a +provisional clock, then re-anchor it on the first frame (`catalog::Producer::anchor`). Readers that copy the clock once (moq-hls export's `EXT-X-PROGRAM-DATE-TIME` and `availabilityStartTime`, derived broadcasts) keep the provisional one, and the hang draft says the mapping is @@ -23,8 +24,8 @@ proposals for the maintainer. Decided (2026-10-05): -- fMP4 and MKV hold their initial reservation until their first frame - anchors, as FLV already does (`flv/import.rs`, "anchored before the +- fMP4, MKV, and MPEG-TS hold their initial reservation until their first + frame anchors, as FLV already does (`flv/import.rs`, "anchored before the reservation below publishes"). The first snapshot then carries the anchored clock. PTS stays verbatim for a single importer, so a passthrough fragment's `tfdt` still agrees with its frame timestamp. @@ -32,7 +33,8 @@ Decided (2026-10-05): triggered it. A snapshot on the wire is a mapping a reader may have copied. - An importer whose first frame arrives after a snapshot is out keeps its PTS verbatim (the anchor is a no-op), the same as on a `with_clock` catalog - today. No in-tree path reaches it: every importer takes a `Reserved`. + today. In-tree, only moq-hls import's later renditions reach it, and they + share the first rendition's PTS base. shared-clock replaces this with its per-importer offset. - Rejected: fixing the clock at catalog creation and offsetting every importer. Every fMP4 passthrough would carry a frame timestamp that @@ -45,13 +47,22 @@ Decided (2026-10-05): Guidance: - An fMP4 `moov` declares every track at once, and MKV's `Tracks` likewise, so - the first frame of any track releases the hold. Several importers on one - catalog (moq-hls import's renditions) each hold their own reservation, so the - catalog still waits for all of them. -- Tests (fail on `main` for fMP4 and MKV): for each of fMP4, MKV, FLV, and TS, - a catalog consumer's first snapshot carries the anchored clock and later - snapshots never change it. Plus an end-to-end fMP4 import into moq-hls export - whose `EXT-X-PROGRAM-DATE-TIME` matches the anchored clock. + the first frame of any track releases the hold. +- MPEG-TS drops its hold after the PMT (`ts/import.rs`, "Every stream in the + initial program is registered now"). Video configs resolve only on the first + frame, after the anchor, but Opus builds its config from PMT descriptors, so + an Opus-only stream publishes before its first PES anchors. Keep the hold + until the first anchor. +- moq-hls import mints each rendition's importer lazily, so the first + rendition can publish before later ones exist. The clock is still final: + renditions share one PTS base, so a later rendition's no-op anchor lands on + the same timeline. +- Tests (fail on `main` for fMP4, MKV, and Opus-only TS): for each of fMP4, + MKV, FLV, and TS, a catalog consumer's first snapshot carries the anchored + clock and later snapshots never change it. The TS case is Opus-only with a + nonzero first PTS, since a video PMT already anchors before it publishes. + Plus an end-to-end fMP4 import into moq-hls export whose + `EXT-X-PROGRAM-DATE-TIME` matches the anchored clock. - Docs: fix `Config::with_clock`, `Producer::clock`, and `Producer::anchor` comments, `doc/lib/rs/moq-mux.md`, and add an `Unreleased` line in `doc/setup/upgrade.md`: the catalog now appears at an importer's first frame, From 687ec125d6162b8f58c8bc5c492793fb6c3c6dbe Mon Sep 17 00:00:00 2001 From: OneTooMany Date: Mon, 5 Oct 2026 12:54:39 +0200 Subject: [PATCH 3/7] quest: offset a late container; move shared-clock and cmaf-frame-timestamp to m1 Co-Authored-By: Claude Opus 5.5 --- quest/m1/README.md | 2 + quest/{m2 => m1}/cmaf-frame-timestamp.md | 2 +- quest/m1/final-clock.md | 61 ++++++++++++++++-------- quest/{m2 => m1}/shared-clock.md | 25 ++++++---- quest/m1/subscribe-live-time.md | 2 +- quest/m2/README.md | 2 - 6 files changed, 61 insertions(+), 33 deletions(-) rename quest/{m2 => m1}/cmaf-frame-timestamp.md (96%) rename quest/{m2 => m1}/shared-clock.md (78%) diff --git a/quest/m1/README.md b/quest/m1/README.md index 013880020c..4b66dd5fbb 100644 --- a/quest/m1/README.md +++ b/quest/m1/README.md @@ -72,6 +72,8 @@ QUIC studies there on that rule. - [Audio capture without ALSA link](/quest/m1/capture-alsa-link.md) - moq-audio capture and playback build on Linux without linking libasound - [Capture by default](/quest/m1/capture-default.md) - moq-video and moq-audio build `capture` by default, so pre-merge checks test it and the capture gate goes away - [Ship capture and playback](/quest/m1/cli-packaging.md) - a released moq binary can capture and play, which no distribution currently enables +- [CMAF frame timestamp](/quest/m1/cmaf-frame-timestamp.md) - CMAF decoders time samples from the moq-lite frame timestamp, using `tfdt` only within the fragment +- [Shared import clock](/quest/m1/shared-clock.md) - an importer joining a clock already in use offsets its PTS instead of moving it, so captures and imports share one timeline - [Final catalog clock](/quest/m1/final-clock.md) - the catalog root clock is final from its first snapshot; fMP4 and MKV imports publish at their first frame, not their init segment - [Mux data timestamp](/quest/m1/mux-data-timestamp.md) - moq-mux JSON and binary producers take a broadcast-clock timestamp, with no ahead-of-now refusal - [Publishing never invents a timestamp](/quest/m1/publish-timestamp.md) - no Rust or binding publish API fills in a timestamp; an untimed payload goes out untimed diff --git a/quest/m2/cmaf-frame-timestamp.md b/quest/m1/cmaf-frame-timestamp.md similarity index 96% rename from quest/m2/cmaf-frame-timestamp.md rename to quest/m1/cmaf-frame-timestamp.md index fb5f3a8d78..91c5360018 100644 --- a/quest/m2/cmaf-frame-timestamp.md +++ b/quest/m1/cmaf-frame-timestamp.md @@ -38,4 +38,4 @@ means. ## Related -- [Shared clock](/quest/m2/shared-clock.md) - the first publisher to rely on it +- [Shared clock](/quest/m1/shared-clock.md) - the first publisher to rely on it diff --git a/quest/m1/final-clock.md b/quest/m1/final-clock.md index dc0ca013bd..37e324849b 100644 --- a/quest/m1/final-clock.md +++ b/quest/m1/final-clock.md @@ -13,7 +13,7 @@ broadcasts) keep the provisional one, and the hang draft says the mapping is "fixed for the broadcast". Non-goals: per-importer offsets for sources sharing a catalog (that is -[shared-clock](/quest/m2/shared-clock.md)), and JS publish. +[shared-clock](/quest/m1/shared-clock.md)), and JS publish. ## Plan @@ -31,16 +31,25 @@ Decided (2026-10-05): `tfdt` still agrees with its frame timestamp. - The first catalog publish fixes the clock (sets `anchored`), whatever triggered it. A snapshot on the wire is a mapping a reader may have copied. -- An importer whose first frame arrives after a snapshot is out keeps its - PTS verbatim (the anchor is a no-op), the same as on a `with_clock` catalog - today. In-tree, only moq-hls import's later renditions reach it, and they - share the first rendition's PTS base. - shared-clock replaces this with its per-importer offset. +- An importer whose first frame arrives after a snapshot is out offsets its + PTS by shared-clock's mechanism, which is why shared-clock is required. A + data track or catalog section publishes the moment it is registered + (`Producer::data_entry` holds its reservation only for `init`), so a + container set up after one, in any order moq-c and moq-ffi allow, joins a + clock it didn't place. moq-hls import's later renditions also reach it, and + share the first rendition's offset through their `Reserved`. +- Rejected: a late anchor as a no-op with verbatim PTS (an fMP4 an hour into + its `tfdt`, or a TS PTS up to about 26.5 h, lands that far off the clock), a + warning or a refusal until shared-clock lands (a regression on `main` for an + order the bindings allow), and holding every publish until a container + anchors (a data-only or capture-only catalog would never publish). +- Rejected: folding shared-clock into this quest. Each keeps its own reviewed + plan, and this one shrinks to the hold and the publish rule. - Rejected: fixing the clock at catalog creation and offsetting every - importer. Every fMP4 passthrough would carry a frame timestamp that - disagrees with its `tfdt`, which needs - [cmaf-frame-timestamp](/quest/m2/cmaf-frame-timestamp.md) first, and it - subsumes most of shared-clock. + importer. Even a lone importer, the common case, would carry frame + timestamps that disagree with its `tfdt`, leaning on + [cmaf-frame-timestamp](/quest/m1/cmaf-frame-timestamp.md) for every fMP4 + passthrough instead of only a joining one. - Rejected: omitting `clock` until anchored. Adding it later is still a change copy-once readers miss. @@ -55,27 +64,39 @@ Guidance: until the first anchor. - moq-hls import mints each rendition's importer lazily, so the first rendition can publish before later ones exist. The clock is still final: - renditions share one PTS base, so a later rendition's no-op anchor lands on - the same timeline. + later renditions take the first one's offset (zero) through their shared + `Reserved`, so they land on the same timeline. - Tests (fail on `main` for fMP4, MKV, and Opus-only TS): for each of fMP4, MKV, FLV, and TS, a catalog consumer's first snapshot carries the anchored clock and later snapshots never change it. The TS case is Opus-only with a - nonzero first PTS, since a video PMT already anchors before it publishes. + nonzero first PTS, since a video stream's config resolves only on its first + frame, after the anchor. Also a TS program of only verbatim PIDs, which + publish through `modify` with no reservation. Plus an end-to-end fMP4 import into moq-hls export whose - `EXT-X-PROGRAM-DATE-TIME` matches the anchored clock. + `EXT-X-PROGRAM-DATE-TIME` matches the anchored clock. And a data track + registered before a container's first frame, then a container starting at a + nonzero PTS: the clock never moves, and the container's frames land at now. + This rewrites `a_write_follows_the_anchored_clock` (`binary.rs`, `json.rs`) + and `an_anchor_is_not_jitter` (`binary.rs`), which create a data track and + then anchor. +- fMP4 detects a duplicate `moov` by its reservation being gone + (`fmp4/import.rs`, `DuplicateMoov`). Holding it to the first fragment lets a + second `moov` slip through, so key the check on the parsed `moov` instead. + Likewise check that a TS PMT version change before the first PES does not + re-reserve against the held handle. - Docs: fix `Config::with_clock`, `Producer::clock`, and `Producer::anchor` - comments, `doc/lib/rs/moq-mux.md`, and add an `Unreleased` line in + comments, `doc/lib/rs/moq-mux.md` (its promise that a data track created + before the first frame follows the anchor), and add an `Unreleased` line in `doc/setup/upgrade.md`: the catalog now appears at an importer's first frame, not its init segment. If [#4821](https://github.com/moq-dev/moq/pull/4821) has landed, align its `Clock` docs, which say a copy goes stale when an importer re-anchors. -Public API: no new items. A catalog fed by an fMP4 or MKV importer is -published at the first frame instead of the init segment, and its root clock +Public API: no new items. A catalog fed by an fMP4, MKV, or Opus-only TS +importer is published at the first frame instead of the init segment, and its root clock never changes after. Wire: none; the hang draft already says the mapping is fixed. -## Related +## Required -- [Shared clock](/quest/m2/shared-clock.md) - per-importer offsets once the clock is fixed before an importer's first frame -- [Mux data timestamp](/quest/m1/mux-data-timestamp.md) - data producers stamp on the catalog clock this fixes +- [Shared clock](/quest/m1/shared-clock.md) - an importer joining a clock already fixed offsets its PTS instead of moving it diff --git a/quest/m2/shared-clock.md b/quest/m1/shared-clock.md similarity index 78% rename from quest/m2/shared-clock.md rename to quest/m1/shared-clock.md index fb7b3b6ad8..db4264f9f1 100644 --- a/quest/m2/shared-clock.md +++ b/quest/m1/shared-clock.md @@ -19,9 +19,9 @@ data tracks onto the anchored clock. The anchor is unreleased. Decided (2026-10-01): - The clock never moves once something stamps with it. The public - `catalog::Producer::clock()` fixes the mapping (sets `anchored`), as the - first catalog publish already does after - [final-clock](/quest/m1/final-clock.md). Crate-internal + `catalog::Producer::clock()` fixes the mapping (sets `anchored`). + [final-clock](/quest/m1/final-clock.md) then makes the first catalog + publish fix it too. Crate-internal readers, like #4668's `Listing`, read the state without fixing it. - An importer anchors once, on its first frame, and gets back an offset: zero if it placed the mapping (PTS stays verbatim), `clock.now() - first_pts` @@ -40,16 +40,24 @@ Decided (2026-10-01): frame timestamp is the broadcast timeline; a payload's timestamps are only relative within the frame. Ad insertion (splicing sources with unrelated PTS bases) needs the same rule. -- m2: nothing in-tree reaches it. `moq-cli publish` runs a capture or a stdin - import, never both, and moq-ffi does not expose capture. +Decided (2026-10-05): + +- m1, ahead of [final-clock](/quest/m1/final-clock.md), which needs the + offset: once the first publish fixes the clock, a container set up after a + data track or catalog section (any order moq-c and moq-ffi allow) joins a + clock it didn't place. +- The offset lives on the `Reserved` handle: the first anchor through any clone + sets it, and every clone uses it. moq-hls import's renditions are separate + fMP4 importers on one PTS base, so it takes one `reserve()` per source and + clones it for each rendition; separate offsets would shift each rendition + by its first frame's arrival gap. No new public items. Guidance: - The offset is signed: a stream starting at 3600 s on a clock reading 10 s shifts down. Refuse a frame that would land below zero rather than clamp it. - A `with_clock` catalog is fixed from the start, so its importers offset too. - Today they publish verbatim PTS on a clock they didn't place, as does an - importer whose first frame follows the first catalog publish. + Today they publish verbatim PTS on a clock they didn't place. - Document on `catalog::Producer::clock` that taking the clock fixes it. - Test: start a synthetic capture, then import an fMP4 starting at PTS 0. Both tracks advance from the capture's timeline with no rewind. Also cover the @@ -63,8 +71,7 @@ Wire: none. ## Required -- [Final catalog clock](/quest/m1/final-clock.md) - the first catalog publish fixes the clock, and fMP4 and MKV anchor before it -- [CMAF frame timestamp](/quest/m2/cmaf-frame-timestamp.md) - decoders honour an offset frame timestamp on passthrough tracks +- [CMAF frame timestamp](/quest/m1/cmaf-frame-timestamp.md) - decoders honour an offset frame timestamp on passthrough tracks ## Related diff --git a/quest/m1/subscribe-live-time.md b/quest/m1/subscribe-live-time.md index 3068133a14..a81f28b84a 100644 --- a/quest/m1/subscribe-live-time.md +++ b/quest/m1/subscribe-live-time.md @@ -50,4 +50,4 @@ Decided in planning (2026-10-03): ## Related - [Untimed model](/quest/m1/untimed-model.md) - absent timestamps end to end; only real pairs feed the estimate -- [Shared clock](/quest/m2/shared-clock.md) - the hang catalog's `{wall, timescale}` anchor sits a layer up; this stays media-agnostic +- [Shared clock](/quest/m1/shared-clock.md) - the hang catalog's `{wall, timescale}` anchor sits a layer up; this stays media-agnostic diff --git a/quest/m2/README.md b/quest/m2/README.md index cb552fe1fd..c73d0b286e 100644 --- a/quest/m2/README.md +++ b/quest/m2/README.md @@ -112,8 +112,6 @@ waiting on an upstream release waits in [m4](/quest/m4/README.md). - [Windows capture parity](/quest/m2/capture-windows.md) - system audio and a settled app-capture policy - [Linux capture parity](/quest/m2/capture-linux.md) - Wayland window/system-audio capture with explicit display-selection and app-capture limits - [Audio capture time](/quest/m2/audio-capture-time.md) - native audio stamps a buffer's capture instant, not when the driver reads it -- [CMAF frame timestamp](/quest/m2/cmaf-frame-timestamp.md) - CMAF decoders time samples from the moq-lite frame timestamp, using `tfdt` only within the fragment -- [Shared import clock](/quest/m2/shared-clock.md) - an importer joining a clock already in use offsets its PTS instead of moving it, so captures and imports share one timeline - [Interop graceful close](/quest/m2/interop-graceful-close.md) - successful runner publications withdraw before disconnecting; external repository approval required - [Egress profile](/quest/m2/quic-egress-profile.md) - measure relay send-path syscalls, pacing bursts, and allocations before optimizing any of them - [SEI separation study](/quest/m2/sei.md) - measure whether separating SEI saves enough, or has a metadata-only consumer, to justify a split From 0272740710be29d82a401d0ae47bd54e207aa903 Mon Sep 17 00:00:00 2001 From: OneTooMany Date: Mon, 5 Oct 2026 12:58:23 +0200 Subject: [PATCH 4/7] quest: carry a source's offset on catalog::Source, not Reserved Co-Authored-By: Claude Opus 5.5 --- quest/m1/final-clock.md | 4 ++-- quest/m1/shared-clock.md | 25 +++++++++++++++++++------ 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/quest/m1/final-clock.md b/quest/m1/final-clock.md index 37e324849b..be7ad42652 100644 --- a/quest/m1/final-clock.md +++ b/quest/m1/final-clock.md @@ -37,7 +37,7 @@ Decided (2026-10-05): (`Producer::data_entry` holds its reservation only for `init`), so a container set up after one, in any order moq-c and moq-ffi allow, joins a clock it didn't place. moq-hls import's later renditions also reach it, and - share the first rendition's offset through their `Reserved`. + share the first rendition's offset through shared-clock's `catalog::Source`. - Rejected: a late anchor as a no-op with verbatim PTS (an fMP4 an hour into its `tfdt`, or a TS PTS up to about 26.5 h, lands that far off the clock), a warning or a refusal until shared-clock lands (a regression on `main` for an @@ -65,7 +65,7 @@ Guidance: - moq-hls import mints each rendition's importer lazily, so the first rendition can publish before later ones exist. The clock is still final: later renditions take the first one's offset (zero) through their shared - `Reserved`, so they land on the same timeline. + `catalog::Source`, so they land on the same timeline. - Tests (fail on `main` for fMP4, MKV, and Opus-only TS): for each of fMP4, MKV, FLV, and TS, a catalog consumer's first snapshot carries the anchored clock and later snapshots never change it. The TS case is Opus-only with a diff --git a/quest/m1/shared-clock.md b/quest/m1/shared-clock.md index db4264f9f1..ccd861fcd5 100644 --- a/quest/m1/shared-clock.md +++ b/quest/m1/shared-clock.md @@ -46,11 +46,20 @@ Decided (2026-10-05): offset: once the first publish fixes the clock, a container set up after a data track or catalog section (any order moq-c and moq-ffi allow) joins a clock it didn't place. -- The offset lives on the `Reserved` handle: the first anchor through any clone - sets it, and every clone uses it. moq-hls import's renditions are separate - fMP4 importers on one PTS base, so it takes one `reserve()` per source and - clones it for each rendition; separate offsets would shift each rendition - by its first frame's arrival gap. No new public items. +- One source's offset lives on a public `catalog::Source` handle, separate + from the publication gate: `catalog.source()` returns a clonable handle for + one PTS base, and `source.reserve()` mints a `Reserved` gated as today that + shares the source's offset. The first anchor through any of them sets it. + `catalog.reserve()` keeps a fresh offset per call. Holding a `Source` never + withholds the catalog. moq-hls import holds one for the whole import, so its + renditions (separate fMP4 importers on one PTS base) and every replacement + importer on an `EXT-X-MAP` change share one offset; separate offsets would + shift each by its first frame's arrival gap. The name is a proposal for the + maintainer. +- Rejected: sharing the offset through `Reserved` clones. A live `Reserved` + withholds the initial catalog, so a handle kept for later importers would + hold it for the whole import. Also rejected: importers handing the offset + to each other (`with_offset` on all four). Guidance: @@ -59,13 +68,17 @@ Guidance: - A `with_clock` catalog is fixed from the start, so its importers offset too. Today they publish verbatim PTS on a clock they didn't place. - Document on `catalog::Producer::clock` that taking the clock fixes it. +- Test: moq-hls import receives its initial catalog while its `Source` is + alive, then replaces a rendition's importer, and the replacement keeps the + same offset. - Test: start a synthetic capture, then import an fMP4 starting at PTS 0. Both tracks advance from the capture's timeline with no rewind. Also cover the reverse order: an importer first keeps its PTS verbatim. A passthrough fragment whose `tfdt` disagrees with its frame timestamp decodes at the frame timestamp, in Rust and JS. -Public API: no new items. `catalog::Producer::clock()` now fixes the mapping, +Public API: `catalog::Source`, `catalog::Producer::source`, and +`Source::reserve` are new. `catalog::Producer::clock()` now fixes the mapping, and importers no longer publish verbatim PTS when the clock was already taken. Wire: none. From 80eb7bd7796c63513b6a5aba6f073033bb8f108d Mon Sep 17 00:00:00 2001 From: OneTooMany Date: Mon, 5 Oct 2026 13:08:19 +0200 Subject: [PATCH 5/7] quest: split the import hold from the strict clock rule The reservation hold fixes a lone importer with no blockers. The rule that the first publish fixes the clock moves into shared-clock, which carries the offset it needs. Co-Authored-By: Claude Opus 5.5 --- quest/m1/README.md | 2 +- quest/m1/final-clock.md | 102 --------------------------------- quest/m1/import-first-frame.md | 69 ++++++++++++++++++++++ quest/m1/shared-clock.md | 46 +++++++++++---- 4 files changed, 106 insertions(+), 113 deletions(-) delete mode 100644 quest/m1/final-clock.md create mode 100644 quest/m1/import-first-frame.md diff --git a/quest/m1/README.md b/quest/m1/README.md index 4b66dd5fbb..9ca93b3578 100644 --- a/quest/m1/README.md +++ b/quest/m1/README.md @@ -72,9 +72,9 @@ QUIC studies there on that rule. - [Audio capture without ALSA link](/quest/m1/capture-alsa-link.md) - moq-audio capture and playback build on Linux without linking libasound - [Capture by default](/quest/m1/capture-default.md) - moq-video and moq-audio build `capture` by default, so pre-merge checks test it and the capture gate goes away - [Ship capture and playback](/quest/m1/cli-packaging.md) - a released moq binary can capture and play, which no distribution currently enables +- [Import at the first frame](/quest/m1/import-first-frame.md) - fMP4, MKV, and MPEG-TS imports publish the catalog at their first frame, so a lone importer's root clock never moves - [CMAF frame timestamp](/quest/m1/cmaf-frame-timestamp.md) - CMAF decoders time samples from the moq-lite frame timestamp, using `tfdt` only within the fragment - [Shared import clock](/quest/m1/shared-clock.md) - an importer joining a clock already in use offsets its PTS instead of moving it, so captures and imports share one timeline -- [Final catalog clock](/quest/m1/final-clock.md) - the catalog root clock is final from its first snapshot; fMP4 and MKV imports publish at their first frame, not their init segment - [Mux data timestamp](/quest/m1/mux-data-timestamp.md) - moq-mux JSON and binary producers take a broadcast-clock timestamp, with no ahead-of-now refusal - [Publishing never invents a timestamp](/quest/m1/publish-timestamp.md) - no Rust or binding publish API fills in a timestamp; an untimed payload goes out untimed - [JS publishing never invents a timestamp](/quest/m1/js-publish-timestamp.md) - the same in @moq/json, @moq/flate, and @moq/net diff --git a/quest/m1/final-clock.md b/quest/m1/final-clock.md deleted file mode 100644 index be7ad42652..0000000000 --- a/quest/m1/final-clock.md +++ /dev/null @@ -1,102 +0,0 @@ -# [S] The catalog clock is final from its first snapshot - -## Goal - -Every catalog snapshot a consumer can read carries the final root `clock`, -and nothing re-anchors it after a snapshot is out. Today fMP4 and MKV imports -publish the catalog when their init segment (`moov`, `Tracks`) resolves the -reservation, and an Opus-only MPEG-TS import when its PMT does, on a -provisional clock, then re-anchor it on the first frame -(`catalog::Producer::anchor`). Readers that copy the clock once (moq-hls -export's `EXT-X-PROGRAM-DATE-TIME` and `availabilityStartTime`, derived -broadcasts) keep the provisional one, and the hang draft says the mapping is -"fixed for the broadcast". - -Non-goals: per-importer offsets for sources sharing a catalog (that is -[shared-clock](/quest/m1/shared-clock.md)), and JS publish. - -## Plan - -The anchor is unreleased, and this breaks an in-tree path (an fMP4 or MKV -import read by moq-hls export), so it lands before the next release cut. -Requested by an external consumer (OneTooMany); scope and milestone are -proposals for the maintainer. - -Decided (2026-10-05): - -- fMP4, MKV, and MPEG-TS hold their initial reservation until their first - frame anchors, as FLV already does (`flv/import.rs`, "anchored before the - reservation below publishes"). The first snapshot then carries the anchored - clock. PTS stays verbatim for a single importer, so a passthrough fragment's - `tfdt` still agrees with its frame timestamp. -- The first catalog publish fixes the clock (sets `anchored`), whatever - triggered it. A snapshot on the wire is a mapping a reader may have copied. -- An importer whose first frame arrives after a snapshot is out offsets its - PTS by shared-clock's mechanism, which is why shared-clock is required. A - data track or catalog section publishes the moment it is registered - (`Producer::data_entry` holds its reservation only for `init`), so a - container set up after one, in any order moq-c and moq-ffi allow, joins a - clock it didn't place. moq-hls import's later renditions also reach it, and - share the first rendition's offset through shared-clock's `catalog::Source`. -- Rejected: a late anchor as a no-op with verbatim PTS (an fMP4 an hour into - its `tfdt`, or a TS PTS up to about 26.5 h, lands that far off the clock), a - warning or a refusal until shared-clock lands (a regression on `main` for an - order the bindings allow), and holding every publish until a container - anchors (a data-only or capture-only catalog would never publish). -- Rejected: folding shared-clock into this quest. Each keeps its own reviewed - plan, and this one shrinks to the hold and the publish rule. -- Rejected: fixing the clock at catalog creation and offsetting every - importer. Even a lone importer, the common case, would carry frame - timestamps that disagree with its `tfdt`, leaning on - [cmaf-frame-timestamp](/quest/m1/cmaf-frame-timestamp.md) for every fMP4 - passthrough instead of only a joining one. -- Rejected: omitting `clock` until anchored. Adding it later is still a change - copy-once readers miss. - -Guidance: - -- An fMP4 `moov` declares every track at once, and MKV's `Tracks` likewise, so - the first frame of any track releases the hold. -- MPEG-TS drops its hold after the PMT (`ts/import.rs`, "Every stream in the - initial program is registered now"). Video configs resolve only on the first - frame, after the anchor, but Opus builds its config from PMT descriptors, so - an Opus-only stream publishes before its first PES anchors. Keep the hold - until the first anchor. -- moq-hls import mints each rendition's importer lazily, so the first - rendition can publish before later ones exist. The clock is still final: - later renditions take the first one's offset (zero) through their shared - `catalog::Source`, so they land on the same timeline. -- Tests (fail on `main` for fMP4, MKV, and Opus-only TS): for each of fMP4, - MKV, FLV, and TS, a catalog consumer's first snapshot carries the anchored - clock and later snapshots never change it. The TS case is Opus-only with a - nonzero first PTS, since a video stream's config resolves only on its first - frame, after the anchor. Also a TS program of only verbatim PIDs, which - publish through `modify` with no reservation. - Plus an end-to-end fMP4 import into moq-hls export whose - `EXT-X-PROGRAM-DATE-TIME` matches the anchored clock. And a data track - registered before a container's first frame, then a container starting at a - nonzero PTS: the clock never moves, and the container's frames land at now. - This rewrites `a_write_follows_the_anchored_clock` (`binary.rs`, `json.rs`) - and `an_anchor_is_not_jitter` (`binary.rs`), which create a data track and - then anchor. -- fMP4 detects a duplicate `moov` by its reservation being gone - (`fmp4/import.rs`, `DuplicateMoov`). Holding it to the first fragment lets a - second `moov` slip through, so key the check on the parsed `moov` instead. - Likewise check that a TS PMT version change before the first PES does not - re-reserve against the held handle. -- Docs: fix `Config::with_clock`, `Producer::clock`, and `Producer::anchor` - comments, `doc/lib/rs/moq-mux.md` (its promise that a data track created - before the first frame follows the anchor), and add an `Unreleased` line in - `doc/setup/upgrade.md`: the catalog now appears at an importer's first frame, - not its init segment. If [#4821](https://github.com/moq-dev/moq/pull/4821) - has landed, align its `Clock` docs, which say a copy goes stale when an - importer re-anchors. - -Public API: no new items. A catalog fed by an fMP4, MKV, or Opus-only TS -importer is published at the first frame instead of the init segment, and its root clock -never changes after. Wire: none; the hang draft already says the mapping is -fixed. - -## Required - -- [Shared clock](/quest/m1/shared-clock.md) - an importer joining a clock already fixed offsets its PTS instead of moving it diff --git a/quest/m1/import-first-frame.md b/quest/m1/import-first-frame.md new file mode 100644 index 0000000000..ad7ceb0fbc --- /dev/null +++ b/quest/m1/import-first-frame.md @@ -0,0 +1,69 @@ +# [S] Importers publish the catalog at their first frame + +## Goal + +A catalog fed by one container importer is first published with its final +root `clock`. Today fMP4 and MKV imports publish the catalog when their init +segment (`moov`, `Tracks`) resolves the reservation, and an Opus-only MPEG-TS +import when its PMT does, on a provisional clock, then re-anchor it on the +first frame (`catalog::Producer::anchor`). Readers that copy the clock once +(moq-hls export's `EXT-X-PROGRAM-DATE-TIME` and `availabilityStartTime`, +derived broadcasts) keep the provisional one, and the hang draft says the +mapping is "fixed for the broadcast". + +Non-goal: a clock that never moves after any publish. A container set up +after a data track or catalog section has published still re-anchors once; +[shared-clock](/quest/m1/shared-clock.md) makes the first publish fix the +clock and offsets that container instead. + +## Plan + +The anchor is unreleased, and this breaks an in-tree path (an fMP4 or MKV +import read by moq-hls export), so it lands before the next release cut. +Requested by an external consumer (OneTooMany); scope and milestone are +proposals for the maintainer. + +Decided (2026-10-05): + +- fMP4, MKV, and MPEG-TS hold their initial reservation until their first + frame anchors, as FLV already does (`flv/import.rs`, "anchored before the + reservation below publishes"). The first snapshot then carries the anchored + clock. PTS stays verbatim for a single importer, so a passthrough fragment's + `tfdt` still agrees with its frame timestamp. +- Split from the strict "first publish fixes the clock" rule, which needs + shared-clock's offset for a container joining a fixed clock. The hold alone + needs no offset and regresses nothing: an earlier publish still lets the + anchor move the clock, as on `main`. +- Rejected: omitting `clock` until anchored. Adding it later is still a change + copy-once readers miss. + +Guidance: + +- An fMP4 `moov` declares every track at once, and MKV's `Tracks` likewise, so + the first frame of any track releases the hold. +- MPEG-TS drops its hold after the PMT (`ts/import.rs`, "Every stream in the + initial program is registered now"). Video configs resolve only on the first + frame, after the anchor, but Opus builds its config from PMT descriptors, so + an Opus-only stream publishes before its first PES anchors. Keep the hold + until the first anchor. +- fMP4 detects a duplicate `moov` by its reservation being gone + (`fmp4/import.rs`, `DuplicateMoov`). Holding it to the first fragment lets a + second `moov` slip through, so key the check on the parsed `moov` instead. + Likewise check that a TS PMT version change before the first PES does not + re-reserve against the held handle. +- Tests (fail on `main` for fMP4, MKV, and Opus-only TS): for each of fMP4, + MKV, FLV, and TS, a catalog consumer's first snapshot carries the anchored + clock and later snapshots never change it. The TS case is Opus-only with a + nonzero first PTS, since a video stream's config resolves only on its first + frame, after the anchor. Also a TS program of only verbatim PIDs, which + publish through `modify` with no reservation. Plus an end-to-end fMP4 import + into moq-hls export whose `EXT-X-PROGRAM-DATE-TIME` matches the anchored + clock. +- Docs: fix the `Config::with_clock` and `Producer::anchor` comments and + `doc/lib/rs/moq-mux.md`, and add an `Unreleased` line in + `doc/setup/upgrade.md`: the catalog now appears at an importer's first + frame, not its init segment. + +Public API: no new items. A catalog fed by an fMP4, MKV, or Opus-only TS +importer is published at the first frame instead of the init segment. Wire: +none; the hang draft already says the mapping is fixed. diff --git a/quest/m1/shared-clock.md b/quest/m1/shared-clock.md index ccd861fcd5..ce637b7b6b 100644 --- a/quest/m1/shared-clock.md +++ b/quest/m1/shared-clock.md @@ -1,4 +1,4 @@ -# [M] One timeline for every source on a catalog +# [L] One timeline for every source on a catalog ## Goal @@ -11,6 +11,12 @@ PTS base on a clock it didn't place. Either way the sources drift apart. Followi backwards, and `container::Producer::write` refuses a group below the last one (`TimestampRewind`), even across a discontinuity. +The catalog's root `clock` is also final from the first snapshot a consumer +sees. [import-first-frame](/quest/m1/import-first-frame.md) covers a lone +importer; a container set up after a data track or catalog section has +published (any order moq-c and moq-ffi allow) still re-anchors it after +copy-once readers (moq-hls export, derived broadcasts) took the old one. + ## Plan Follow-up of [#4668](https://github.com/moq-dev/moq/pull/4668), which moved @@ -19,9 +25,7 @@ data tracks onto the anchored clock. The anchor is unreleased. Decided (2026-10-01): - The clock never moves once something stamps with it. The public - `catalog::Producer::clock()` fixes the mapping (sets `anchored`). - [final-clock](/quest/m1/final-clock.md) then makes the first catalog - publish fix it too. Crate-internal + `catalog::Producer::clock()` fixes the mapping (sets `anchored`). Crate-internal readers, like #4668's `Listing`, read the state without fixing it. - An importer anchors once, on its first frame, and gets back an offset: zero if it placed the mapping (PTS stays verbatim), `clock.now() - first_pts` @@ -40,12 +44,24 @@ Decided (2026-10-01): frame timestamp is the broadcast timeline; a payload's timestamps are only relative within the frame. Ad insertion (splicing sources with unrelated PTS bases) needs the same rule. + Decided (2026-10-05): -- m1, ahead of [final-clock](/quest/m1/final-clock.md), which needs the - offset: once the first publish fixes the clock, a container set up after a - data track or catalog section (any order moq-c and moq-ffi allow) joins a - clock it didn't place. +- The first catalog publish also fixes the clock, whatever triggered it: a + snapshot on the wire is a mapping a reader may have copied. A data track or + section publishes the moment it is registered (`Producer::data_entry` holds + its reservation only for `init`), so a container set up after one gets an + offset. This lands with the offset, not before it, so the rule never exists + without it. +- m1: the anchor is unreleased, and landing this before the cut means no + release ships a clock that moves after a publish. +- Rejected for a container joining a published clock: a no-op with verbatim + PTS (an fMP4 an hour into its `tfdt`, or a TS PTS up to about 26.5 h, lands + that far off), a warning or refusal (a regression for an order the bindings + allow), and holding every publish until a container anchors (a data-only or + capture-only catalog would never publish). Fixing the clock at catalog + creation is rejected too: even a lone importer would carry frame timestamps + that disagree with its `tfdt`. - One source's offset lives on a public `catalog::Source` handle, separate from the publication gate: `catalog.source()` returns a clonable handle for one PTS base, and `source.reserve()` mints a `Reserved` gated as today that @@ -67,7 +83,15 @@ Guidance: shifts down. Refuse a frame that would land below zero rather than clamp it. - A `with_clock` catalog is fixed from the start, so its importers offset too. Today they publish verbatim PTS on a clock they didn't place. -- Document on `catalog::Producer::clock` that taking the clock fixes it. +- Document on `catalog::Producer::clock` that taking the clock fixes it, and + fix `doc/lib/rs/moq-mux.md`'s promise that a data track created before the + first frame follows the anchor. +- Test: a data track registered before a container's first frame, then a + container starting at a nonzero PTS: the clock never moves, and the + container's frames land at now. This rewrites + `a_write_follows_the_anchored_clock` (`binary.rs`, `json.rs`) and + `an_anchor_is_not_jitter` (`binary.rs`), which create a data track and then + anchor. - Test: moq-hls import receives its initial catalog while its `Source` is alive, then replaces a rendition's importer, and the replacement keeps the same offset. @@ -78,12 +102,14 @@ Guidance: timestamp, in Rust and JS. Public API: `catalog::Source`, `catalog::Producer::source`, and -`Source::reserve` are new. `catalog::Producer::clock()` now fixes the mapping, +`Source::reserve` are new. `catalog::Producer::clock()` and the first publish +now fix the mapping, and importers no longer publish verbatim PTS when the clock was already taken. Wire: none. ## Required +- [Import at the first frame](/quest/m1/import-first-frame.md) - a lone importer anchors before the first publish, so the publish rule doesn't offset it - [CMAF frame timestamp](/quest/m1/cmaf-frame-timestamp.md) - decoders honour an offset frame timestamp on passthrough tracks ## Related From 0de741e65b7207bd33bdc51adde1e707813ba0a0 Mon Sep 17 00:00:00 2001 From: OneTooMany Date: Mon, 5 Oct 2026 13:59:01 +0200 Subject: [PATCH 6/7] quest: shared-clock rewrites SCTE-35 pts_adjustment by the offset Co-Authored-By: Claude Opus 5.5 --- quest/m1/shared-clock.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/quest/m1/shared-clock.md b/quest/m1/shared-clock.md index ce637b7b6b..39a56f903e 100644 --- a/quest/m1/shared-clock.md +++ b/quest/m1/shared-clock.md @@ -76,6 +76,19 @@ Decided (2026-10-05): withholds the initial catalog, so a handle kept for later importers would hold it for the whole import. Also rejected: importers handing the offset to each other (`with_offset` on all four). +- One payload exception: an SCTE-35 section on a section-framed verbatim track + absorbs the offset in its `pts_adjustment` (modulo 2^33, at 90 kHz), with + `CRC_32` recomputed. Its `pts_time` sits on the source PTS base, and nothing + downstream can recover the offset: the section's frame timestamp is the video + clock at arrival plus that same offset. The field keeps its size and is clear + even in an encrypted section, so TS export and the typed cues of + [#2279](/quest/m2/2279-hang-typed-scte-35-ad-cue-signaling-carried-opaquely.md) + read splice times on the broadcast timeline with no change of their own. +- Rejected for SCTE-35: recording the offset in the `mpegts` catalog section for + TS export to add (a new catalog field every typed consumer would also have to + apply), refusing to offset a TS import that carries sections (SCTE-35 could + never share a clock), and deferring to #2279 (this quest is what makes the + splice times stale). Guidance: @@ -95,6 +108,9 @@ Guidance: - Test: moq-hls import receives its initial catalog while its `Source` is alive, then replaces a rendition's importer, and the replacement keeps the same offset. +- Test: a TS import carrying a `splice_insert` joins a clock already in use. In + its TS export, `pts_time + pts_adjustment` lands on the exported video PTS of + the splice point, and the section's CRC verifies. - Test: start a synthetic capture, then import an fMP4 starting at PTS 0. Both tracks advance from the capture's timeline with no rewind. Also cover the reverse order: an importer first keeps its PTS verbatim. A passthrough From 3064b545d50b9288e73d1fb6aa9ba7f07145c862 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Mon, 5 Oct 2026 10:33:21 -0700 Subject: [PATCH 7/7] quest: apply the maintainer's clock decisions shared-clock offsets every with_clock importer and names its handle catalog::Input; cmaf-frame-timestamp falls back to tfdt for untimed frames and lands after the untimed model. Co-Authored-By: Claude Opus 5.5 --- quest/m1/cmaf-frame-timestamp.md | 18 ++++++++--- quest/m1/import-first-frame.md | 4 +-- quest/m1/shared-clock.md | 53 +++++++++++++++++++------------- 3 files changed, 48 insertions(+), 27 deletions(-) diff --git a/quest/m1/cmaf-frame-timestamp.md b/quest/m1/cmaf-frame-timestamp.md index 91c5360018..d4a7957ab1 100644 --- a/quest/m1/cmaf-frame-timestamp.md +++ b/quest/m1/cmaf-frame-timestamp.md @@ -25,17 +25,27 @@ shifts `trun` data offsets. confirm with a test. - Check the TS exporter's verbatim carriage, which keeps PES PTS inside the payload. Rewrite those from the frame timestamp, or refuse a mismatch. -- An untimed CMAF frame is refused once the frame timestamp is the - fragment's timeline (decided 2026-10-02, with - [moq-net carries untimed frames faithfully](/quest/m1/untimed-model.md)). +- An untimed CMAF frame decodes at its `tfdt` (decided 2026-10-05, replacing + the 2026-10-02 decision to refuse it). Rejected: refusing it, and parking + the quest. - `main`: a behavior fix with no API change. +Open PR [#4826](https://github.com/moq-dev/moq/pull/4826) implements this. It +lands after [#4822](https://github.com/moq-dev/moq/pull/4822) +([untimed model](/quest/m1/untimed-model.md)), which brings untimed frames to +`main` and itself waits on a typed-timedness mock-up quest. + Test: in Rust and JS, a fragment whose `tfdt` disagrees with its frame -timestamp decodes at the frame timestamp, with B-frame offsets preserved. +timestamp decodes at the frame timestamp, with B-frame offsets preserved, and +an untimed fragment decodes at its `tfdt`. Public API: none. Wire: none; this states what the frame timestamp already means. +## Required + +- [Untimed model](/quest/m1/untimed-model.md) - untimed frames reach `main`, so the `tfdt` fallback has something to decode + ## Related - [Shared clock](/quest/m1/shared-clock.md) - the first publisher to rely on it diff --git a/quest/m1/import-first-frame.md b/quest/m1/import-first-frame.md index ad7ceb0fbc..4356b04a6a 100644 --- a/quest/m1/import-first-frame.md +++ b/quest/m1/import-first-frame.md @@ -20,8 +20,8 @@ clock and offsets that container instead. The anchor is unreleased, and this breaks an in-tree path (an fMP4 or MKV import read by moq-hls export), so it lands before the next release cut. -Requested by an external consumer (OneTooMany); scope and milestone are -proposals for the maintainer. +Requested by an external consumer (OneTooMany). Ranked first in m1, ahead of +shared-clock and cmaf-frame-timestamp (maintainer, 2026-10-05). Decided (2026-10-05): diff --git a/quest/m1/shared-clock.md b/quest/m1/shared-clock.md index 39a56f903e..f742f84e72 100644 --- a/quest/m1/shared-clock.md +++ b/quest/m1/shared-clock.md @@ -62,20 +62,26 @@ Decided (2026-10-05): capture-only catalog would never publish). Fixing the clock at catalog creation is rejected too: even a lone importer would carry frame timestamps that disagree with its `tfdt`. -- One source's offset lives on a public `catalog::Source` handle, separate - from the publication gate: `catalog.source()` returns a clonable handle for - one PTS base, and `source.reserve()` mints a `Reserved` gated as today that - shares the source's offset. The first anchor through any of them sets it. - `catalog.reserve()` keeps a fresh offset per call. Holding a `Source` never +- One source's offset lives on a public `catalog::Input` handle, separate + from the publication gate: `catalog.input()` returns a clonable handle for + one PTS base, and `input.reserve()` mints a `Reserved` gated as today that + shares the input's offset. The first anchor through any of them sets it. + `catalog.reserve()` keeps a fresh offset per call. Holding an `Input` never withholds the catalog. moq-hls import holds one for the whole import, so its renditions (separate fMP4 importers on one PTS base) and every replacement importer on an `EXT-X-MAP` change share one offset; separate offsets would - shift each by its first frame's arrival gap. The name is a proposal for the - maintainer. + shift each by its first frame's arrival gap. Named `Input`, not `Source`, + which would clash with the public `moq_mux::Source`; `Timebase` was the + other candidate. - Rejected: sharing the offset through `Reserved` clones. A live `Reserved` withholds the initial catalog, so a handle kept for later importers would hold it for the whole import. Also rejected: importers handing the offset to each other (`with_offset` on all four). +- A `with_clock` catalog is fixed from the start, so every importer gets an + offset, the first included. Otherwise a later importer could end up with a + negative timestamp. This drops the promise that a `with_clock` PTS zero + names a recording's real start. Rejected: the first importer keeps its PTS + and only later ones shift, and no `with_clock` importer shifts. - One payload exception: an SCTE-35 section on a section-framed verbatim track absorbs the offset in its `pts_adjustment` (modulo 2^33, at 90 kHz), with `CRC_32` recomputed. Its `pts_time` sits on the source PTS base, and nothing @@ -94,18 +100,23 @@ Guidance: - The offset is signed: a stream starting at 3600 s on a clock reading 10 s shifts down. Refuse a frame that would land below zero rather than clamp it. -- A `with_clock` catalog is fixed from the start, so its importers offset too. - Today they publish verbatim PTS on a clock they didn't place. -- Document on `catalog::Producer::clock` that taking the clock fixes it, and - fix `doc/lib/rs/moq-mux.md`'s promise that a data track created before the - first frame follows the anchor. +- Docs: document on `catalog::Producer::clock` that taking the clock fixes + it. Fix every doc that says the clock re-anchors or that `with_clock` keeps + PTS verbatim: `Config::with_clock` in `catalog/producer.rs`, + `doc/lib/rs/moq-mux.md` (the importer paragraph: a data track created before + the first frame following the anchor, and a `with_clock` zero naming the + real start), `doc/setup/upgrade.md` (pinning with `with_clock` when the + source's zero is known, and reading `catalog.clock()` at write time because + an importer re-anchors it), and the `binary.rs` module doc's "read at write + time" note. Replace `with_clock_names_the_contents_real_start` with a test + that a `with_clock` catalog's first importer is shifted onto it. - Test: a data track registered before a container's first frame, then a container starting at a nonzero PTS: the clock never moves, and the container's frames land at now. This rewrites `a_write_follows_the_anchored_clock` (`binary.rs`, `json.rs`) and `an_anchor_is_not_jitter` (`binary.rs`), which create a data track and then anchor. -- Test: moq-hls import receives its initial catalog while its `Source` is +- Test: moq-hls import receives its initial catalog while its `Input` is alive, then replaces a rendition's importer, and the replacement keeps the same offset. - Test: a TS import carrying a `splice_insert` joins a clock already in use. In @@ -113,14 +124,14 @@ Guidance: the splice point, and the section's CRC verifies. - Test: start a synthetic capture, then import an fMP4 starting at PTS 0. Both tracks advance from the capture's timeline with no rewind. Also cover the - reverse order: an importer first keeps its PTS verbatim. A passthrough - fragment whose `tfdt` disagrees with its frame timestamp decodes at the frame - timestamp, in Rust and JS. - -Public API: `catalog::Source`, `catalog::Producer::source`, and -`Source::reserve` are new. `catalog::Producer::clock()` and the first publish -now fix the mapping, -and importers no longer publish verbatim PTS when the clock was already taken. + reverse order: an importer first on a default clock keeps its PTS verbatim. + A passthrough fragment whose `tfdt` disagrees with its frame timestamp + decodes at the frame timestamp, in Rust and JS. + +Public API: `catalog::Input`, `catalog::Producer::input`, and +`Input::reserve` are new. `catalog::Producer::clock()` and the first publish +now fix the mapping, and importers no longer publish verbatim PTS when the +clock was already taken or set with `Config::with_clock`. Wire: none. ## Required