Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion quest/m0/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand All @@ -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
Comment thread
kixelated marked this conversation as resolved.
Resolved
83 changes: 83 additions & 0 deletions quest/m0/hls-media-grid.md
Original file line number Diff line number Diff line change
@@ -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 `<track>.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, `<track>.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
14 changes: 9 additions & 5 deletions quest/m1/hls-conformance.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
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.
Expand Down
78 changes: 13 additions & 65 deletions quest/m1/time-addressing/consumers.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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 `<track>.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
Expand All @@ -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
1 change: 0 additions & 1 deletion quest/m2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
29 changes: 0 additions & 29 deletions quest/m2/producer-retire.md

This file was deleted.

Loading