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
2 changes: 1 addition & 1 deletion quest/m1/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ lands before the cut.
- [IETF fill timeout](/quest/m1/ietf-fill-timeout.md) - native FILL_TIMEOUT bounds upstream waiting on drafts 18-22, alongside local content-age enforcement
- [Browser follows a renamed track](/quest/m1/browser-rename-check.md) - a real browser keeps playing when `@moq/publish` renames a track after a codec change, while size-only changes keep their identity
- [Rust identity enforcement](/quest/m1/catalog-identity-rs.md) - `moq-mux` refuses a redefined rendition, and every Rust publisher mints a new rendition name for a decoder identity change and updates everything else in place
- [Archive](/quest/m1/archive/README.md) - record selected tracks to any object_store and replay them over FETCH or derived HLS; preserve published format boundaries while completing replay and DVR correctness
- [Broadcast archives](/quest/m1/archive/README.md) - SUBSCRIBE_ALL records every track under a prefix to any object_store, a host serves a stored broadcast back, and the catalog drops its archive pointer for an opt-in `hls` field
- [In-band auth](/quest/m1/auth/README.md) - a session tells its peer what it may publish and subscribe to, unions tokens presented in band, and fails loud on an out-of-scope publish
- [IETF epochs](/quest/m1/ietf-epochs.md) - negotiate explicit broadcast identity on drafts 17-22, retaining unchanged paths and epochless clients on updated relays
- [Announcement takeover](/quest/m1/announce-takeover.md) - a new independent route explicitly continues instances served through its predecessor, with shared Rust/JS semantics and lite-07 wire
Expand Down
234 changes: 77 additions & 157 deletions quest/m1/archive/README.md

Large diffs are not rendered by default.

72 changes: 72 additions & 0 deletions quest/m1/archive/catalog-hls.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# [L] Catalog HLS field

## Goal

The hang catalog has no root `archive` section: `timelines`, `timescale`,
`replay`, `store`, and `version` are gone from `rs/hang`, `js/hang`,
`drafts/draft-lcurley-moq-hang.md`, `moq-mux`, and `moq-hls`. What remains of
timelines is an opt-in, per-rendition `hls` field that exists only for HLS
compatibility: it names a track whose records give each HLS segment's first
group and pts, and declares the nominal segment duration. A rendition without
it is not HLS-renderable.

## Plan

Decided by the maintainer on 2026-10-17:

- No publisher-owned archive pointer remains; archives are found by the host
that stores them ([epic](/quest/m1/archive/README.md)).
- **Renamed to `hls`** (catalog field, track naming, types, wording), since it
exists only for HLS compatibility, not archiving or seeking; seeking uses
`Time(pts)` ranges. The track naming convention becomes `<track>.hls.z`;
consumers still use the advertised name.
- **Opt-in.** `moq-mux` publishes it only when the application enables it,
and nothing publishes it by default. Rejected: always-on timelines for every
track, which cost every publisher for a feature most never use.
- **Aligned segments are the opt-in contract.** A publisher that enables `hls`
aligns group boundaries across the renditions it lists, so segment n starts
at the same pts in every rendition. This is part of why it is opt-in. An edge
maps segment n to record n of each rendition directly; the reference
rendition resolving others against their own timelines, keyframe snapping,
and `EXT-X-GAP` for a rendition with no nearby boundary all go away
(`rs/moq-hls/src/export/spans.rs`). An edge that finds renditions misaligned
refuses that segment loudly rather than repairing it.
- **Per rendition, video and audio only.** The catalog track, data tracks, and
untimed tracks get no `hls` field. Each declares its nominal segment
duration, which is the landed per-timeline `duration` (#5715) moved onto the
rendition; it never changes for the life of the track.
- **Records are (first group, pts).** Segments start at group boundaries, so
mid-group splits, frame positions, and record `end`s go away. `EXTINF`
derives from the next record's pts; an ended track's last record carries an
explicit duration. Rejected: group only, which would make the edge read media
to time a segment.
- The archive does not use these records, and DVR seeks with `Time(pts)`
ranges.

Guidance:

- `moq-hls` history mode becomes explicit configuration only: today the
catalog's `store` marks a timeline durable (`durable` in
`rs/moq-hls/src/export/mod.rs`).
- Fold in the old README's fix: `moq-mux`'s consumer refuses a record whose
`sequence` differs from its window index, as its `Producer::resume` already
does
([review](https://github.com/moq-dev/moq/pull/4280#pullrequestreview-5332725290)).
- `js/hang` updates its schema and consumer; the JS producer and the
`@moq/publish` opt-in are [JS HLS field](/quest/m2/js-hls.md).
- Update the draft's Timeline section (renamed for HLS) and changelog,
`doc/concept/hang.md`'s Archive bullet, and `doc/bin/hls.md` (how to opt in,
and the alignment it requires); run `just drafts check`.

Test that a catalog with the old root `archive` entry still parses with the
entry ignored, that nothing publishes `hls` unless enabled, that aligned
renditions list matching segments with a declared target, that a misaligned
segment is refused, and that a rendition without `hls` gets no playlist.

Public API: `hang::catalog::Archive` is deleted, the rendition types gain an
`hls` field in Rust and JS, and `moq-mux` gains the opt-in. Wire: the hang
catalog format.

## Required

- [Archive consumer](/quest/m1/archive/consumer.md) - the old writer and reader, which read the root `archive` section, are gone first
66 changes: 66 additions & 0 deletions quest/m1/archive/consumer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# [M] Archive consumer

## Goal

`archive::Consumer` serves one broadcast's stored key prefix as an ordinary
broadcast: every stored track, the archived catalog included, with its
original groups, frames, timescale, and priority. A host publishes it at a path of its choosing;
moq-relay never uses it. `moq export archive` records through
`archive::Producer` and `moq import archive` serves through
`archive::Consumer`. It replaces `moq_archive::Reader`.

## Plan

Decided by the maintainer on 2026-10-17 (see the
[epic](/quest/m1/archive/README.md)):

- One cached LIST of the prefix is the index: it names the tracks and maps
any group or pts to an object. Each track's `.info` supplies its timescale
and priority before any media is read. A growing archive refreshes each
track with its own cursor, a LIST of that track's key prefix StartAfter its
newest known key, since one prefix-wide cursor skips a new key under a
lexically earlier track. A delimiter LIST of the broadcast prefix finds
tracks added since. It also re-reads a track's newest object, which the
producer may rewrite in place while an untimed group is open.
- Each served track advertises no Publisher Max Age limit (TRACK_INFO's
encoded `0`), not the source's: the store's retention bounds what it holds,
and DVR pruning is the publisher dropping a group early, which TRACK_INFO
permits. Advertising the source's bound would expire every archived group
older than it relative to the newest.
- Serve through `broadcast::Dynamic` and `track::Dynamic`. Create every listed
track up front, so a SUBSCRIBE_ALL on the served broadcast sees them all.
- The archived catalog is an ordinary track, so a reader sees its newest
stored group, not a union of every rendition recorded. A rendition removed
before the archive ends is therefore not listed at the end; the replay-catalog
quest's union and `enabled: false` handling are dropped with it.
- The served broadcast takes its epoch from the caller or mints one; it never
claims the source's epoch, since it is served at another path.

Guidance:

- An absent group below the newest stored one is a gap and reports as dropped.
A group below the oldest stored key was pruned or never stored and reports
the same way; history-mode callers rely on the start answering with what
remains ([History from the retained floor](/quest/m1/archive/history-floor.md)).
- Keep a byte-bounded object LRU so neighbouring groups come from one GET.
- When [Dynamic range requests](/quest/m1/subscribe-ranges/dynamic.md) lands, the
`track::Dynamic` handler resolves a `Time(pts)` end by picking the object
from the key listing, then the group containing pts from that object's
group timestamps, whichever quest lands second.
- Move `rs/moq-hls/src/export/archive_tests.rs` onto a consumer broadcast with
its archived catalog, deleting the hand-built catalog.
- Update `doc/bin/cli.md` and `doc/bin/hls.md` inline with an export, import,
and HLS example.

Test with the in-memory store: a round trip preserving structure and payloads
byte for byte, a gap, a pruned start, a growing archive followed through
refresh (including two tracks where a new key sorts before the other track's
cursor, and a track added mid-recording), history older than the source's max
age still served, an untimed track, and an empty or unrecognized prefix
refused loudly.

Public API: `archive::Consumer` replaces `Reader`. Wire: none.

## Required

- [Archive producer](/quest/m1/archive/producer.md) - the layout this serves
43 changes: 0 additions & 43 deletions quest/m1/archive/flush.md

This file was deleted.

56 changes: 28 additions & 28 deletions quest/m1/archive/history-floor.md
Original file line number Diff line number Diff line change
@@ -1,42 +1,42 @@
# [M] History mode starts at a DVR recording's retained floor
# [S] History from the retained floor

## Goal

An HLS export in history mode lists a recording that `moq_archive::Writer`
retention has pruned, starting at its oldest retained segment, and the
Reader's timeline index stays within the retained window. Today
`moq_hls::export::Config::history` refuses it with `Error::HistoryPruned`
before listing anything, because the exporter cannot tell a pruned start from
missing history.
An HLS export in history mode lists an archive consumer broadcast whose
oldest objects were deleted, starting at its oldest stored segment. Today
`moq_hls::export::Config::history` refuses a pruned start with
`Error::HistoryPruned` before listing anything, because the exporter cannot
tell a pruned start from missing history. A gap after the floor still fails,
never listing as a complete recording.

## Plan

Decided 2026-10-11 (#5236): refuse retained recordings now, and quest the
floor. A gap after the floor must still fail, never list as a complete
recording.
Decided 2026-10-11 (#5236): refuse pruned recordings now, and quest the floor.

Prefer a subscribe range: once moq-lite-07 ranges land
([Subscribe ranges](/quest/m1/subscribe-ranges/README.md)), an `Absolute(0)..`
range answers from the oldest group that still exists and names the pruned
ones with SUBSCRIBE_DROP, so history starts at the first delivered group.
Otherwise the Reader answers a request below its retained floor with an error
distinct from a missing group.
Decided 2026-10-17: kept as its own `moq-hls` quest rather than folded into
the [Archive consumer](/quest/m1/archive/consumer.md). The consumer only
answers a group below its oldest stored key as dropped; this quest makes
history mode start there. The old Reader's timeline-map pruning and any
retention field in the catalog are gone with the reader and the root
`archive` section. The archive producer's DVR mode is what prunes the start.

The same floor lets `moq_archive::Reader` prune its timeline-group map
(`Shared::timelines`), which today grows with a long retained recording's
whole lifetime: its refresh lists only objects after its cursor, so it never
sees the writer's deletions. Drop mappings below the floor rather than
bounding the map by age or size, which could discard retained history
(#5236 review).
History mode reads the archived `hls` track's groups from group 0. Start
at the first `hls` group the broadcast delivers, using its checkpoint's
retained window, and skip records whose first media group is below the floor.
Once [Subscribe ranges](/quest/m1/subscribe-ranges/README.md) lands, an
`Absolute(0)..` range answers from the oldest group that exists and names the
rest with SUBSCRIBE_DROP; before that, the consumer's drop serves the same
purpose.

Surface retention in the catalog's archive section only if the exporter still
needs to know up front; that changes the hang format (`js/hang`,
`drafts/draft-lcurley-moq-hang.md`).
Test an archive whose oldest objects are deleted: history lists from the first
stored segment, and a hole after it still fails.

Test a retained recording whose first timeline objects are deleted: history
lists from the first retained segment, and a hole after it still fails.
## Required

- [Archive consumer](/quest/m1/archive/consumer.md) - serves the pruned archive and drops groups below its floor
- [Catalog HLS field](/quest/m1/archive/catalog-hls.md) - the `hls` records history mode reads

## Related

- [Subscribe ranges](/quest/m1/subscribe-ranges/README.md) - a range from group 0 answers from the oldest group that still exists
- [HLS failure signal](/quest/m1/hls-failure-signal.md) - the other history-mode gap left by #5236
- [DVR rewind](/quest/m2/archive-dvr.md) - seeks through the same bounded archive
30 changes: 17 additions & 13 deletions quest/m1/archive/hls-target.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
## Goal

A `moq-hls` media playlist advertises one `EXT-X-TARGETDURATION` for the
whole run, taken from the reference timeline's declared duration, so every
edge and every reload agree on it. Segments stay one-to-one with timeline
records: the edge never skips, splits, or renumbers one.
whole run, taken from the declared duration of the rendition's opt-in `hls`
field, so every edge and every reload agree on it. Segments stay one-to-one
with `hls` records: the edge never skips, splits, or renumbers one.

## Plan

Expand All @@ -17,22 +17,22 @@ and falls when that record is evicted. Codex flagged it
The 09-28 audit reversed #4280's decision 4 (the observed maximum), and the
09-29 planning settled the rest:

- The target is `ceil` of the reference timeline's declared duration.
Until that timeline's entry is declared, the rendition has no playlist yet.
- The target is `ceil` of the declared `hls` duration from
[Catalog HLS field](/quest/m1/archive/catalog-hls.md) (retargeted
2026-10-17 from the root `archive` map). Until it is declared, the rendition
has no playlist yet.
- A segment whose rounded `EXTINF` exceeds the target (a GOP that overran the
publisher's declared value) is listed anyway, with a rate-limited warning.
This deliberately deviates from RFC 8216 4.3.3.1's MUST, because refusing
the segment loses content, and splitting or skipping it at the edge breaks
the stable media sequence numbers of RFC 8216 6.2.2. Document the
deviation. hls.js tolerates it; Apple's mediastreamvalidator flags it.
- A record the publisher split mid-group at its safety ceiling is listed as
its own segment. Stop merging split halves back into one range
(`a_frame_split_record_is_not_a_video_boundary` in `spans.rs`). This is legal
because moq-hls never emits `EXT-X-INDEPENDENT-SEGMENTS`, so a segment may
start without a keyframe.
- A non-reference rendition keeps the reference record's `EXTINF`, even though
its keyframe-snapped content can run longer. The user decided this needs no
change.

Decided 2026-10-17: renditions that opt into `hls` align their segments, so
the old non-reference `EXTINF` rule (a keyframe-snapped rendition keeping the
reference record's duration) and the split-record handling
(`a_frame_split_record_is_not_a_video_boundary` in `spans.rs`) go away with
Catalog HLS field rather than here.

Replace `target_duration_follows_the_observed_segments` with tests that pin
the declared target across a window whose segment durations vary, and that
Expand All @@ -41,3 +41,7 @@ comment ("default 1s minimum"; the minimum is 2s). Hold the target per
playlist URI, not per `Rendition`: `renditions::Producer::sync` replaces the
`Rendition` on a catalog reconfigure, so test that a reconfigure mid-run
keeps the original target.

## Required

- [Catalog HLS field](/quest/m1/archive/catalog-hls.md) - the opt-in `hls` field whose declared duration sets the target
Loading