From 63bcb4f5d6ab6c0f15cf9afddf7ca95c9a20733e Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sun, 21 Jun 2026 20:28:29 -0700 Subject: [PATCH 1/2] moq-hang: add optional Timeline track for time->group seeking Add an optional per-media `timeline` track that maps each moq-lite Group to the presentation timestamp of its first frame, so a subscriber can seek by time (resolve a timestamp to a Group sequence) without downloading and parsing the start of every Group. It is the hang-flavored analog of MSF's media timeline track, but binary and append-only rather than a JSON array. Design: - One frame per entry; the timeline is a single Group that grows over the broadcast, so a late subscriber reading from frame 0 gets the full index. - Each frame payload is a single varint: the Group delta from the previous entry (normally 1; larger values allow a sparse index). - The presentation timestamp rides moq-lite's existing FRAME Timestamp Delta (zigzag signed, first-of-group absolute), in the track's Publisher Timescale -- application-defined, and SHOULD match the media track being indexed. - Referenced from VideoSchema/AudioSchema via a new optional `timeline` field naming the track. No moq-lite changes are required. Validated with `make draft-lcurley-moq-hang.txt`. Co-Authored-By: Claude Opus 4.8 --- draft-lcurley-moq-hang.md | 59 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/draft-lcurley-moq-hang.md b/draft-lcurley-moq-hang.md index d8bbfdf..feebee2 100644 --- a/draft-lcurley-moq-hang.md +++ b/draft-lcurley-moq-hang.md @@ -50,6 +50,7 @@ Hang introduces additional terminology: - **Participant**: A moq-lite broadcaster that may produce any number of media tracks. - **Catalog**: A JSON document that describes each available media track, supporting live updates. - **Container**: A tiny header in front of each media payload containing the timestamp. +- **Timeline**: An optional index mapping each media Group to its presentation timestamp, used for seeking. # Discovery @@ -123,6 +124,7 @@ A video track contains the necessary information to decode a video stream. ~~~ type VideoSchema = { "renditions": Map, + "timeline": TrackName | undefined, "priority": u8, "display": { "width": number, @@ -137,6 +139,8 @@ The `renditions` field contains a map of track names to video decoder configurat See the [WebCodecs specification](https://www.w3.org/TR/webcodecs/#video-decoder-config) for specifics and registered codecs. Any Uint8Array fields are hex-encoded as a string. +The optional `timeline` field names a [Timeline](#timeline) track that indexes this media for seeking. + For example: ~~~ @@ -157,6 +161,7 @@ For example: "framerate": 30.0 } }, + "timeline": "video/timeline", "priority": 2, "display": { "width": 1280, @@ -174,6 +179,7 @@ An audio track contains the necessary information to decode an audio stream. ~~~ type AudioSchema = { "renditions": Map, + "timeline": TrackName | undefined, "priority": u8, } ~~~ @@ -182,6 +188,8 @@ The `renditions` field contains a map of track names to audio decoder configurat See the [WebCodecs specification](https://www.w3.org/TR/webcodecs/#audio-decoder-config) for specifics and registered codecs. Any Uint8Array fields are hex-encoded as a string. +The optional `timeline` field names a [Timeline](#timeline) track that indexes this media for seeking. + For example: ~~~ @@ -200,6 +208,7 @@ For example: "bitrate": 64000 } }, + "timeline": "audio/timeline", "priority": 1, } ~~~ @@ -217,6 +226,56 @@ The remainder of the payload is codec specific; see the WebCodecs specification For example, h.264 with no `description` field would be annex.b encoded, while h.264 with a `description` field would be AVCC encoded. +# Timeline +The timeline is an optional track that indexes a media track's Groups by presentation timestamp. +It lets a subscriber seek — resolving a presentation timestamp to a Group sequence — without first downloading and parsing the start of every Group. +It carries no media payload and is typically given a lower priority than the media tracks it indexes. + +A media schema references its timeline track by name via the `timeline` field (see [Catalog](#catalog)). +A timeline reflects the Group and timestamp structure of one media track. +The renditions of a single media already share that structure — matching Group sequence numbers and per-group timestamps so a subscriber can switch between them — so one timeline serves all of a media's renditions. +Audio and video do not share that structure and use separate timelines. + +The timeline track is a single Group that grows over the lifetime of the broadcast. +Each frame appends one entry, in Group order, mapping a Group to the presentation timestamp of that Group's first frame. +A subscriber builds the full index by reading the Group from its first frame; because moq-lite always retains the latest Group, a subscriber that joins late still receives every prior entry. +A publisher SHOULD use a single Group, but MAY start a new Group to re-anchor the index for an exceptionally long broadcast. + +To seek to a presentation timestamp, a subscriber selects the entry with the greatest timestamp not exceeding the target and subscribes to (or fetches) that Group, which begins with a keyframe (see [Container](#container)). + +## Encoding +The timeline reuses the moq-lite `FRAME` to carry the presentation timestamp, leaving a single value in the payload: + +~~~ +Timeline Frame { + Group Delta (i), +} +~~~ + +The presentation timestamp is expressed in the track's `Publisher Timescale` ([moql]). +This is application-defined and SHOULD match the timescale of the media track being indexed so the values are directly comparable when seeking. + +**Group Delta**: +The increase in Group sequence from the previous entry, encoded as an unsigned variable-length integer. +The first entry in the Group encodes the absolute Group sequence (a delta from 0). +This is normally `1`, since a publisher assigns Group sequences sequentially, but a larger value lets a timeline index Groups sparsely (for example, only every Nth Group). + +**Presentation Timestamp**: +The presentation timestamp of the Group's first frame is not stored in the payload; it is carried by the moq-lite frame's `Timestamp Delta` field ([moql]), a zigzag-encoded signed delta from the previous entry in the track's timescale. +The first entry in the Group therefore carries the absolute timestamp, following the moq-lite frame rules. +A signed delta also accommodates the occasional backward step caused by frame reordering (e.g. an open GOP). + +For example, a video track using a microsecond timescale with a constant two-second GOP starting at Group 100 produces these entries (the timestamp is carried by the moq-lite frame, the group delta by the payload): + +~~~ +entry 0: timestamp=0 group=100 # absolute anchor +entry 1: timestamp=+2000000 group=+1 +entry 2: timestamp=+2000000 group=+1 +~~~ + +Each entry occupies only a few bytes, and a regular cadence repeats identical deltas, so the index stays small even for long broadcasts. + + # Security Considerations TODO Security From 1514c7d1841a52a6642d526c0d57aab788251394 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sun, 21 Jun 2026 20:38:58 -0700 Subject: [PATCH 2/2] moq-hang: address review on Timeline track - Clarify the glossary: a timeline maps each Group to the presentation timestamp of its *first frame* (matching the encoding section). - Recommend against re-anchoring the index in a new Group. moq-lite no longer guarantees retention of non-latest Groups (Publisher Cache was removed), so splitting the index across Groups risks earlier entries being dropped before a late subscriber can read them; keep the whole timeline in the single growing Group. Co-Authored-By: Claude Opus 4.8 --- draft-lcurley-moq-hang.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/draft-lcurley-moq-hang.md b/draft-lcurley-moq-hang.md index feebee2..9c63573 100644 --- a/draft-lcurley-moq-hang.md +++ b/draft-lcurley-moq-hang.md @@ -50,7 +50,7 @@ Hang introduces additional terminology: - **Participant**: A moq-lite broadcaster that may produce any number of media tracks. - **Catalog**: A JSON document that describes each available media track, supporting live updates. - **Container**: A tiny header in front of each media payload containing the timestamp. -- **Timeline**: An optional index mapping each media Group to its presentation timestamp, used for seeking. +- **Timeline**: An optional index mapping each media Group to the presentation timestamp of its first frame, used for seeking. # Discovery @@ -239,7 +239,7 @@ Audio and video do not share that structure and use separate timelines. The timeline track is a single Group that grows over the lifetime of the broadcast. Each frame appends one entry, in Group order, mapping a Group to the presentation timestamp of that Group's first frame. A subscriber builds the full index by reading the Group from its first frame; because moq-lite always retains the latest Group, a subscriber that joins late still receives every prior entry. -A publisher SHOULD use a single Group, but MAY start a new Group to re-anchor the index for an exceptionally long broadcast. +A publisher SHOULD keep the whole timeline in this single Group rather than starting a new one: moq-lite guarantees retention only of the latest Group, so splitting the index across Groups risks the earlier entries being dropped before a late subscriber can read them. To seek to a presentation timestamp, a subscriber selects the entry with the greatest timestamp not exceeding the target and subscribes to (or fetches) that Group, which begins with a keyframe (see [Container](#container)).