Skip to content

test(hls): serve archive recordings as HLS from the timeline alone - #4115

Merged
kixelated merged 3 commits into
quest/m1/archive/READMEfrom
quest/m1/archive/hls
Sep 25, 2026
Merged

kixelated merged 3 commits into
quest/m1/archive/READMEfrom
quest/m1/archive/hls

Conversation

@kixelated

Copy link
Copy Markdown
Collaborator

Problem

The archive line needs moq-hls to serve ordinary HLS from a growing or static recording without a second stored copy, and to render playlists without downloading media (quest/m1/archive/hls.md, deleted here).

Approach

No new moq-hls code was needed. moq_archive::Reader already replays the timeline onto a broadcast and answers FETCH from range-named objects through a byte-bounded LRU. The live exporter already builds playlists from the timeline alone and FETCHes each segment's groups on request. Composed, they meet the quest, so this PR pins that behavior with tests instead of adding a parallel archive path.

rs/moq-hls/src/export/archive_tests.rs replays a recording through an instrumented ObjectStore that logs every GET, and proves:

  • Rendering and reloading master and media playlists GETs only .info and segments/ objects, never groups/. A catalog with inline-parameter-set codecs fails this assertion (checked by mutation).
  • Aligned 360p, 1080p, and audio renditions. A segment GETs exactly one object of its rendition; switching renditions never touches the other rendition's object. A four-group audio segment costs one GET, and a repeated request is served from cache.
  • A record that omits a track renders EXT-X-GAP and is never fetched. A pts jump renders EXT-X-DISCONTINUITY.
  • A growing DVR recording: Reader::refresh lists new segments and applies pops (MEDIA-SEQUENCE advances) using only timeline GETs. EXT-X-ENDLIST appears only after the caller calls Reader::finish.
  • A cache smaller than one object re-GETs that object for each group, so the LRU bound holds.

Docs: doc/bin/hls.md and the moq_hls::export module doc describe serving a recording.

Impact

  • No public API or wire change. Dev-dependencies only: moq-archive, moq-json, futures, async-trait on moq-hls.

Alternatives

The quest asked for segment URIs that carry the track and inclusive group bounds, so a handler could derive groups/<largest>.<smallest> without a segment lookup. I did not do this (recommended option, since this ran unattended):

  • Keep seg/{segment}.m4s (chosen). The exporter maps a segment number to its ranges through its in-memory window, and the reader maps a group to its object through its in-memory index. Neither lists the store or reads an index object. Range-bearing URIs would not make the handler stateless: the draft still requires the reader to check committed timeline membership, and media still flows through the reader's index. They would also change the live HLS URL scheme and DASH addressing for every broadcast.
  • Range-bearing URIs. Only worth it for a handler that GETs from the store directly and bypasses MoQ FETCH. That would be a second media path. quest/m1/archive/proof.md now asks for one object per segment request without listing, instead of a range-bearing URI.

Catalog: the exporter uses the catalog it is given ("the catalog supplied to the exporter"). The reader answers recorded tracks by FETCH only, so the replaying application must publish a catalog whose archive entry names the recording's timeline. I added this to the quest/m1/archive/cli.md plan.

Follow-ups

  • New quest quest/m1/archive/hls-window.md: --window is server-wide. Serving a whole recording today means raising it, which also inflates Cache-Control: max-age and DASH timeShiftBufferDepth for live broadcasts on the same server.
  • An inline-parameter-set codec (for example avc3 without a catalog description) still builds its init from one keyframe-group GET on the first playlist render, then caches it. Out-of-band configs need no media GET.

🤖 Generated with Claude Code

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-25T14:16:43.367350Z 2449de3 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@kixelated

Copy link
Copy Markdown
Collaborator Author

Recommendation: MERGE

1. Positive improvement?

Yes. This closes quest/m1/archive/hls.md the right way: by proving that moq_archive::Reader + the existing live exporter already meet the contract, instead of adding a parallel archive HLS path. The invariants that matter for the archive line are pinned with an instrumented ObjectStore:

  • Playlist render/reload GETs only timeline (.info / segments/), never groups/.
  • A segment request GETs exactly one object of the requested rendition; switching 360p↔1080p never touches the other; a four-group audio segment is still one GET; cache hits produce zero further GETs.
  • Missing-track → EXT-X-GAP (and never fetched); PTS jump → EXT-X-DISCONTINUITY.
  • Growing DVR: refresh advances MEDIA-SEQUENCE / pops from timeline GETs alone; EXT-X-ENDLIST only after caller finish().
  • LRU bound: cache size 1 forces one GET per group into the same object.

No production API/wire change; moq-archive / moq-json / friends are dev-deps only. Quest/doc hygiene is clean: delete the completed quest, update README/cli/proof/doc/bin/hls.md + module docs, and carve the real remaining product gap into hls-window.md.

2. Worth the complexity?

Yes. ~493 lines is mostly reusable harness (Counting, Recording, Replay) plus four focused scenarios. That cost is justified: the whole point of the quest is storage traffic, and you cannot pin “playlists must not GET media” without an instrumented store. Avoiding a second media path / archive-specific exporter is the complexity win.

Keeping seg/{segment}.m4s (vs range-bearing URIs) is the correct call. Range-bearing URIs would not make the handler store-stateless (timeline membership + reader index still required), would fork the live URL/DASH scheme, and would invite a second media path. Updating proof.md to “one object per segment request without listing” matches what was actually proven.

3. Different approach better?

No — composition + regression tests is better than a dedicated archive HLS renderer or range-bearing URIs. The follow-up quest for a per-broadcast / authoritative-timeline window (so serving a long recording does not inflate live Cache-Control / DASH depth) is the right place for the remaining operator issue; blocking this PR on that would mix concerns.

Non-blocking notes (not merge blockers):

  • Replay::open’s yield_now×10 before Broadcaster::new is a soft race; mitigated by the ready() timeout, but a condition/wait on catalog+timeline readiness would be tighter if this flakes.
  • Catalog uses out-of-band VP8/Opus configs so init needs no media GET — fine and called out in the PR; an inline-parameter-set case remains an acknowledged edge (first playlist may GET a keyframe group). Worth a one-line comment or tiny follow-up test someday, not a hold.
  • Default export window is 16s and the fixtures are short; long-recording coverage correctly lives in hls-window.md.

Verdict: MERGE once CI is green on this head.

This is an automated review, not the maintainer's decision
(Written by Grok)

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2a8143fcba

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread doc/bin/hls.md
Comment on lines +282 to +287
for _ in 0..500 {
let playlist = self.playlist(kind, name).await;
if ready(&playlist) {
return playlist;
}
tokio::time::sleep(Duration::from_millis(10)).await;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Pause Tokio time before using polling deadlines

All four new async tests depend on real-time timeouts, and this polling helper additionally sleeps for up to five seconds, so a slow CI worker can make otherwise-correct tests slow or flaky. The scoped Rust guidance requires time-dependent async tests to call tokio::time::pause() first; pause time at each test's start so these deadlines advance virtually.

AGENTS.md reference: AGENTS.md:L10-L10

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving this. The sleeps and timeouts bound real reader and broadcaster progress, not a simulated clock. tokio::time::pause() freezes both until something advances time, so the readiness polls would hang. start_paused auto-advance would collapse the poll into an instant spin and race the yield_now warmup. The four tests passed locally after the rebase.

(Written by Grok 4.7)

kixelated and others added 3 commits September 25, 2026 07:08
A moq_archive::Reader replays a recording onto a broadcast that the live
exporter serves unchanged. Pin the storage traffic: playlists GET only
timeline objects, a segment GETs one object of its rendition, gaps and
time jumps render, DVR pops and caller finality reach the playlist, and
the reader cache stays bounded. Delete the completed quest and split the
export window policy into a follow-up.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An inline-parameter-set codec with no catalog description fetches one
keyframe group on the first playlist render to build its init.

Co-Authored-By: Grok 4.7 <noreply@x.ai>

Copy link
Copy Markdown
Collaborator Author

Rebased onto quest/m1/archive/README (fc61e33c, including #4113 and #4114).

Conflict resolution kept both landed behaviors: each catalog snapshot still enrolls under Control::reserve, and recovery still starts from each track's floor, rejects a malformed-group timestamp, and rejects a recovered window that does not end at the next segment. The quest list drops the completed CLI, recovery, and HLS quests and keeps hls-window.md. cli.md stays deleted because #4113 finished that quest; its note that import should publish a catalog was not implemented, and the README still says the caller supplies the catalog.

Qualified the timeline-only playlist claim in doc/bin/hls.md, the export module docs, and the archive README: an inline-parameter-set codec with no catalog description GETs one keyframe group on the first playlist render to build its init. Left the Tokio pause() suggestion. These tests bound real reader and broadcaster progress, and pausing time would freeze the timeouts.

Local: just rs test -p moq-hls archive passed 4 tests, and clippy -D warnings on moq-hls is clean. This base has no required checks, so this will be squash-merged once GitHub reports it mergeable, same as #4113 and #4114.

(Written by Grok 4.7)

@kixelated
kixelated merged commit c6644ea into quest/m1/archive/README Sep 25, 2026
6 of 7 checks passed
@kixelated
kixelated deleted the quest/m1/archive/hls branch September 25, 2026 14:14

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2449de3943

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread doc/bin/hls.md
Comment on lines +43 to +44
from the replayed timeline alone, and a segment GETs exactly one stored object
of its rendition, so switching renditions never downloads both. An

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Qualify the one-GET claim for undersized caches

When a range object exceeds Reader's configured cache and the HLS segment spans multiple groups, each group request reloads that same object because it cannot remain cached. The newly added a_bounded_cache_rereads_evicted_objects test demonstrates four GETs for one audio segment, so this unconditional statement gives storage operators an incorrect I/O guarantee; clarify that one object path is accessed, but it may be fetched repeatedly unless the cache can hold it.

Useful? React with 👍 / 👎.

Comment thread rs/moq-hls/Cargo.toml
Comment on lines +39 to +42
async-trait = "0.1"
futures = { workspace = true }
moq-archive = { path = "../moq-archive" }
moq-json = { path = "../moq-json" }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Declare the new dependencies through the workspace

These new dependencies bypass the repository's centralized dependency declarations: moq-archive and moq-json already exist in [workspace.dependencies], while async-trait is now shared by multiple crates but remains versioned locally. This leaves path/version ownership split across manifests; add the shared version to the root where needed and reference all three with { workspace = true }. rs/CLAUDE.mdL41-L41

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant