From 623e96081c0d8ebece2da5bb99643acb0188c5de Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Fri, 25 Sep 2026 10:44:23 -0700 Subject: [PATCH 1/8] quest(archive): replay catalog for stock HLS replay Co-Authored-By: Claude Opus 5.5 --- quest/m1/archive/README.md | 1 + quest/m1/archive/dvr.md | 5 ++++ quest/m1/archive/replay-catalog.md | 40 ++++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+) create mode 100644 quest/m1/archive/replay-catalog.md diff --git a/quest/m1/archive/README.md b/quest/m1/archive/README.md index b9117707b2..924575e813 100644 --- a/quest/m1/archive/README.md +++ b/quest/m1/archive/README.md @@ -126,6 +126,7 @@ owned by that prerequisite, not duplicated in archive storage. ## Quests +- [Replay catalog](/quest/m1/archive/replay-catalog.md) - `moq import archive` publishes the recorded catalog live with `store` set, so stock `moq export hls` serves the whole replay - [Browser archive](/quest/m1/archive/browser.md) - the same contract for browser-published broadcasts - [DVR rewind](/quest/m1/archive/dvr.md) - seek through a bounded archive and return to live playback - [Archive proof](/quest/m1/archive/proof.md) - prove persistence ordering, selective reads, exact FETCH replay, and timeline-only HLS generation diff --git a/quest/m1/archive/dvr.md b/quest/m1/archive/dvr.md index 096633a809..9161ca2c3a 100644 --- a/quest/m1/archive/dvr.md +++ b/quest/m1/archive/dvr.md @@ -26,6 +26,11 @@ The reader evicts popped spans from its object cache, but a group it already served stays in `moq_net`'s track cache until the pool reclaims it. Decide whether expiry during a seek needs a group eviction API in `moq-net`. +`moq-hls` reads a timeline from the catalog's own broadcast, and an +`archive.replay` path only marks it non-durable. Decide whether a live +broadcast's exporter follows `replay` to the recording for rewind, or whether +viewers address the replay broadcast directly. + ## Closes - [#2275](https://github.com/moq-dev/moq/issues/2275) - close this issue when the quest finishes diff --git a/quest/m1/archive/replay-catalog.md b/quest/m1/archive/replay-catalog.md new file mode 100644 index 0000000000..af9c3de47f --- /dev/null +++ b/quest/m1/archive/replay-catalog.md @@ -0,0 +1,40 @@ +# [S] Replay catalog + +## Goal + +A replayed recording plays as VOD HLS from the stock server: `moq export +archive`, then `moq import archive`, then `moq export hls` lists the whole +recording with no embedder supplying a catalog. The replay broadcast publishes +its recorded catalog live, stamped with the recording's `store` and `version`, +so the exporter treats its timeline as durable and lists past `--window`. A +`--follow` replay grows like an event playlist. + +Choosing which catalog applies to which media group stays with +[Catalog track identity](/quest/m2/catalog-tracks.md). + +## Plan + +Today the reader writes live groups only for the timeline; the recorded catalog +is FETCH-only, so a SUBSCRIBE to it on the replay broadcast never sees a group +and `moq-hls` never finds the `archive` entry. The HLS archive tests work around +this by hand-building a catalog. + +- Republish each recorded catalog group live in timeline order, so the newest + one is at the live edge and a `--follow` replay picks up catalogs recorded + after it opened. +- Stamp `store` with the URL passed to `import archive`, and `version` with the + recording format. Refuse a URL carrying userinfo so credentials never land in + a catalog. `replay` stays unset: the timeline lives on this broadcast. +- Keep the logic in `moq-cli`; `moq-archive` stays catalog-agnostic. Select the + catalog track with the CLI's catalog format as `export archive` does: hang + and hang.z are stamped, MSF is refused. +- `import` hardcodes the default timeline name. If the recorded catalog is + reachable first, prefer its `archive.track`. + +Add a CI test that records a broadcast longer than the default window, replays +it, and asserts the stock exporter lists segment 0 with a durable +`timeShiftBufferDepth`. Consider moving the HLS archive tests onto the +republished catalog instead of their hand-built one. + +Update `doc/bin/cli.md` and `doc/bin/hls.md` inline with an export, import, and +serve example. From 4320405cb467eb2bafbb075db37557b15c36e305 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Fri, 25 Sep 2026 11:44:11 -0700 Subject: [PATCH 2/8] quest(archive): plan per-track segments before replay catalog Co-Authored-By: Claude Opus 5.5 --- quest/m1/archive/README.md | 1 + quest/m1/archive/replay-catalog.md | 8 +++++++ quest/m1/archive/track-segments.md | 38 ++++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+) create mode 100644 quest/m1/archive/track-segments.md diff --git a/quest/m1/archive/README.md b/quest/m1/archive/README.md index 924575e813..47f520bda2 100644 --- a/quest/m1/archive/README.md +++ b/quest/m1/archive/README.md @@ -126,6 +126,7 @@ owned by that prerequisite, not duplicated in archive storage. ## Quests +- [Per-track archive segments](/quest/m1/archive/track-segments.md) - decide per-track segmenting and expiry so a DVR keeps every track's newest group while HLS stays aligned - [Replay catalog](/quest/m1/archive/replay-catalog.md) - `moq import archive` publishes the recorded catalog live with `store` set, so stock `moq export hls` serves the whole replay - [Browser archive](/quest/m1/archive/browser.md) - the same contract for browser-published broadcasts - [DVR rewind](/quest/m1/archive/dvr.md) - seek through a bounded archive and return to live playback diff --git a/quest/m1/archive/replay-catalog.md b/quest/m1/archive/replay-catalog.md index af9c3de47f..f833cf947c 100644 --- a/quest/m1/archive/replay-catalog.md +++ b/quest/m1/archive/replay-catalog.md @@ -38,3 +38,11 @@ republished catalog instead of their hand-built one. Update `doc/bin/cli.md` and `doc/bin/hls.md` inline with an export, import, and serve example. + +A DVR can expire the only recorded catalog today; import must work once +[Per-track archive segments](/quest/m1/archive/track-segments.md) keeps one in +the window, and fail loudly on a recording with none. + +## Required + +- [Per-track archive segments](/quest/m1/archive/track-segments.md) - a DVR keeps a catalog in its window diff --git a/quest/m1/archive/track-segments.md b/quest/m1/archive/track-segments.md new file mode 100644 index 0000000000..04b82f3809 --- /dev/null +++ b/quest/m1/archive/track-segments.md @@ -0,0 +1,38 @@ +# [M] Per-track archive segments + +## Goal + +Decide how a recording's tracks segment and expire independently, so a DVR +keeps the newest group of every track, such as a catalog that never changes, +while HLS still gets aligned audio and video segments. Produce the draft change +and rewritten implementation quests; this is a planning quest, not production +code. + +## Plan + +Today one timeline numbers segments for every track. A non-pacing track's group +is listed only in the segment open when it arrived, so a DVR expires a static +catalog with the first video segment and deletes the only copy. A long-lived +group, such as a catalog carrying deltas, is listed once in the segment it +opened in, so frames appended later are not addressable per segment. + +Aligned segments exist mainly for HLS, and they break down for tracks that are +not audio or video. Open questions: + +- A segment counter per track, so catalog or audio segment 0 need not align + with video segment 0, versus a timeline per track. +- How HLS keeps aligned renditions under either shape, and whether every + track's segments still flush at the same timeline commit. +- Whether a long-lived group's frames split across segments, trading an extra + GET for per-segment addressability, while audio and video groups stay one + object each. +- How expiry keeps each track's newest group, and what a reader or importer + does when a track has nothing in the window. + +Weigh the format and wire cost against the landed writer, reader, and HLS +exporter, and against [Catalog track identity](/quest/m2/catalog-tracks.md). +Present the recommendation for maintainer agreement before changing the draft. + +## Related + +- [Catalog track identity](/quest/m2/catalog-tracks.md) - which catalog applies to a media group, which per-track expiry must not settle by accident From 264c8d6d4e610d7c857e86006b1ad20c0d13f091 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Fri, 25 Sep 2026 11:48:30 -0700 Subject: [PATCH 3/8] quest(archive): replace the recorded archive entry on replay Co-Authored-By: Claude Opus 5.5 --- quest/m1/archive/replay-catalog.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/quest/m1/archive/replay-catalog.md b/quest/m1/archive/replay-catalog.md index f833cf947c..e73c6bc1eb 100644 --- a/quest/m1/archive/replay-catalog.md +++ b/quest/m1/archive/replay-catalog.md @@ -28,8 +28,9 @@ this by hand-building a catalog. - Keep the logic in `moq-cli`; `moq-archive` stays catalog-agnostic. Select the catalog track with the CLI's catalog format as `export archive` does: hang and hang.z are stamped, MSF is refused. -- `import` hardcodes the default timeline name. If the recorded catalog is - reachable first, prefer its `archive.track`. +- The recorded catalog's `archive` entry describes the source's live timeline, + not the recording's, so replace it with the timeline the reader replays + (track, timescale, duration bound) rather than trusting the recorded one. Add a CI test that records a broadcast longer than the default window, replays it, and asserts the stock exporter lists segment 0 with a durable From 5b4dfcfe7959b52d849df286d5ad893cc55a9209 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Fri, 25 Sep 2026 12:34:28 -0700 Subject: [PATCH 4/8] quest(archive): per-track timelines questline Co-Authored-By: Claude Opus 5.5 --- quest/m1/archive/README.md | 5 ++- quest/m1/archive/browser.md | 1 + quest/m1/archive/dvr.md | 4 +++ quest/m1/archive/proof.md | 9 +++-- quest/m1/archive/replay-catalog.md | 19 +++++----- quest/m1/archive/track-segments.md | 38 -------------------- quest/m1/archive/track-timeline/README.md | 44 +++++++++++++++++++++++ quest/m1/archive/track-timeline/core.md | 39 ++++++++++++++++++++ quest/m1/archive/track-timeline/js.md | 17 +++++++++ 9 files changed, 125 insertions(+), 51 deletions(-) delete mode 100644 quest/m1/archive/track-segments.md create mode 100644 quest/m1/archive/track-timeline/README.md create mode 100644 quest/m1/archive/track-timeline/core.md create mode 100644 quest/m1/archive/track-timeline/js.md diff --git a/quest/m1/archive/README.md b/quest/m1/archive/README.md index 47f520bda2..89cd0aedad 100644 --- a/quest/m1/archive/README.md +++ b/quest/m1/archive/README.md @@ -63,6 +63,9 @@ DVR expiry recover from the next checkpoint; `Reader::finish` applies out-of-ban ### Format +[Per-track timelines](/quest/m1/archive/track-timeline/README.md) replaces the +aligned segments below with one timeline per track. + The format is the draft's [Recording section](/drafts/draft-lcurley-moq-hang.md#recording). The application chooses the object prefix, selected tracks, retention, and credentials; `moq-archive` owns the @@ -126,7 +129,7 @@ owned by that prerequisite, not duplicated in archive storage. ## Quests -- [Per-track archive segments](/quest/m1/archive/track-segments.md) - decide per-track segmenting and expiry so a DVR keeps every track's newest group while HLS stays aligned +- [Per-track timelines](/quest/m1/archive/track-timeline/README.md) - every track segments and expires on its own timeline, and HLS is derived from group timestamps at the edge - [Replay catalog](/quest/m1/archive/replay-catalog.md) - `moq import archive` publishes the recorded catalog live with `store` set, so stock `moq export hls` serves the whole replay - [Browser archive](/quest/m1/archive/browser.md) - the same contract for browser-published broadcasts - [DVR rewind](/quest/m1/archive/dvr.md) - seek through a bounded archive and return to live playback diff --git a/quest/m1/archive/browser.md b/quest/m1/archive/browser.md index 78a0dd061c..6f6e56b01a 100644 --- a/quest/m1/archive/browser.md +++ b/quest/m1/archive/browser.md @@ -34,3 +34,4 @@ remains downstream (moq.pro) work. ## Required - [JavaScript FETCH](/quest/m1/js-fetch.md) - generic on-demand group serving and IETF FETCH support +- [JS per-track timelines](/quest/m1/archive/track-timeline/js.md) - the timeline this archive records against diff --git a/quest/m1/archive/dvr.md b/quest/m1/archive/dvr.md index 9161ca2c3a..48959cd37c 100644 --- a/quest/m1/archive/dvr.md +++ b/quest/m1/archive/dvr.md @@ -31,6 +31,10 @@ whether expiry during a seek needs a group eviction API in `moq-net`. broadcast's exporter follows `replay` to the recording for rewind, or whether viewers address the replay broadcast directly. +## Required + +- [Rust per-track timelines](/quest/m1/archive/track-timeline/core.md) - seeks through per-track timelines + ## Closes - [#2275](https://github.com/moq-dev/moq/issues/2275) - close this issue when the quest finishes diff --git a/quest/m1/archive/proof.md b/quest/m1/archive/proof.md index 225449edf4..b24575dbb1 100644 --- a/quest/m1/archive/proof.md +++ b/quest/m1/archive/proof.md @@ -7,8 +7,9 @@ reads, and timeline-only HLS generation from one multi-rendition broadcast. ## Plan -Record explicitly enrolled video, audio, catalog, and non-media tracks. Cut at -aligned keyframe boundaries, including multiple audio groups per segment, then +Record explicitly enrolled video, audio, catalog, and non-media tracks. Cut each +track on its own timeline, including many audio groups per object and a group +split across objects by frame, then replay their original sequences, timestamps, and payloads through `track::Dynamic`. @@ -60,3 +61,7 @@ restart must finish this cleanup before accepting new groups. Finally render and reload HLS playlists while rejecting every media-object GET until a segment URI is requested. A segment request must resolve one object from the replayed timeline without any listing or separate index object. + +## Required + +- [Rust per-track timelines](/quest/m1/archive/track-timeline/core.md) - proves the per-track format, not the aligned one diff --git a/quest/m1/archive/replay-catalog.md b/quest/m1/archive/replay-catalog.md index e73c6bc1eb..d6f5bb8ec5 100644 --- a/quest/m1/archive/replay-catalog.md +++ b/quest/m1/archive/replay-catalog.md @@ -14,23 +14,23 @@ Choosing which catalog applies to which media group stays with ## Plan -Today the reader writes live groups only for the timeline; the recorded catalog +Today the reader writes live groups only for timelines; the recorded catalog is FETCH-only, so a SUBSCRIBE to it on the replay broadcast never sees a group and `moq-hls` never finds the `archive` entry. The HLS archive tests work around this by hand-building a catalog. -- Republish each recorded catalog group live in timeline order, so the newest +- Republish each recorded catalog group live in its timeline's order, so the newest one is at the live edge and a `--follow` replay picks up catalogs recorded after it opened. - Stamp `store` with the URL passed to `import archive`, and `version` with the recording format. Refuse a URL carrying userinfo so credentials never land in - a catalog. `replay` stays unset: the timeline lives on this broadcast. + a catalog. `replay` stays unset: the timelines live on this broadcast. - Keep the logic in `moq-cli`; `moq-archive` stays catalog-agnostic. Select the catalog track with the CLI's catalog format as `export archive` does: hang and hang.z are stamped, MSF is refused. -- The recorded catalog's `archive` entry describes the source's live timeline, - not the recording's, so replace it with the timeline the reader replays - (track, timescale, duration bound) rather than trusting the recorded one. +- The recorded catalog's `archive` entry describes the source's live + timelines, not the recording's, so replace it with the timelines the reader + replays rather than trusting the recorded ones. Add a CI test that records a broadcast longer than the default window, replays it, and asserts the stock exporter lists segment 0 with a durable @@ -40,10 +40,9 @@ republished catalog instead of their hand-built one. Update `doc/bin/cli.md` and `doc/bin/hls.md` inline with an export, import, and serve example. -A DVR can expire the only recorded catalog today; import must work once -[Per-track archive segments](/quest/m1/archive/track-segments.md) keeps one in -the window, and fail loudly on a recording with none. +Cover a DVR recording whose catalog outlived its first video segment, and fail +loudly on a recording with no catalog. ## Required -- [Per-track archive segments](/quest/m1/archive/track-segments.md) - a DVR keeps a catalog in its window +- [Rust per-track timelines](/quest/m1/archive/track-timeline/core.md) - a DVR keeps its catalog's newest group diff --git a/quest/m1/archive/track-segments.md b/quest/m1/archive/track-segments.md deleted file mode 100644 index 04b82f3809..0000000000 --- a/quest/m1/archive/track-segments.md +++ /dev/null @@ -1,38 +0,0 @@ -# [M] Per-track archive segments - -## Goal - -Decide how a recording's tracks segment and expire independently, so a DVR -keeps the newest group of every track, such as a catalog that never changes, -while HLS still gets aligned audio and video segments. Produce the draft change -and rewritten implementation quests; this is a planning quest, not production -code. - -## Plan - -Today one timeline numbers segments for every track. A non-pacing track's group -is listed only in the segment open when it arrived, so a DVR expires a static -catalog with the first video segment and deletes the only copy. A long-lived -group, such as a catalog carrying deltas, is listed once in the segment it -opened in, so frames appended later are not addressable per segment. - -Aligned segments exist mainly for HLS, and they break down for tracks that are -not audio or video. Open questions: - -- A segment counter per track, so catalog or audio segment 0 need not align - with video segment 0, versus a timeline per track. -- How HLS keeps aligned renditions under either shape, and whether every - track's segments still flush at the same timeline commit. -- Whether a long-lived group's frames split across segments, trading an extra - GET for per-segment addressability, while audio and video groups stay one - object each. -- How expiry keeps each track's newest group, and what a reader or importer - does when a track has nothing in the window. - -Weigh the format and wire cost against the landed writer, reader, and HLS -exporter, and against [Catalog track identity](/quest/m2/catalog-tracks.md). -Present the recommendation for maintainer agreement before changing the draft. - -## Related - -- [Catalog track identity](/quest/m2/catalog-tracks.md) - which catalog applies to a media group, which per-track expiry must not settle by accident diff --git a/quest/m1/archive/track-timeline/README.md b/quest/m1/archive/track-timeline/README.md new file mode 100644 index 0000000000..784ff87860 --- /dev/null +++ b/quest/m1/archive/track-timeline/README.md @@ -0,0 +1,44 @@ +# Per-track timelines + +## Goal + +Every track carries its own timeline, so tracks segment, commit, and expire +independently. A DVR keeps each track's newest group, such as a catalog that +never changes, and an append-only group stays addressable as frames arrive. An +edge like `moq-hls` derives HLS and DASH from group timestamps, so a publisher +never needs to know about HLS. + +Nothing on `main` has users yet: break the timeline, catalog `archive` entry, +and recording format in place, with no compatibility path. + +## Plan + +Today one timeline numbers aligned segments for every track. A DVR pops whole +segments, so a static catalog expires with the first video segment, and the +writer only records a non-pacing group once it completes, so a group that never +closes is never stored. `moq_json::window` pops from the front, so one shared +timeline cannot keep an old catalog record while dropping newer video. + +Decisions: + +- One timeline track per track, live and recorded. `moq-mux` publishes them for + every broadcast; an unsubscribed track costs nothing. +- The catalog's root `archive` entry maps each track to its timeline, including + the catalog track itself. `replay`, `store`, and `version` stay beside it. +- Each track cuts on its own: automatically at a group boundary between a + minimum and maximum duration (roughly 1s and 10s), splitting a long-lived + group by frame at the maximum. Manual cuts stay as an optimization, such as a + video keyframe cutting audio so derived segments need fewer objects. +- A stored object may hold a frame range of a group, not only whole groups. +- HLS and DASH segments are derived at the edge from group timestamps, not + from storage objects. Fetching extra objects is fine when they land in the + reader's cache for the next request. + +## Quests + +- [Rust per-track timelines](/quest/m1/archive/track-timeline/core.md) - the draft, `hang`, `moq-mux`, `moq-archive`, and `moq-hls` move to per-track timelines in one change +- [JS per-track timelines](/quest/m1/archive/track-timeline/js.md) - `@moq/hang` publishes and reads the same per-track timelines as Rust + +## Related + +- [Catalog track identity](/quest/m2/catalog-tracks.md) - the catalog's own timeline gives it timestamps, but which catalog applies to a group stays there diff --git a/quest/m1/archive/track-timeline/core.md b/quest/m1/archive/track-timeline/core.md new file mode 100644 index 0000000000..077a15c9c3 --- /dev/null +++ b/quest/m1/archive/track-timeline/core.md @@ -0,0 +1,39 @@ +# [XL] Rust per-track timelines + +## Goal + +The moq-hang draft, `hang`, `moq-mux`, `moq-archive`, and `moq-hls` use one +timeline per track, per the [line's decisions](/quest/m1/archive/track-timeline/README.md). +A DVR recording keeps every track's newest group, an append-only group is +stored as its frames arrive, and `moq-hls` serves aligned HLS and DASH from +live or recorded timelines without the publisher cutting for it. + +## Plan + +These crates compile together, so they change in one PR. Update the draft's +timeline and Recording sections in the same change; most of their rules assume +one aligned segment counter (one timeline per broadcast, cross-track boundaries, +whole-segment retention, reading segment N of track T through record N). + +- **Timeline:** a record describes one stored span of its own track: sequence, + pts, duration, and the group and frame range. Drop cross-track pacing and the + completeness wait; keep manual cuts. Retention pops each track's window + independently and always keeps its newest record. +- **Catalog:** replace the root `archive.track` with a map from track to + timeline, covering the catalog track too. +- **Writer and reader:** commit each track independently, and store frame + ranges so a never-closing group is recorded. Bump the recording `version` and + refuse the old one. Recovery, grace deletion, and FETCH replay follow the + per-track index. +- **HLS and DASH:** choose segment boundaries from group starts common to every + video rendition, near a target duration, and number them so every edge and + every reload agrees, including after DVR pops. Audio and other renditions + take the groups and frames whose timestamps fall in each span, possibly from + more than one object. The reader's cache absorbs the overlap. + +Carry over the existing tests (DVR trim, durable listing, archive replay) and +add a static catalog outliving its first video segment, an append-only group +spanning several objects, audio cut independently of video, and renditions +that start at different times. + +Update `doc/concept/hang.md` and the HLS and CLI docs this makes stale. diff --git a/quest/m1/archive/track-timeline/js.md b/quest/m1/archive/track-timeline/js.md new file mode 100644 index 0000000000..461ff1cf75 --- /dev/null +++ b/quest/m1/archive/track-timeline/js.md @@ -0,0 +1,17 @@ +# [M] JS per-track timelines + +## Goal + +`@moq/hang` publishes one timeline per track with the same records, cuts, and +catalog `archive` map as Rust, and parses them identically. + +## Plan + +Port the landed Rust shape to `js/hang/src/timeline.ts` and its catalog schema, +replacing the aligned timeline and cross-track pacing rather than keeping both. +Cover the same cut rules and a static catalog outliving other tracks' records, +and check the records against Rust output in the interop suite. + +## Required + +- [Rust per-track timelines](/quest/m1/archive/track-timeline/core.md) - the format this mirrors From 4c94f32c33ba21d30e3ef362d59f613b8a3038b5 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Fri, 25 Sep 2026 12:44:12 -0700 Subject: [PATCH 5/8] quest(archive): gap misaligned renditions, land the line together Co-Authored-By: Claude Opus 5.5 --- quest/m1/archive/track-timeline/README.md | 4 +++- quest/m1/archive/track-timeline/core.md | 20 ++++++++++++-------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/quest/m1/archive/track-timeline/README.md b/quest/m1/archive/track-timeline/README.md index 784ff87860..2b4190e23b 100644 --- a/quest/m1/archive/track-timeline/README.md +++ b/quest/m1/archive/track-timeline/README.md @@ -9,7 +9,9 @@ edge like `moq-hls` derives HLS and DASH from group timestamps, so a publisher never needs to know about HLS. Nothing on `main` has users yet: break the timeline, catalog `archive` entry, -and recording format in place, with no compatibility path. +and recording format in place, with no compatibility path, even though hang +0.21 released the current shape. The children merge into this line's branch, +so Rust and JS reach `main` together. ## Plan diff --git a/quest/m1/archive/track-timeline/core.md b/quest/m1/archive/track-timeline/core.md index 077a15c9c3..234160b6e0 100644 --- a/quest/m1/archive/track-timeline/core.md +++ b/quest/m1/archive/track-timeline/core.md @@ -11,7 +11,7 @@ live or recorded timelines without the publisher cutting for it. ## Plan These crates compile together, so they change in one PR. Update the draft's -timeline and Recording sections in the same change; most of their rules assume +timeline and Recording sections in the same change, in a new draft revision; most of their rules assume one aligned segment counter (one timeline per broadcast, cross-track boundaries, whole-segment retention, reading segment N of track T through record N). @@ -25,15 +25,19 @@ whole-segment retention, reading segment N of track T through record N). ranges so a never-closing group is recorded. Bump the recording `version` and refuse the old one. Recovery, grace deletion, and FETCH replay follow the per-track index. -- **HLS and DASH:** choose segment boundaries from group starts common to every - video rendition, near a target duration, and number them so every edge and - every reload agrees, including after DVR pops. Audio and other renditions - take the groups and frames whose timestamps fall in each span, possibly from - more than one object. The reader's cache absorbs the overlap. +- **HLS and DASH:** take segment boundaries from a reference video rendition, + near a target duration, and number them so every edge and every reload + agrees, including after DVR pops. Every other video rendition snaps each + boundary to its nearest group start within a tolerance (around 1s); a + segment with no start in range becomes a gap (`EXT-X-GAP` in HLS), so a + player switching renditions lands on the next real segment. Audio and other + renditions take the groups and frames whose timestamps fall in each span, + possibly from more than one object. The reader's cache absorbs the overlap. Carry over the existing tests (DVR trim, durable listing, archive replay) and add a static catalog outliving its first video segment, an append-only group -spanning several objects, audio cut independently of video, and renditions -that start at different times. +spanning several objects, audio cut independently of video, renditions that +start at different times, and video renditions whose group starts never +coincide. Update `doc/concept/hang.md` and the HLS and CLI docs this makes stale. From 99f6326ad0747d44239a39cbac01b2e32839f500 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Fri, 25 Sep 2026 12:44:26 -0700 Subject: [PATCH 6/8] quest(archive): rewrap core plan Co-Authored-By: Claude Opus 5.5 --- quest/m1/archive/track-timeline/core.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/quest/m1/archive/track-timeline/core.md b/quest/m1/archive/track-timeline/core.md index 234160b6e0..82bc2253bc 100644 --- a/quest/m1/archive/track-timeline/core.md +++ b/quest/m1/archive/track-timeline/core.md @@ -11,9 +11,10 @@ live or recorded timelines without the publisher cutting for it. ## Plan These crates compile together, so they change in one PR. Update the draft's -timeline and Recording sections in the same change, in a new draft revision; most of their rules assume -one aligned segment counter (one timeline per broadcast, cross-track boundaries, -whole-segment retention, reading segment N of track T through record N). +timeline and Recording sections in the same change, in a new draft revision; +most of their rules assume one aligned segment counter (one timeline per +broadcast, cross-track boundaries, whole-segment retention, reading segment N +of track T through record N). - **Timeline:** a record describes one stored span of its own track: sequence, pts, duration, and the group and frame range. Drop cross-track pacing and the From 0c835d3c6bf24c1601aa9be22a71d9169b98be2f Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Fri, 25 Sep 2026 12:44:52 -0700 Subject: [PATCH 7/8] quest(archive): publishers wanting HLS should align GOPs Co-Authored-By: Claude Opus 5.5 --- quest/m1/archive/track-timeline/core.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/quest/m1/archive/track-timeline/core.md b/quest/m1/archive/track-timeline/core.md index 82bc2253bc..b6b0f3d933 100644 --- a/quest/m1/archive/track-timeline/core.md +++ b/quest/m1/archive/track-timeline/core.md @@ -31,7 +31,9 @@ of track T through record N). agrees, including after DVR pops. Every other video rendition snaps each boundary to its nearest group start within a tolerance (around 1s); a segment with no start in range becomes a gap (`EXT-X-GAP` in HLS), so a - player switching renditions lands on the next real segment. Audio and other + player switching renditions lands on the next real segment. Gaps are a + best-effort fallback: the draft and HLS docs say a publisher wanting HLS + export SHOULD align video GOPs across renditions. Audio and other renditions take the groups and frames whose timestamps fall in each span, possibly from more than one object. The reader's cache absorbs the overlap. From 9f9b25e102cbd5d02ad69055d0c3ab0df43e391f Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Fri, 25 Sep 2026 12:50:42 -0700 Subject: [PATCH 8/8] quest(archive): audio-only boundaries, per-track browser commits Co-Authored-By: Claude Opus 5.5 --- quest/m1/archive/browser.md | 20 ++++++++++---------- quest/m1/archive/track-timeline/core.md | 10 +++++----- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/quest/m1/archive/browser.md b/quest/m1/archive/browser.md index 6f6e56b01a..7d08f060cf 100644 --- a/quest/m1/archive/browser.md +++ b/quest/m1/archive/browser.md @@ -9,16 +9,16 @@ layout, and FETCH behavior as native `moq-archive` users. Port the archive contract to the JS packages with memory and OPFS storage. The application explicitly enrolls video, audio, catalog, or arbitrary data tracks; -the archive does not infer them from Hang. The JS timeline already publishes -through the Window (`js/hang/src/container/timeline.ts:134`, `:165`, backed by -`js/json/src/window/`) with pacing tracks and application-driven cuts; add the -deferred commit Rust has (`Producer::deferred`, -`rs/moq-mux/src/timeline.rs:917`). - -Persist one range-named object per track per segment after its groups complete, -then publish the archive timeline record. Match the 19-digit group-bound keys, -ascending delta-encoded IDs, and sequential timeline discovery used by Rust. A typical audio segment contains many -one-group-per-frame audio groups. Match the Rust binary envelope bytes and `.info` property values, per the [Recording section](/drafts/draft-lcurley-moq-hang.md#recording), without inferring catalog-to-group applicability. +the archive does not infer them from Hang. Record against the per-track +timelines from [JS per-track timelines](/quest/m1/archive/track-timeline/js.md). + +Commit each track independently: persist each stored span, including a frame +range of a still-open group, then publish that track's timeline record. Match +the object keys, envelope bytes, `.info` property values, and timeline +discovery the Rust writer uses after +[Rust per-track timelines](/quest/m1/archive/track-timeline/core.md), per the +[Recording section](/drafts/draft-lcurley-moq-hang.md#recording), without +inferring catalog-to-group applicability. [Catalog track identity](/quest/m2/catalog-tracks.md) addresses that separately. Use [JavaScript FETCH](/quest/m1/js-fetch.md)'s on-demand group requests to diff --git a/quest/m1/archive/track-timeline/core.md b/quest/m1/archive/track-timeline/core.md index b6b0f3d933..4651a338f9 100644 --- a/quest/m1/archive/track-timeline/core.md +++ b/quest/m1/archive/track-timeline/core.md @@ -26,9 +26,9 @@ of track T through record N). ranges so a never-closing group is recorded. Bump the recording `version` and refuse the old one. Recovery, grace deletion, and FETCH replay follow the per-track index. -- **HLS and DASH:** take segment boundaries from a reference video rendition, - near a target duration, and number them so every edge and every reload - agrees, including after DVR pops. Every other video rendition snaps each +- **HLS and DASH:** take segment boundaries from a reference video rendition + (an audio one when the broadcast has no video), near a target duration, and + number them so every edge and every reload agrees, including after DVR pops. Every other video rendition snaps each boundary to its nearest group start within a tolerance (around 1s); a segment with no start in range becomes a gap (`EXT-X-GAP` in HLS), so a player switching renditions lands on the next real segment. Gaps are a @@ -40,7 +40,7 @@ of track T through record N). Carry over the existing tests (DVR trim, durable listing, archive replay) and add a static catalog outliving its first video segment, an append-only group spanning several objects, audio cut independently of video, renditions that -start at different times, and video renditions whose group starts never -coincide. +start at different times, video renditions whose group starts never +coincide, and an audio-only broadcast. Update `doc/concept/hang.md` and the HLS and CLI docs this makes stale.