diff --git a/quest/m0/README.md b/quest/m0/README.md index 7cefe94a3d2..90b85920fba 100644 --- a/quest/m0/README.md +++ b/quest/m0/README.md @@ -3,7 +3,10 @@ ## Goal The work in flight now: a passing Nightly, fixes for the `release` line, the -quest merge driver, local reviews, CI cost and latency, and test flakes. +quest merge driver, local reviews, CI cost and latency, test flakes, and +replacing the catalog's HLS records with a declared segment grid. That last +one is a breaking catalog and API change, pulled ahead of m1's other planned +format changes; it lands on `main` and ships with the next cut. ## Plan @@ -18,6 +21,10 @@ serve instead of enumerating broadcasts. Serving the relay's ingested-only view (`origin::Consumer::local()`) to localhost workers belongs to moq.pro's edge, which embeds moq-relay; it moved there on 2026-09-28. +The [HLS grid](/quest/m0/hls-media-grid.md) quest is the one breaking change +here (maintainer, 2026-11-02): it moved out of m1's time-addressing chain, +since cutting segments from first-frame pts waits on nothing in m1. + ## Required - [Nightly passes on main](/quest/m0/nightly/README.md) - the scheduled Nightly passes end to end, so red means a new break @@ -30,3 +37,4 @@ edge, which embeds moq-relay; it moved there on 2026-09-28. - [CI cost and latency](/quest/m0/ci/README.md) - cloud CI runs only in a batched merge gate as one `just ci` pass per batch, expensive checks run nightly and gate `release`, caches move to R2, and runners are chosen last - [More tests under load](/quest/m0/test-flakes-2/README.md) - load-only and queue failures, one quest per flake, fixed at the cause, since each one fails a whole batch - [Backport the dot-segment and auth-redirect refusals](/quest/m0/backport-release-hardening.md) - `release`'s relay refuses dot-segment paths and its auth client refuses redirects +- [HLS export on a declared grid](/quest/m0/hls-media-grid.md) - the catalog's `hls` field and record tracks go away, and the export numbers segments on `media.join.interval` from media tracks diff --git a/quest/m0/hls-media-grid.md b/quest/m0/hls-media-grid.md new file mode 100644 index 00000000000..6d08c9afce2 --- /dev/null +++ b/quest/m0/hls-media-grid.md @@ -0,0 +1,83 @@ +# [L] HLS export cuts segments on a declared grid, without records + +## Goal + +hang drops the per-rendition `hls` catalog field, the `.hls` record +tracks, and A/V segment alignment. The catalog gains a `media` section with +`media.join.interval`, and the root `clock` moves to `media.clock` in the same +break. moq-mux, moq-c, and `@moq/publish` declare the interval. The HLS/DASH +export and the HLS recorder read the media tracks directly and number segments +on that grid from each group's first-frame pts. An HLS-only viewer of an +on-demand publisher starts its capture and gets a playlist. + +## Plan + +Decided 2026-11-02 (maintainer): the removal moves to m0, decoupled from +[Consumers by start](/quest/m1/time-addressing/consumers.md). That quest had +planned it (2026-10-27) to follow the time-addressing chain, cutting segments +from group start timestamps. Here the export uses each group's first-frame +pts instead, so nothing waits on groups by start. Once groups are addressed by +start, consumers.md swaps the first-frame pts for the group start. + +- **Catalog.** A `media` section holds what applies to every audio and video + track. `media.join.interval` promises that each one starts a group at least + that often outside a pause, and is the export's grid, so every rendition + shares segment numbers. It is fixed for the broadcast instance, since the + numbering depends on it. Nested so a later join mechanism that is not a + fixed grid can sit beside it. moq-mux declares it from its GOP hint, moq-c + from `group_duration_us` ([moq-dev/moq#7068](https://github.com/moq-dev/moq/pull/7068)), + which declares the `hls` duration today, and `@moq/publish` from its GOP + setting. A broadcast without an interval is not HLS-renderable. +- **Clean break (maintainer, 2026-11-02).** No record fallback in the export, + and no records emitted for old exports. `hang::hls`, the `hls` field in Rust + and the JS zod schema, `.hls` tracks, and A/V segment alignment are + deleted; `moq_mux::hls` shrinks to declaring the interval. +- **Segmentation (maintainer, 2026-10-27/28, carried over unchanged).** HLS + numbers segments by position, so every edge and every reload must derive the + same consecutive numbers from timestamps alone; a pts grid is the only + stateless rule that does. Rejected: one platform grid (long-GOP publishers + overrun it constantly), per-edge counting (failover renumbers), and a + publisher counter (per-producer state again). + - Each group belongs to one segment, numbered by grid line: the first line + it contains, so segment N starts at the group containing pts + N × interval. A group containing no line joins the segment of the group + before it, or after a gap, the segment of the line it starts after. The + edge trims each segment to its own groups. The media sequence number is N. + - Whole groups only. Never split a group: a segment starting mid-GOP is not + independent. + - A segment with no groups is `EXT-X-GAP`, so numbering never skips across a + pause or an overrun. A group overrunning the interval is listed at its + first line, so its `EXTINF` understates the media; document the deviation + beside the fixed target duration's overrun rule in + `rs/moq-hls/src/export/rendition.rs`. + - The target duration is the interval (2026-10-28). A segment can run up to + twice the interval even without an overrun, since the group containing a + line can start up to one interval before it; regular GOPs never do. One + whose rounded `EXTINF` exceeds the target is listed under the overrun + rule. Rejected: twice the interval, which doubles every player's + hold-back. + - DASH lists the same segments by `$Time$`. +- Pin the segmentation with table-driven tests over pauses that end between + lines or past the next one, unequal GOPs, and overruns. +- **On-demand capture (maintainer, 2026-11-02, moved from consumers.md).** + The exporter requests record tracks today, but on-demand capture starts from + media-track demand, so an HLS-only viewer never starts it. Prove with a test + that an HLS-only viewer of an on-demand publisher (a default + `moq import capture`, whose audio has been on demand since + [moq-dev/moq#7238](https://github.com/moq-dev/moq/pull/7238)) starts capture + and gets a playlist with both renditions. +- Convert the HLS recorder (`rs/moq-cli/src/hls/record.rs`). Update the hang + draft and the HLS, CLI, and archive docs this makes stale, including + `doc/concept/archives.md`, whose HLS paragraph says a recording needs its + `hls` record tracks. + +Public API: breaking. `hang::hls` and the per-rendition `hls` field are +deleted, `clock` moves to `media.clock`, and `moq_mux::hls` shrinks to +declaring the interval. Wire: hang drops the `hls` field and the record +tracks, adds `media.join.interval`, and moves `clock` to `media.clock`. + +## Related + +- [Consumers by start](/quest/m1/time-addressing/consumers.md) - later swaps the first-frame pts for group starts +- [HLS and DASH segment conformance](/quest/m1/hls-conformance.md) - the nightly probes validate this segmentation +- [HLS export follows a resolution change](/quest/m1/hls-reinit-same-codec.md) - its hand-over race is reconsidered without records diff --git a/quest/m1/hls-conformance.md b/quest/m1/hls-conformance.md index 221a0b866a8..35bad6177bd 100644 --- a/quest/m1/hls-conformance.md +++ b/quest/m1/hls-conformance.md @@ -15,16 +15,20 @@ reloads and a mid-run catalog estimate update. From the 2026-10-24 moq-hls audit, which found no player-level HLS or DASH coverage. Decided with the maintainer on 2026-10-24: -- ffprobe conformance lands in m1, so it validates the - Catalog HLS field redesign and the fixed target duration before the cut. A +- ffprobe conformance lands in m1, so it validates the grid segmentation of + [HLS export on a declared grid](/quest/m0/hls-media-grid.md) (which + replaced the Catalog HLS field on 2026-11-02) and the fixed target duration + before the cut. A browser player test follows in m2 ([HLS and DASH browser playback](/quest/m2/hls-player-test.md)). - Cover a sliding window that evicts segments, a GOP that overruns the declared duration, a mid-run reconfiguration (new rendition instance), and a recording through the segment cursors. -- Cover an unaligned ladder, renditions whose keyframes and segment numbers - differ: every segment is listed and fetched (no `EXT-X-GAP`), and the DASH - manifest omits `segmentAlignment`. +- Cover an unaligned ladder, renditions whose keyframes differ: every + segment is listed and fetched (no `EXT-X-GAP`), and the DASH manifest + omits `segmentAlignment`. Since the grid replaced records (2026-11-02), + every rendition shares the grid's segment numbers while its segment + boundaries follow its own keyframes; the test checks both. - Runs nightly; move any part fast enough into the merge gate. Public API: none. Wire: none. diff --git a/quest/m1/time-addressing/consumers.md b/quest/m1/time-addressing/consumers.md index d22127353e3..1aa026a91e6 100644 --- a/quest/m1/time-addressing/consumers.md +++ b/quest/m1/time-addressing/consumers.md @@ -4,9 +4,9 @@ Every first-party consumer that numbered groups works by start timestamp, in Rust and JS, and hang stops writing duration and discontinuity markers -that group ends and gaps now carry. The HLS/DASH export cuts segments -from group starts and ends on the broadcast's declared join interval, so the -HLS records go away. +that group ends and gaps now carry. The HLS/DASH export places each group on +the declared `media.join.interval` grid by its start instead of its first +frame's pts. ## Plan @@ -21,53 +21,13 @@ Decided 2026-10-25 (maintainer), from the spike: - Legacy egress synthesizes neither: old players lose end durations and pause markers, which they already tolerate being absent. -Decided 2026-10-27 (maintainer): moq-mux publishes HLS records by default -until this lands. Then the `.hls` record tracks, the per-rendition -`hls` catalog field, and any A/V segment alignment go away. - -Decided 2026-10-27 (maintainer, from an HLS audit): how the export -cuts segments without records. HLS numbers segments by position, so every -edge and every reload must derive the same consecutive numbers from -timestamps alone, and a pts grid is the only stateless rule that does. -Rejected: one platform grid (long-GOP publishers overrun it constantly), -per-edge counting (failover renumbers), and a publisher counter (per-producer -state again). - -- **A catalog `media` section** holds what applies to every audio and video - track. `media.join.interval` promises that each one starts a group at least - that often outside a pause, and is the export's grid, so every rendition - shares segment numbers. It is fixed for the broadcast instance, since the - numbering depends on it. The root `clock` moves to `media.clock` in the same - break. Nested so a later join mechanism that is not a fixed grid can sit - beside it. - moq-mux declares the interval from its GOP hint, and `@moq/publish` does - too. moq-c's `group_duration_us` hint - ([#7068](https://github.com/moq-dev/moq/pull/7068)), which declares the `hls` - duration today, then declares the interval. A broadcast without one is not HLS-renderable. -- **Each group belongs to one segment, numbered by grid line**: the first - line it contains, so an edge finds segment N from a `Time` start at pts - N × interval, the group that resolves to. A group containing no line - joins the segment of the group before it, or after a gap, the segment of - the line it starts after. The edge trims each segment to its own groups, - so ranges need no exclusive time end. The media sequence number is N. -- **Whole groups only.** Never split a group: a segment starting mid-GOP is - not independent. -- **A segment with no groups is `EXT-X-GAP`**, so numbering never skips - across a pause or an overrun. A group overrunning the interval is listed - at its first line, so its `EXTINF` understates the media; document the - deviation beside the fixed target duration's overrun rule (in - `rs/moq-hls/src/export/rendition.rs`), whose target then comes from the - interval. -- **The target duration is the interval** (maintainer, 2026-10-28). A - segment can run up to twice the interval even without an overrun, since - the group containing a line can start up to one interval before it; - regular GOPs never do. One whose rounded `EXTINF` exceeds the target is - listed under the overrun rule. Rejected: twice the interval, which is - RFC 8216 compliant but doubles every player's hold-back. -- DASH lists the same segments by `$Time$`. - -Pin the segmentation with table-driven tests over pauses that end between -lines or past the next one, unequal GOPs, and overruns. +Decided 2026-11-02 (maintainer): removing the HLS records, the per-rendition +`hls` field, and A/V segment alignment moved to m0 as +[HLS export on a declared grid](/quest/m0/hls-media-grid.md), which also adds +`media.join.interval`, moves `clock` to `media.clock`, and settles the +segmentation rules decided here on 2026-10-27/28. It places groups by their +first frame's pts; this quest swaps that for the group start, without +changing the segment numbers. Convert each consumer that numbers groups: hang and moq-mux timelines, HLS/DASH export, `seek` for GOP-aligned failover in moq-mux, HLS import's @@ -76,24 +36,12 @@ relay and CLI `fetch --group`, and player continuity checks in the Rust and JS container consumers and `js/watch`. Update the hang draft and the docs each one makes stale; rename CLI flags per the repo's CLI rule. -Added 2026-10-27 (maintainer, from an external release-to-main audit): an -HLS exporter today requests the record tracks, but on-demand capture starts -from media-track demand, so an HLS-only viewer never starts it. Once the -export reads media tracks directly, prove that an HLS-only viewer of an -on-demand publisher starts capture and gets a playlist. Since -[moq-dev/moq#7238](https://github.com/moq-dev/moq/pull/7238), Rust capture -audio is on demand by default (raw PCM kept until subscribed), so until then -an HLS-only viewer of a default `moq import capture` gets no audio rendition -without `--audio on`. - Public API: breaking where a consumer exposes group numbers (CLI -`--group`, archive keys), `hang::hls` and the per-rendition `hls` field are -deleted, and `moq_mux::hls` shrinks to declaring the interval. Wire: hang -drops empty duration frames, discontinuity marker groups, the `hls` field, -and the record tracks, adds `media.join.interval`, and moves `clock` to -`media.clock`. +`--group`, archive keys). Wire: hang drops empty duration frames and +discontinuity marker groups. ## Required +- [HLS export on a declared grid](/quest/m0/hls-media-grid.md) - supplies the grid and removes the records; this quest only swaps its first-frame pts placement for group starts - [Rust groups by start](/quest/m1/time-addressing/model.md) - the Rust API these consume - [@moq/net by start](/quest/m1/time-addressing/js.md) - the JS API these consume diff --git a/quest/m2/README.md b/quest/m2/README.md index b72e6da55be..709d54c5edd 100644 --- a/quest/m2/README.md +++ b/quest/m2/README.md @@ -47,7 +47,6 @@ lite-07; bitrate enforcement is not part of this release. - [HLS and DASH browser playback](/quest/m2/hls-player-test.md) - hls.js and dash.js play, reload, switch, and survive reconfiguration nightly - [The DASH manifest keeps segments across a reconfigure](/quest/m2/dash-reconfigure.md) - a same-codec reconfigure keeps the old instance's segments listed in DASH, as HLS does - [Re-init retires timelines](/quest/m2/reinit-retire.md) - fMP4 and MKV re-inits retire the old timeline, so import state stays bounded -- [Replaced producers retire their records](/quest/m2/producer-retire.md) - encoders and bindings retire a replaced producer's HLS records - [Broadcast fairness](/quest/m2/broadcast-fairness.md) - broadcasts share a cluster link fairly, so one tenant's priority never starves another's - [Cache fair share](/quest/m2/cache-fairness.md) - one tenant cannot crowd another out of the relay's shared group cache - [Peer grant bound](/quest/m2/peer-grant-bound.md) - a peer's AUTH_OK costs near-linear time in its bytes on every input shape, in Rust and in a browser diff --git a/quest/m2/producer-retire.md b/quest/m2/producer-retire.md deleted file mode 100644 index 54c1c28c519..00000000000 --- a/quest/m2/producer-retire.md +++ /dev/null @@ -1,29 +0,0 @@ -# [S] Replaced producers retire their records - -## Goal - -An application that keeps replacing a rendition's producer on one broadcast -does not keep one HLS record track per replacement until the broadcast ends. -moq-video, moq-audio, moq-ffi, and moq-c retire a replaced producer's -records, as the container importers and `moqsink` do since -[moq-dev/moq#6937](https://github.com/moq-dev/moq/pull/6937). - -## Plan - -Decided 2026-10-28 (maintainer): m2, since the churn comes from the local -application, not a network peer. - -- Each of these mints a fresh track name per producer. Call - `moq_mux::catalog::Producer::retire` when a producer is replaced or - dropped mid-broadcast, after its last frame is flushed and its recorder is - released (`retire` errors while the recorder is alive). -- Keep #6783's rule that a dropped rendition's name can return with its - record numbering continued: only the owner that minted a name retires it. -- Test repeated replacement in each: record tracks stay flat. - -Public API: none expected; the bindings may document that replacing a -producer ends its records. Wire: none. - -## Related - -- [Re-init retires timelines](/quest/m2/reinit-retire.md) - the same bound for fMP4 and MKV re-inits