Skip to content

feat(hang): define segment-addressed recordings - #2574

Open
kixelated wants to merge 6 commits into
devfrom
claude/moq-archive-status-yr9pfb
Open

feat(hang): define segment-addressed recordings#2574
kixelated wants to merge 6 commits into
devfrom
claude/moq-archive-status-yr9pfb

Conversation

@kixelated

@kixelated kixelated commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Define a HANG recording format addressed by the broadcast timeline: .timeline, immutable per-track .track metadata, and one object per track and segment.
  • Make durable storage authoritative. Unbounded archive and duration-bounded DVR are separate retention modes, with object-before-index ordering and atomic timeline gaps when a durable write fails.
  • Make timeline enrollment non-pacing by default in Rust and TypeScript. Continuous media explicitly opts into pacing, while catalogs and metadata cannot accidentally stall segmentation.
  • Enroll the built-in plaintext catalog through the default non-pacing path so every recording contains the catalog groups needed to interpret its media segments.
  • Require canonical uppercase percent escapes and a recording-owned DEFLATE stream so replacing a source timeline record with a gap cannot corrupt later records.

The root causes were three mismatches in the original proposal. A recorded segment retained FRAME timestamp deltas without persisting the track timescale and ordering needed to reconstruct TRACK_INFO. Non-pacing tracks were documented as arrival-ordered but flushed using media timestamps, so future-based metadata could be left behind and could inflate the final segment duration. The low-level enrollment primitive was only exercised directly by tests, so the normal catalog producer never reported its published groups to the timeline.

This follows the durable Recording/DVR direction in #2275 and #2281, plus moq-dev/moq.pro#807. The old standalone archive format and relay-RAM-derived retention are not used.

Public API changes

  • Rust moq_mux::timeline::Producer::track is now infallible and enrolls a non-pacing track.
  • Rust adds moq_mux::timeline::Producer::pacing_track for continuous media that votes on boundaries and gates completeness.
  • TypeScript @moq/hang timeline Producer.track is now non-pacing.
  • TypeScript adds Producer.pacingTrack for continuous media.

This is a semantic break to the existing low-level timeline API, so the existing dev target is appropriate. High-level catalog and media producers select the correct mode internally.

Test plan

  • nix develop --command just fix
  • nix develop --command just drafts check
  • nix develop --command just check
  • nix develop --command just test
  • Focused Rust and TypeScript timeline suites

The Rust/TypeScript timeline implementations and regression tests are kept in sync. No other cross-package sync row applies.

(Written by GPT-5)

claude added 2 commits July 31, 2026 00:24
moq-archive specified a generic, arrival-ordered chunk format at the
moq-lite layer, with its own per-track index mapping group runs to byte
ranges. It predated the timeline rework, which already publishes a
per-segment index of group ranges per track.

Addressing a recording by segment instead of by flush interval collapses
the format: the segment number names the object, so the timeline is the
only index and the separate index log, chunk numbering, byte offsets,
partial spans, and overlap resolution all disappear. It also gives a
reader one whole-object GET per segment per track, which byte-range
splicing across chunks could not.

That makes the format a hang concept rather than a moq-lite one, since
segments are defined by the timeline and the timeline is hang. Specify it
in the hang draft and delete the standalone draft, which was never
submitted to the datatracker.

Segment objects carry their own group boundaries so they parse without
the timeline, and frames stay byte-identical to moq-lite FRAME so a
recorded group is a FETCH response body unchanged.

Known trade: a group arriving after its segment object is written is not
recorded. Writers wait for completeness and bound that wait.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013Cxx2ebpiGmWaD8AvJkRdi
The segmenter required every enrolled track to vote on boundaries and to
report past a segment's end before its record could flush. That works for
audio and video, whose groups arrive continuously, and stalls the timeline
permanently for anything else: boundary() returns None the moment an
enrolled track has no group past the threshold, and a catalog publishes a
group only when the renditions change.

So a broadcast could only segment tracks it could also pace, which left
catalogs, metadata, and application tracks like a chat log with no way to
be indexed at all.

Add Producer::passive alongside Producer::track. A passive track's groups
are recorded into whichever segment is open when they arrive, but it never
votes on a boundary, never gates completeness, and never anchors the first
segment (a catalog published while the encoder warms up would otherwise
stretch segment 0 across the whole startup gap). It also does not create
the timeline track, so segmentation stays opt-in by pacing track.

Placement is by arrival rather than content time, since nothing waits for
these groups: one that shows up after its segment flushed is recorded in
the next. The frames still carry their own timestamps.

A passive track whose group never closes is recorded once, in the segment
its group opened in. Rolling the group at segment boundaries is the fix and
is left to the publisher.

Recording drops its special case for the catalog as a result: every track
the timeline lists is stored the same way.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013Cxx2ebpiGmWaD8AvJkRdi
@kixelated
kixelated marked this pull request as ready for review August 3, 2026 17:55

@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: 2ed2fe6e74

ℹ️ 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 on lines +528 to +531
~~~
<prefix>/.timeline
<prefix>/<track>/<segment>
~~~

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Persist each track's immutable metadata

Persisting only .timeline and per-track segment objects makes a recording impossible to decode or replay after the live broadcast disappears. The stored FRAME timestamps are deltas in the track's negotiated timescale, and moq-lite requires TRACK_INFO before parsing them (drafts/draft-lcurley-moq-lite.md:1180-1182 and 1050-1054), but this layout stores neither the timescale nor the other immutable properties needed to reconstruct TRACK_INFO. Add per-track metadata to the recording layout so readers can interpret frames and serve the recorded tracks. (Written by GPT-5.6 Sol)

Useful? React with 👍 / 👎.

Comment thread rs/moq-mux/src/timeline.rs Outdated
Comment on lines +525 to +526
pub fn passive(&self, name: &str) -> crate::Result<Recorder> {
self.enroll(name, true)

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 Assign passive groups by arrival instead of timestamp

This only marks the track passive for boundary and completeness calculations. flush_segment still stops draining it when group_pts >= end, and the terminal duration still takes the maximum frontier across all tracks; the JS implementation mirrors both behaviors. When passive metadata uses a future or different timestamp basis, such as catalog snapshots stamped with Timestamp::now() alongside file media starting at PTS 0, an update that arrived during an open segment remains pending until the final flush and can inflate or fail the final segment. Drain all pending passive groups at each flush and exclude passive frontiers from terminal duration. (Written by GPT-5.6 Sol)

Useful? React with 👍 / 👎.

Comment thread drafts/draft-lcurley-moq-hang.md Outdated
Comment on lines +601 to +602
Segment objects are immutable and SHOULD be served with long-lived caching.
`.timeline` and `.catalog` change while the recording is live and SHOULD be served with short lifetimes; all three become immutable once the recording has ended.

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 Remove the nonexistent .catalog object

The layout defines only .timeline and immutable <track>/<segment> objects, with catalog generations stored as ordinary segment objects under the encoded catalog track name. Consequently there is no .catalog object that changes during a live recording, and catalog segment objects should receive the long-lived caching described immediately above. This instruction can make implementations request or publish an undefined resource and apply the wrong cache policy. (Written by GPT-5.6 Sol)

Useful? React with 👍 / 👎.

kixelated and others added 2 commits August 16, 2026 15:22
Co-Authored-By: OpenAI Codex <noreply@openai.com>
Persist immutable track metadata alongside per-track segment objects, and specify archive and duration-bounded DVR retention with the recording timeline as the authoritative durable index.

Fix passive timeline tracks to drain by arrival and exclude their timestamps from final media duration. Add matching Rust and TypeScript regression coverage.

Co-Authored-By: OpenAI Codex <noreply@openai.com>
@kixelated kixelated changed the title drafts: fold the archive format into hang as a Recording section feat(hang): define segment-addressed recordings Aug 16, 2026

@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: c8dec4b58d

ℹ️ 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 rs/moq-mux/src/timeline.rs Outdated
Comment on lines +529 to +530
pub fn passive(&self, name: &str) -> crate::Result<Recorder> {
self.enroll(name, true)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Enroll the built-in catalog as a passive track

The new recording layout relies on catalog groups appearing in timeline records, but the production catalog::Producer never calls this method or reports its snapshot groups through the returned recorder. A repo-wide search at this commit finds passive() used only in timeline tests, while rs/moq-mux/src/catalog/producer.rs continues to emit catalog.json directly. Consequently, recordings produced through the normal Rust catalog/media path have no catalog segment objects and cannot recover the rendition and decoder configuration after the live catalog disappears. Wire the built-in catalog publisher to a passive recorder rather than only exposing the primitive. (Written by GPT-5.6 Sol)

Useful? React with 👍 / 👎.

Comment on lines +655 to +656
If any object for a segment cannot be made durable, the writer MUST record an atomic gap for that segment rather than publish a partial set of tracks.
The gap is the same timeline record with `tracks` absent or empty, so it preserves the segment number and timing but references no objects.

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 Re-encode the timeline after writing a gap

When an object write fails in an unbounded archive, this rule requires replacing the source record with a modified gap record, contradicting the earlier requirement that .timeline contain the source frames verbatim. More importantly, later source frames were DEFLATE-compressed against the original record through the shared window, so appending them after a rewritten gap makes readers decompress against a different dictionary and corrupts the remaining timeline. Specify that the writer must re-encode the gap and every subsequent record, or encode the failure out of band. (Written by GPT-5.6 Sol)

Useful? React with 👍 / 👎.

Comment thread drafts/draft-lcurley-moq-hang.md Outdated
Comment on lines +564 to +565
`<track>` is the track's name with every byte outside `A-Z a-z 0-9 _ -` percent-encoded.
An encoded name therefore never contains `/` and never begins with `.`, so a track can neither collide with the reserved `.timeline` name nor address anything outside the prefix.

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 Canonicalize encoded track directory names

This does not specify the hexadecimal letter case for percent encoding, so two compliant implementations can derive different object keys for the same track. For example, the built-in catalog.json name can become either catalog%2Ejson or catalog%2ejson, which are distinct on case-sensitive filesystems and object stores; an independently implemented reader can therefore return 404 for every object written by another implementation. Require one canonical hex case for encoded bytes. (Written by GPT-5.6 Sol)

Useful? React with 👍 / 👎.

kixelated and others added 2 commits August 16, 2026 16:23
Enroll catalog.json as a passive timeline track and report each published snapshot group so durable recordings include the catalog needed to interpret media segments.

Specify uppercase percent escapes and require recording-owned timeline re-encoding so atomic gaps do not corrupt the shared DEFLATE dictionary.

Co-Authored-By: OpenAI Codex <noreply@openai.com>
Make ordinary timeline track enrollment non-pacing so sparse catalog or metadata tracks cannot accidentally stall segment publication. Add explicit pacing_track and pacingTrack methods for continuous media that votes on boundaries and gates completeness.

BREAKING CHANGE: Rust timeline Producer::track is now infallible and non-pacing, and TypeScript Producer.track is now non-pacing. Use pacing_track or pacingTrack for media tracks that drive segmentation.

Co-Authored-By: OpenAI Codex <noreply@openai.com>

@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: ddaf604060

ℹ️ 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".

A writer SHOULD wait for the segment to be complete rather than write early, and MAY bound that wait so a track that has stopped without closing cannot stall the recording indefinitely.
This is a deliberate trade: addressing content by segment is what makes a segment retrievable in one request, and it costs the ability to append a late group to an object already written.

A writer SHOULD record the broadcast's final state on a clean end, so a reader can distinguish an ended recording from one whose writer died.

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 Define a durable clean-end marker

Define an on-disk representation for this final state. After either a clean shutdown or a writer crash, the recording currently contains the same last sync-flushed timeline record and immutable segment objects; merely observing that .timeline has stopped changing cannot distinguish those cases. Without a terminal marker or status object, readers cannot implement the stated distinction or know when the archive is safe to cache as immutable. (Written by GPT-5.6 Sol)

AGENTS.md reference: AGENTS.md:L96-L100

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.

2 participants