moq L. Curley Internet-Draft 24 September 2026 Intended status: Informational Expires: 28 March 2027 Media over QUIC - Hang draft-lcurley-moq-hang-03 Abstract Hang is a real-time conferencing protocol built on top of moq-lite. A room consists of multiple participants who publish media tracks. All updates are live, such as a change in participants or media tracks. Note to Readers This document was generated by an AI model from the implementation at github.com/moq-dev/moq (https://github.com/moq-dev/moq) and is maintained alongside it. Submit an issue (https://github.com/moq- dev/moq/issues) or PR (https://github.com/moq-dev/moq/pulls) if this spec sucks and you want to fix anything. Status of This Memo This Internet-Draft is submitted in full conformance with the provisions of BCP 78 and BCP 79. Internet-Drafts are working documents of the Internet Engineering Task Force (IETF). Note that other groups may also distribute working documents as Internet-Drafts. The list of current Internet- Drafts is at https://datatracker.ietf.org/drafts/current/. Internet-Drafts are draft documents valid for a maximum of six months and may be updated, replaced, or obsoleted by other documents at any time. It is inappropriate to use Internet-Drafts as reference material or to cite them other than as "work in progress." This Internet-Draft will expire on 28 March 2027. Copyright Notice Copyright (c) 2026 IETF Trust and the persons identified as the document authors. All rights reserved. Curley Expires 28 March 2027 [Page 1] Internet-Draft hang September 2026 This document is subject to BCP 78 and the IETF Trust's Legal Provisions Relating to IETF Documents (https://trustee.ietf.org/ license-info) in effect on the date of publication of this document. Please review these documents carefully, as they describe your rights and restrictions with respect to this document. Code Components extracted from this document must include Revised BSD License text as described in Section 4.e of the Trust Legal Provisions and are provided without warranty as described in the Revised BSD License. Table of Contents 1. Conventions and Definitions . . . . . . . . . . . . . . . . . 3 2. Terminology . . . . . . . . . . . . . . . . . . . . . . . . . 3 3. Discovery . . . . . . . . . . . . . . . . . . . . . . . . . . 4 4. Catalog . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 4.1. Root . . . . . . . . . . . . . . . . . . . . . . . . . . 5 4.2. Clock . . . . . . . . . . . . . . . . . . . . . . . . . . 6 4.3. Video . . . . . . . . . . . . . . . . . . . . . . . . . . 6 4.4. Audio . . . . . . . . . . . . . . . . . . . . . . . . . . 8 4.4.1. PCM . . . . . . . . . . . . . . . . . . . . . . . . . 9 4.5. Text . . . . . . . . . . . . . . . . . . . . . . . . . . 9 4.6. Data Tracks . . . . . . . . . . . . . . . . . . . . . . . 11 4.6.1. JSON . . . . . . . . . . . . . . . . . . . . . . . . 12 4.6.2. Binary . . . . . . . . . . . . . . . . . . . . . . . 12 4.6.3. mode . . . . . . . . . . . . . . . . . . . . . . . . 12 4.6.4. compression . . . . . . . . . . . . . . . . . . . . . 14 4.6.5. broadcast . . . . . . . . . . . . . . . . . . . . . . 14 4.7. Binary Fields . . . . . . . . . . . . . . . . . . . . . . 14 4.8. Common Rendition Fields . . . . . . . . . . . . . . . . . 14 4.8.1. broadcast . . . . . . . . . . . . . . . . . . . . . . 15 4.8.2. label . . . . . . . . . . . . . . . . . . . . . . . . 15 4.8.3. container . . . . . . . . . . . . . . . . . . . . . . 15 4.8.4. jitter . . . . . . . . . . . . . . . . . . . . . . . 16 5. Container . . . . . . . . . . . . . . . . . . . . . . . . . . 16 5.1. legacy . . . . . . . . . . . . . . . . . . . . . . . . . 17 5.2. cmaf . . . . . . . . . . . . . . . . . . . . . . . . . . 18 5.3. loc . . . . . . . . . . . . . . . . . . . . . . . . . . . 18 6. Compression . . . . . . . . . . . . . . . . . . . . . . . . . 18 7. Timeline . . . . . . . . . . . . . . . . . . . . . . . . . . 19 7.1. Catalog Section . . . . . . . . . . . . . . . . . . . . . 19 7.2. Track Framing . . . . . . . . . . . . . . . . . . . . . . 21 7.3. Records . . . . . . . . . . . . . . . . . . . . . . . . . 21 7.4. Segmentation . . . . . . . . . . . . . . . . . . . . . . 23 8. MPEG-TS Service Information . . . . . . . . . . . . . . . . . 24 8.1. Catalog Section . . . . . . . . . . . . . . . . . . . . . 24 8.2. Track Format . . . . . . . . . . . . . . . . . . . . . . 25 9. Recording . . . . . . . . . . . . . . . . . . . . . . . . . . 26 9.1. Layout . . . . . . . . . . . . . . . . . . . . . . . . . 27 Curley Expires 28 March 2027 [Page 2] Internet-Draft hang September 2026 9.2. Track Objects . . . . . . . . . . . . . . . . . . . . . . 28 9.3. Segment Objects . . . . . . . . . . . . . . . . . . . . . 28 9.4. Writer Behavior . . . . . . . . . . . . . . . . . . . . . 30 9.5. Retention . . . . . . . . . . . . . . . . . . . . . . . . 31 9.6. Bootstrap and Recovery . . . . . . . . . . . . . . . . . 32 9.7. Reader Behavior . . . . . . . . . . . . . . . . . . . . . 32 10. Rooms . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33 11. Security Considerations . . . . . . . . . . . . . . . . . . . 34 12. IANA Considerations . . . . . . . . . . . . . . . . . . . . . 34 13. References . . . . . . . . . . . . . . . . . . . . . . . . . 34 13.1. Normative References . . . . . . . . . . . . . . . . . . 34 13.2. Informative References . . . . . . . . . . . . . . . . . 35 Appendix A: Changelog . . . . . . . . . . . . . . . . . . . . . . 36 moq-hang-03 . . . . . . . . . . . . . . . . . . . . . . . . . . 36 Acknowledgments . . . . . . . . . . . . . . . . . . . . . . . . . 37 Author's Address . . . . . . . . . . . . . . . . . . . . . . . . 37 1. Conventions and Definitions The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all capitals, as shown here. 2. Terminology Hang is built on top of moq-lite [moql] and uses much of the same terminology. A quick recap: * *Broadcast*: A collection of Tracks from a single publisher. * *Track*: A series of Groups, each of which can be delivered and decoded _out-of-order_. * *Group*: A series of Frames, each of which must be delivered and decoded _in-order_. * *Frame*: A sized payload of bytes representing a single moment in time. Hang introduces additional terminology: * *Room*: A collection of participants, publishing under a common prefix. * *Participant*: A moq-lite broadcaster that may produce any number of media tracks. Curley Expires 28 March 2027 [Page 3] Internet-Draft hang September 2026 * *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. 3. Discovery The first requirement for a real-time conferencing application is to discover other participants in the same room. Hang does this using moq-lite's ANNOUNCE capabilities. A room consists of a path. Any participants within the room MUST publish a broadcast with the room path as a prefix which SHOULD end with the .hang suffix. For example: /room123/alice.hang /room123/bob.hang /room456/zoe.hang A participant issues an ANNOUNCE_PLEASE message to discover any other participants in the same room. The server (relay) will then respond with an ANNOUNCE message for any matching broadcasts, including their own. For example: ANNOUNCE_PLEASE prefix=/room/ ANNOUNCE suffix=alice.hang active=true ANNOUNCE suffix=bob.hang active=true If a participant leaves or is disconnected, their broadcast is unannounced. Publishers and subscribers SHOULD terminate any subscriptions once a participant is unannounced. ANNOUNCE suffix=alice.hang active=false 4. Catalog The catalog describes the available media tracks for a single participant. It's a JSON document that extends the W3C WebCodecs specification [webcodecs]. Curley Expires 28 March 2027 [Page 4] Internet-Draft hang September 2026 The catalog is published as a catalog.json track within the broadcast so it can be updated live as the participant's media tracks change. A participant MAY forgo publishing a catalog if it does not wish to publish any media tracks now and in the future. The catalog track consists of multiple groups, one for each update. Each group contains a single frame with UTF-8 JSON. A publisher MUST NOT write multiple frames to a group until a future specification includes a delta-encoding mechanism (via JSON Patch most likely). A publisher SHOULD also serve the catalog as a catalog.json.z track: the identical JSON under the same group and frame rules, differing only by compression (Section 6). A consumer reads whichever of the two tracks it prefers. 4.1. Root The root of the catalog is a JSON document with the following schema: type Catalog = { "audio": AudioSchema | undefined, "video": VideoSchema | undefined, "archive": ArchiveSchema | undefined, "clock": ClockSchema | undefined, "text": TextSchema | undefined, "json": JsonTracks | undefined, "binary": BinaryTracks | undefined, // ... any custom fields ... } Additional fields MAY be added based on the application. The catalog SHOULD be mostly static, delegating any dynamic content to other tracks. For example, a chat entry should name a chat track, not carry individual chat messages. This way catalog updates are rare and a client MAY choose to not subscribe. The json and binary sections (Section 4.6) are how a track like that is listed. This specification defines audio, video, and text media tracks, plus an optional archive entry (Section 7.1) naming the timeline track (Section 7) that indexes their segments, an optional root clock entry (Section 4.2) mapping content time to wall time, and the application data tracks in Section 4.6. Curley Expires 28 March 2027 [Page 5] Internet-Draft hang September 2026 4.2. Clock The catalog's root clock field is the broadcast's one continuous clock: type ClockSchema = { "wall": number, "timescale": number | undefined, } The wall field is the wall-clock time of PTS zero, in timescale units since the moq epoch, 2020-01-01T00:00:00Z. A consumer derives the wall-clock time of any media timestamp as wall + pts after converting that timestamp into this timescale, and Unix time by adding the epoch back (for HLS EXT-X-PROGRAM-DATE-TIME or DASH availabilityStartTime). Every media track and the archive index refer to this one mapping after timescale conversion; there are no competing wall epochs. The timescale field is the units per second for wall, defaulting to 1000000 (microseconds) when absent. A zero timescale is invalid, as is a wall outside the JSON-safe integer range; both are refused rather than truncated. The mapping is fixed for the broadcast and independent of archive: a live-only publisher exposes its clock without creating a segment index. A discontinuity marker is a delivery event, not a new epoch, and a system-clock adjustment never retimes the mapping. 4.3. Video A video track contains the necessary information to decode a video stream. type VideoSchema = { "renditions": Map, "display": { "width": number, "height": number, } | undefined, "rotation": number | undefined, "flip": boolean | undefined, } The renditions field contains a map of track names to video decoder configurations. See the WebCodecs specification (https://www.w3.org/TR/webcodecs/#video-decoder-config) for specifics and registered codecs. Any field carrying raw bytes, notably description, is a hex string (Section 4.7). Curley Expires 28 March 2027 [Page 6] Internet-Draft hang September 2026 The display field is the size to render the video at, in pixels. It is separate from a rendition's displayAspectWidth/displayAspectHeight because changing it does not require reinitializing the decoder. In addition to the WebCodecs fields, each rendition MAY carry the common rendition fields (Section 4.8) plus: type VideoDecoderConfigExtensions = { "displayAspectWidth": number | undefined, "displayAspectHeight": number | undefined, "stalled": boolean | undefined, } displayAspectWidth and displayAspectHeight give the display aspect ratio of the media, stretching or shrinking the coded pixels. A consumer that understands neither field MUST assume square pixels, a 1:1 ratio. Both MUST be present together; a consumer that sees only one MUST ignore it. stalled indicates that the publisher recommends temporarily avoiding the rendition. The track remains available when stalled is true. A consumer SHOULD select an unstalled rendition when it supports one, but MAY select a stalled rendition when no unstalled rendition is suitable. If absent, stalled defaults to false. For example: Curley Expires 28 March 2027 [Page 7] Internet-Draft hang September 2026 { "renditions": { "720p": { "label": "HD", "codec": "avc1.64001f", "container": { "kind": "legacy" }, "codedWidth": 1280, "codedHeight": 720, "bitrate": 6000000, "stalled": true, "framerate": 30.0, "jitter": 34 }, "480p": { "codec": "avc1.64001e", "container": { "kind": "legacy" }, "codedWidth": 848, "codedHeight": 480, "bitrate": 2000000, "framerate": 30.0, "jitter": 34 } }, "display": { "width": 1280, "height": 720 }, "rotation": 0, "flip": false, } 4.4. Audio An audio track contains the necessary information to decode an audio stream. type AudioSchema = { "renditions": Map, } The renditions field contains a map of track names to audio decoder configurations. See the WebCodecs specification (https://www.w3.org/TR/webcodecs/#audio-decoder-config) for specifics and registered codecs. Any field carrying raw bytes, notably description, is a hex string (Section 4.7). In addition to the WebCodecs fields, each rendition MAY carry the common rendition fields (Section 4.8). Curley Expires 28 March 2027 [Page 8] Internet-Draft hang September 2026 4.4.1. PCM Hang defines the "pcm" audio codec for uncompressed samples. The sampleRate and numberOfChannels fields MUST be present and greater than zero. The description field MUST NOT be present. If bitrate is present, it MUST equal sampleRate * numberOfChannels * 32. Each codec payload consists of interleaved IEEE 754 binary32 samples in little-endian byte order. Samples are ordered by sample frame, then by ascending channel index within each frame. The payload length MUST be a non-zero multiple of 4 * numberOfChannels. The frame timestamp identifies the presentation time of its first sample. The frame duration in seconds is the payload length divided by 4 * numberOfChannels * sampleRate. For example: { "renditions": { "stereo": { "label": "English stereo", "codec": "opus", "container": { "kind": "legacy" }, "sampleRate": 48000, "numberOfChannels": 2, "bitrate": 128000, "jitter": 20 }, "mono": { "codec": "opus", "container": { "kind": "legacy" }, "sampleRate": 48000, "numberOfChannels": 1, "bitrate": 64000, "jitter": 20 } }, } 4.5. Text A text track carries timed text: captions or subtitles. Unlike audio and video there is no WebCodecs decoder for text, so a consumer parses each cue directly and renders it as an overlay. Curley Expires 28 March 2027 [Page 9] Internet-Draft hang September 2026 type TextSchema = { "renditions": Map, } type TextConfig = { "format": "vtt" | "ttml" | "utf8" | string, "role": "subtitle" | "caption" | string | undefined, "lang": string | undefined, // plus the common rendition fields } The renditions field maps track names to text configurations, typically one per language. The format field selects the cue serialization, and tells a consumer how to parse each frame's payload: * vtt: WebVTT (W3C WebVTT (https://www.w3.org/TR/webvtt1/)). Each payload is a self-contained WEBVTT segment whose cues carry absolute timing. A cue's embedded start time MUST match its enclosing frame timestamp. * ttml: TTML / IMSC1 (W3C IMSC (https://www.w3.org/TR/ttml- imsc1.1/)) fragment (XML) with absolute timing. A cue's embedded start time MUST match its enclosing frame timestamp. * utf8: raw UTF-8 text with no embedded timing or styling. The cue is shown from its frame timestamp until the next cue; an empty payload clears it. A consumer MUST ignore a rendition whose format it does not recognize. The role field describes the accessibility intent, defaulting to subtitle: * subtitle: a transcription of the spoken dialogue, same-language or translated. * caption: a textual representation of all audio, including non- speech sounds, for viewers who cannot hear it. The vocabulary is expected to grow, so a consumer MUST NOT reject a rendition whose role it does not recognize. It SHOULD keep such a rendition selectable and treat the role as subtitle, and MUST preserve the value verbatim if it republishes the catalog. Unlike format, an unrecognized role never prevents rendering: it describes intent, not the wire. Curley Expires 28 March 2027 [Page 10] Internet-Draft hang September 2026 The lang field is the BCP-47 [RFC5646] language tag of the track, for example en or es-419. Two renditions sharing a lang are told apart by label (Section 4.8.2), one of the common rendition fields (Section 4.8). Regardless of format, each frame's timestamp (Section 5) is the authoritative cue start time on the media clock, so a relay and a consumer can order and schedule cues without parsing the payload. A text track has no delta frames: every frame is a self-contained cue, so a group MAY consist of multiple frames, following the same rule as a codec that lacks delta frames (Section 5). For example: { "renditions": { "captions.en": { "format": "vtt", "container": { "kind": "legacy" }, "role": "caption", "lang": "en", "label": "English" }, "subtitles.es": { "format": "vtt", "container": { "kind": "legacy" }, "role": "subtitle", "lang": "es" } } } 4.6. Data Tracks Beyond media, a broadcast MAY publish application data tracks: a chat log, a telemetry feed, a thumbnail, a serialized state blob. The catalog lists them in two sections, split by payload encoding: type JsonTracks = { "tracks": Map, } type BinaryTracks = { "tracks": Map, } Curley Expires 28 March 2027 [Page 11] Internet-Draft hang September 2026 A json track's frames are UTF-8 JSON values; a binary track's frames are opaque to everything but the application. The split is by what a generic consumer can do without knowing the application: parse, inspect, and re-serialize a JSON track, but only copy a binary one. Each map is keyed by track name, so an entry's key is the name to subscribe to. Unlike the media sections these are not rendition sets: entries are distinct tracks, not alternatives to choose between. A publisher MUST omit a section that holds no tracks. A consumer discovers these tracks the same way it discovers media, by reading the catalog; nothing else in the broadcast announces them. 4.6.1. JSON type JsonSchema = { "mode": Mode, "compression": Compression | undefined, "schema": string | undefined, "broadcast": string | undefined, } The schema field is an optional identifier for the shape of each value, typically the URL of a JSON Schema. It is descriptive: a consumer that does not recognize it MUST still be able to read the track. 4.6.2. Binary type BinarySchema = { "mode": Mode, "compression": Compression | undefined, "mime": string | undefined, "broadcast": string | undefined, } The mime field is an optional media type ([RFC6838]) for each payload, for example image/jpeg. It is descriptive, the same as schema above. 4.6.3. mode type Mode = "snapshot" | "stream" Curley Expires 28 March 2027 [Page 12] Internet-Draft hang September 2026 The mode field says how a track's groups compose its frames into what a consumer sees. It is REQUIRED and has no default: reading an append log as a latest-value document silently discards every payload but the last. A consumer MUST ignore a track whose mode it does not recognize. A snapshot track is lossy. Each group is self-contained and supersedes the previous one, so a consumer reads only the newest group and a publisher MAY drop older ones. A group's first frame is a complete value. A json track MAY follow it with JSON Merge Patch ([RFC7396]) deltas applied in order; a binary track MUST write exactly one frame per group, since an opaque payload has no delta form. A stream track is lossless in the sense that nothing supersedes anything else. It is a single group, never rolled, carrying one self-contained payload per frame, delivered in order. A publisher that cannot write a payload MUST close the track rather than continue the log in a second group. A second group would present a gap as if it were a complete log; ending the track surfaces the failure instead, and a publisher with more to say opens a new track. A consumer MUST NOT skip to the newest group, since a later group does not supersede an earlier one the way a snapshot group does. It reads the one group the track carries, and MUST fail the read if the track carries a second, since whatever would have completed the first group is gone and yielding the remainder would present that gap as a continuous log. This is why a consumer takes groups in arrival order rather than by ascending sequence: groups are separate streams, so a second group MAY arrive with a lower sequence than the first, and skipping it would report a truncated log as a whole one. A consumer MUST NOT wait for the first group to end before it looks for a second. A publisher that opens a second group while the first is still open is broken in exactly the way this rule exists to catch, and a consumer that only checks at the group boundary waits forever on a group that publisher may never finish. A consumer MAY yield payloads it has already received from the first group before it fails, since those payloads precede the gap, but it MUST fail rather than block once they run out. Retention is bounded, and this is the limit of "lossless". A group's cache is finite, so a publisher that writes more than it holds evicts the log's earliest frames. A consumer that has not kept up, or that subscribes later, then cannot read the log at all: the read fails once it reaches the evicted prefix, rather than silently resuming at Curley Expires 28 March 2027 [Page 13] Internet-Draft hang September 2026 whatever the cache still holds. That is the intended behaviour, since a partial log presented as a whole one is exactly what this mode exists to prevent, and under compression (Section 6) the retained frames are undecodable anyway without the evicted prefix as context. A publisher SHOULD therefore keep a stream track's log within what its groups retain, and split anything unbounded across successive tracks; a consumer that needs the whole log SHOULD subscribe before the publisher exceeds that. 4.6.4. compression type Compression = "deflate" The compression field names the compression applied to the track's frames. If absent, the frames are uncompressed. A consumer MUST ignore a track whose compression it does not recognize, since it cannot decode the frames. The deflate value is the group-scoped DEFLATE of Section 6. A snapshot group covers a single value (plus any deltas), so its window spans that group alone; a stream group's frames compress against the earlier ones in the log. 4.6.5. broadcast The broadcast field carries the same meaning here as it does for a media rendition (Section 4.8.1). 4.7. Binary Fields A decoder config field carrying raw bytes, notably description (an AllowSharedBufferSource in WebCodecs), is carried in the catalog as a hex string ([RFC4648], Section 8). A publisher SHOULD emit lowercase hexadecimal characters and MUST NOT emit a 0x prefix or any separators. A consumer MUST accept either case. Note that this differs from the cmaf container's init field (Section 5), which is base64 ([RFC4648], Section 4); the two alphabets overlap, so the encoding cannot be detected and must be specified. 4.8. Common Rendition Fields Audio, video, and text renditions share the following fields, extending the WebCodecs decoder config for audio and video: Curley Expires 28 March 2027 [Page 14] Internet-Draft hang September 2026 type CommonExtensions = { "broadcast": string | undefined, "label": string | undefined, "container": Container, "jitter": number | undefined, } 4.8.1. broadcast By default a rendition's track lives in the same broadcast that served the catalog. The broadcast field overrides that, naming a different broadcast that publishes the track. The value is a relative path, resolved against the path of the broadcast that served the catalog. It uses relative reference resolution ([RFC3986], Section 5.2): a non-empty reference replaces the catalog broadcast's last path segment before applying . and .. segments. For example, ./source in a catalog served by room/ transcode resolves to room/source, while . resolves to room. An empty reference resolves to the catalog broadcast itself. A publisher MUST NOT use an absolute path, nor a reference that escapes above the root. The root is the consumer's authorized subtree, so such a reference names content the consumer cannot reach. A consumer MUST reject a catalog containing one, rather than resolving the reference against a different broadcast or ignoring the rendition. This lets a publisher author a catalog that points at tracks it does not republish. For example, a transcoder produces a catalog listing its own downstream renditions alongside the untouched source rendition, referencing the latter in the source broadcast rather than copying the bytes through. A consumer subscribes to such a rendition in the referenced broadcast, using the rendition's track name unchanged. 4.8.2. label The label field is a human-readable rendition name for a track picker. It is presentation metadata, not the track name used to subscribe. Multiple renditions MAY use the same label. 4.8.3. container The container used to frame this rendition's media, as described in Section 5. If absent, it defaults to { "kind": "legacy" }. Curley Expires 28 March 2027 [Page 15] Internet-Draft hang September 2026 4.8.4. jitter The maximum delay, in milliseconds, between a frame being ready and the publisher flushing it. A consumer's jitter buffer SHOULD be at least this large to avoid stalling. If absent, a consumer SHOULD assume each frame is flushed immediately. It is measured at the publisher: how far behind the media clock a frame is when the publisher hands it to the transport, whether an encoder, a reorder buffer, or a segmenter held it. An importer can estimate this delay from the media span of a batch, such as a run of audio PES packets between video packets in TS or an fMP4 fragment, without measuring the time spent waiting for input. It is never a measurement of the network, which a consumer observes for itself and which no two consumers of the same broadcast would agree on. A publisher MUST round the value up to a whole number of milliseconds, so a consumer sizing a buffer against it is never handed a bound below the real one. A publisher MUST NOT advertise 0; a track that flushes each frame immediately omits the field instead. A consumer receiving 0 SHOULD treat the field as absent, since it is a publisher rounding down rather than a claim of zero delay. A publisher MUST NOT lower a previously advertised value, since a burst it emitted once it may emit again. For example: * If each frame is flushed immediately, a video track's jitter is 1000/framerate rounded up: 34 at 30 fps. * If up to 3 B-frames may be emitted in a row, it is 3 * 1000/ framerate. * If frames are buffered into 2 second segments, it is 2000. * If frames are flushed several at a time, it is the media span of the whole burst, not of one frame. An audio frame's duration is codec dependent. AAC often uses 1024 samples per frame, so at 44100Hz an immediately-flushed track's jitter is 24. 5. Container Audio, video, and text tracks use a container to encapsulate the media payload. A rendition declares its container via the container field of its catalog entry (Section 4.8): Curley Expires 28 March 2027 [Page 16] Internet-Draft hang September 2026 type Container = { "kind": "legacy" } | { "kind": "cmaf", "init": string } | { "kind": "loc" } The kind field selects the framing; a consumer MUST ignore a rendition whose kind it does not recognize. Every container shares the same group rules: Each moq-lite group MUST start with a keyframe, except a group that contains only a discontinuity marker. If the codec does not support delta frames (e.g. audio), a group MAY consist of multiple keyframes. Otherwise, a group MUST consist of a single keyframe followed by zero or more delta frames. A group with no decodable frames is a walk-now discontinuity: one empty codec payload and no media. A consumer MUST NOT submit the marker to a decoder. Empty groups (zero objects) are permitted and mean nothing. After a discontinuity the timeline continues forward. A publisher that stops producing and may resume on the same track (e.g. an encoder idle for lack of demand) SHOULD publish a discontinuity marker when it stops, so the group before the pause does not reach across the gap and read as live. A group whose timestamps fall below the live edge earlier groups reached is malformed. A delivered sequence hole is a playhead event unless the boundary is contiguous within 1 ms. A consumer re-applies startup delay and skip at a playhead event; it does not reset codec state. 5.1. legacy The default, used when the container field is absent. Each frame starts with a timestamp, a QUIC variable-length integer (62-bit max) encoded in microseconds. The remainder of the payload is codec specific; see the WebCodecs specification for specifics. For video, a frame with an empty codec payload is the exclusive end of the frame before it, not media. A video publisher SHOULD end each group with one when the exclusive end is known. A publisher MAY estimate an unknown final duration from the frame cadence, but MUST NOT use batching or reorder delay as that duration. For reordered video, the group presentation endpoint does not necessarily bound the preceding frame in decode order; the publisher omits the marker unless that frame's exclusive end is known. A consumer MUST skip it and MUST NOT submit it to a decoder. It does not mean the track ended. Curley Expires 28 March 2027 [Page 17] Internet-Draft hang September 2026 For audio, an empty codec payload retains its terminal-trimming meaning: its timestamp is the exclusive endpoint of the source media. When a codec must receive additional packets to emit buffered source samples, the marker MUST precede those terminal packets in the same group. A consumer MUST NOT submit the marker to the codec decoder, MUST decode the terminal packets, and MUST discard decoded samples at or after the endpoint until a later group begins. Audio publishers do not append per-group duration markers because the codec defines each packet's duration. Data tracks retain empty payloads as data, without endpoint semantics. For example, h.264 with no description field would be annex.b encoded, while h.264 with a description field would be AVCC encoded. For a text track, the remainder is the cue in the track's declared format (for example a WEBVTT segment). 5.2. cmaf Each frame is a complete fragmented MP4 fragment (moof+mdat), carrying its own timestamps. Audio samples MUST be marked as sync samples, including samples inside a group. A sync sample does not declare an audio group boundary; the publisher chooses those boundaries. The init field is the initialization segment (ftyp+moov) for the track, base64-encoded ([RFC4648], Section 4). A consumer MUST feed init to the decoder before the first frame. 5.3. loc Each frame is a Low Overhead Container frame [I-D.ietf-moq-loc]: a property block, carrying the timestamp among other properties, followed by the codec payload. For audio and video, consumers accept empty codec payload metadata with the same video-duration and audio-terminal-trimming meanings as the legacy container. Data tracks retain empty payloads as data. A consumer MUST NOT submit it to a decoder. 6. Compression Some metadata tracks are compressed. Curley Expires 28 March 2027 [Page 18] Internet-Draft hang September 2026 Compression is signalled per track, never inferred from the track's name. A data track (Section 4.6) declares it with the compression field (Section 4.6.4). The two tracks this specification defines as always compressed, catalog.json.z (Section 4) and the timeline track (Section 7), are identified by their role instead. The .z suffix on those names is a naming convention, and a consumer MUST NOT treat it as a signal. Such a track is compressed per [moqflate]: each group is one raw DEFLATE stream, sync flushed at each frame boundary. The suffix is the declaration. 7. Timeline The timeline track is the broadcast's segment index. MoQ groups carry only an opaque sequence number; the timestamps live inside the media frames. The timeline republishes the broadcast's segmentation as metadata: one record per segment, mapping a span of content time to the group ranges that carry it on each media track. A consumer can answer "which groups cover time T on track X" and "where is the live edge" from a few bytes per segment, without downloading media. This is sufficient to render an HLS or DASH playlist, seek a VOD recording, or index an archive. The timeline is optional. There is one timeline per broadcast, because its purpose is that segments are aligned across the broadcast's tracks: segment N covers the same span of content time on every track, which is what HLS requires of switchable renditions. A broadcast that does not need aligned segments simply omits it. 7.1. Catalog Section The catalog's root archive field is the one name for the segment index, and for any durable recording of those ranges: type ArchiveSchema = { "track": string, "timescale": number | undefined, "durationMax": number | undefined, "replay": string | undefined, "store": string | undefined, "version": number | undefined, } The track field names the MoQ track carrying the segment records. The name timeline.z is RECOMMENDED; a consumer MUST use the advertised name rather than assuming it. A live publisher without a store advertises archive with the timeline fields (track, timescale, Curley Expires 28 March 2027 [Page 19] Internet-Draft hang September 2026 durationMax) alone. Every range the timeline advertises is FETCHable; with a store they are also durable. There is no sibling timeline entry and no generation: a client that must tell recordings apart compares replay and store. The timescale field is the units per second for the records' pts and duration values, and for durationMax. If absent, it defaults to 1000 (milliseconds). A zero timescale is invalid and MUST be refused. The durationMax field, if present, is the declared upper bound on a segment's duration, in timescale units. A publisher that controls its encoder knows its keyframe cadence up front, so a consumer can size buffers or write an HLS EXT-X-TARGETDURATION from the catalog alone, before observing a single segment. The value MUST NOT change for the life of the broadcast, and a publisher MUST NOT emit a record whose duration exceeds it. A publisher that cannot honor that MUST omit the field rather than emit a record contradicting it. The field is absent when the media decides the segmentation instead, which is the common case: a real-time encoder places keyframes on demand and a single GOP may be minutes long, and a publisher importing a source it does not control cannot promise anything about that source. A consumer needing a bound then derives one from the records it has seen, raising it as longer segments arrive. Wall-clock mapping is the catalog root clock (Section 4.2), not this section: a consumer derives the wall-clock time of any segment as clock.wall + pts after converting pts into the clock's timescale. The replay field, if present, is a relative MoQ broadcast path (Section 4.8.1) the archive is served back from. Absent, the timeline lives on the catalog's own broadcast. A wildcard replay path names no generation. The store field, if present, is the object-store URL the recording objects (Section 9) live under. Authorization for replay and store is external. The version field is the recording object format version (Section 9.2). It is 1 for this specification. A publisher that exposes a store MUST set it; a publisher that does not MUST omit it. A catalog that composes another broadcast's renditions MUST preserve that child's archive entry instead of synthesizing one. Curley Expires 28 March 2027 [Page 20] Internet-Draft hang September 2026 7.2. Track Framing The timeline track is a sliding window of records. An unbounded publisher only appends records, while a DVR publisher also removes records from the front as they expire. The first frame of each group is a UTF-8 JSON object containing a checkpoint of the retained window: { "offset": number, "start"?: number, "records": TimelineRecord[], } The offset is the absolute index of the oldest retained record. start is the absolute index of records[0] and defaults to offset when omitted. A publisher MAY omit a retained prefix from a checkpoint; a consumer that did not receive that prefix reports offset through start as skipped. Each subsequent frame is one UTF-8 JSON operation, either { "push": TimelineRecord } to append a record at the next absolute index, or { "pop": number } to remove that many records from the front. Indices MUST NOT exceed 2^53 - 1. A publisher MAY roll groups to bound late-join cost or improve compression. Each new group restates a decodable suffix of the retained window, so group boundaries are an encoding detail and MUST NOT surface as duplicate records to the application. A consumer that missed records reports their absolute index range as skipped before continuing with the retained suffix. The frames are DEFLATE-compressed ([RFC1951]) within each group. The publisher ends each frame's compressed data with an empty sync-flush block (the 0x00 0x00 0xff 0xff trailer is removed, as in [RFC7692]), so a consumer decompresses frames incrementally from the group's first frame. The .z suffix on the RECOMMENDED track name marks this compression, mirroring the catalog's catalog.json.z sibling. 7.3. Records Each record describes one complete segment: Curley Expires 28 March 2027 [Page 21] Internet-Draft hang September 2026 type TimelineRecord = { "segment": number, "pts": number, "duration": number, "tracks": Map | undefined, } type TimelineRange = { "start": number, "end": number, "keyframe": boolean | undefined, } The segment field is the segment's number. Numbers are consecutive within a broadcast, anchoring HLS EXT-X-MEDIA-SEQUENCE; they are explicit rather than implied by record order so a reader joining mid- stream, or reading a windowed recording, keeps stable numbering. The pts field is the segment's start and duration its length, both in the timeline's timescale. The next record's pts equals pts + duration unless content time itself jumped; a consumer SHOULD treat such a jump as a discontinuity. The tracks field maps each participating track name to the group ranges it contributes. Each range covers groups start through end inclusive, as used by moq-lite FETCH and SUBSCRIBE. More than one range means the group sequence is discontinuous inside the segment: the skipped groups never existed. A track absent from the map has no content for the span (a gap; HLS EXT-X-GAP). Participating tracks need not be audio or video. A catalog, or an application's own metadata track such as a chat log, is listed exactly like a media track, which is what lets a recording (Section 9) address all of them the same way. A consumer that only wants renditions therefore MUST select tracks by consulting the catalog rather than by assuming every name in the map is media. A record MUST tolerate and SHOULD preserve unknown fields, like the catalog. The keyframe field states whether the range's first group starts with a keyframe, i.e. whether a player can join or switch renditions there. If absent, it defaults to true; a publisher sets false when a source resumes without one, so an exporter knows not to advertise the segment as independently decodable. Curley Expires 28 March 2027 [Page 22] Internet-Draft hang September 2026 7.4. Segmentation A segment is a span of content time shared by every media track. A track contributes every group whose start falls inside the span, so a segment boundary SHOULD land on a group start: every group already begins with a keyframe (Section 5), so a boundary at a group start lets each track contribute whole groups and remain independently decodable. A segment MAY span multiple groups of a track (short groups packed into a longer segment). How boundaries are chosen is publisher policy: following a source's existing segmentation (an imported HLS playlist, CMAF segments on disk), or pacing by a minimum duration. A publisher pacing itself SHOULD end a segment at the earliest point that is a group start on every enrolled track and at least the minimum past the segment's start, which makes the track with the coarsest groups pace the broadcast and leaves no track's group split across a boundary. A minimum is always satisfiable, whereas a maximum is not: a single group longer than it cannot be divided. Where no such point exists because two tracks have different coarse cadences, a publisher MUST choose one of them rather than a point interior to any track's group. Whatever the policy, a publisher MUST NOT emit a record until the segment is complete: every _pacing_ track's groups for the span are known. Records are therefore self-contained and immediately servable, and the newest record is the live edge. A pacing track that has produced nothing for the span holds the record back; a publisher that knows a track has stopped for good closes it, and the record then simply omits it (a gap). A pacing track is one whose groups arrive continuously, which is what makes them usable as boundaries. A track that publishes on its own schedule cannot pace: a catalog emits a group only when the renditions change, so a timeline waiting for it would stall the moment it went quiet. Such a track is _non-pacing_: its groups are listed in whichever segment is open when they arrive, but it never determines a boundary and never holds a record back. A publisher SHOULD record its catalog this way, so a recording can resolve the renditions in effect at any segment. Placement of a non-pacing track's groups is therefore by arrival rather than by content time: nothing waits for them, so a group that arrives after its segment has already been published is listed in the next one. When a segment closes, every non-pacing group that arrived while it was open belongs to that segment regardless of the timestamp basis carried by the non-pacing track. The frames still carry their own timestamps, so no timing information is lost. A non-pacing track's timestamps do not extend the final segment's duration. A Curley Expires 28 March 2027 [Page 23] Internet-Draft hang September 2026 non-pacing track whose group never closes (an append-log such as a moq-json stream) is listed once, in the segment its group opened in. A publisher that needs such content addressable per segment SHOULD roll the group at segment boundaries, which costs the shared compression window but makes each segment self-contained. A group that starts before the first boundary belongs to the first segment. The final segment of an ended broadcast has no closing boundary; its duration runs to the newest known content. A publisher SHOULD carry the end of the last group's content into that value, since a publisher that knows only where each group _started_ would report a duration one group short, and zero for a final segment that is a single group. 8. MPEG-TS Service Information A broadcast imported from an MPEG-TS multiplex can carry the multiplex's standalone service-information tables (ISO/IEC 13818-1 program-specific information and ETSI EN 300 468 DVB SI: SDT, NIT, BAT, EIT, and any table the publisher does not recognize) so an exporter can re-emit them byte-for-byte. The sections are opaque: nothing in this mechanism parses a table, so an unrecognized long- form table round-trips exactly like a known one. Short-form tables, recognized or not, are carried with latest-value semantics instead (see below): the short-form tables broadcast systems define are clocks and stuffing, and a hypothetical multi-section static one would collapse to its most recent section. The tables are state, not events: a transport stream retransmits them continuously only because a TS receiver can tune in at any moment, so the repetition rate is a property of the unreliable transport, not of the data. Here each table rides a dedicated snapshot track, delivered once at join and republished on change, per the root catalog's guidance that dynamic content is delegated to other tracks. Consumers that are not TS exporters never pay for it. 8.1. Catalog Section The catalog's mpegts section maps each carried table to its track under an si field, keyed by the PID the sections ride on and then by table_id: Curley Expires 28 March 2027 [Page 24] Internet-Draft hang September 2026 type Mpegts = { // ... other MPEG-TS carriage fields ... "si": { [pid: string]: { [tableId: string]: SiEntry } } | undefined, } type SiEntry = { "track": string, "interval": number | undefined, } Both keys are written in decimal, since JSON object keys are strings. table_id is byte 0 of generic section syntax, so the key is no less generic than the PID; which table_id ranges mean what is a DVB convention that appears nowhere in the schema. * track: the name of the snapshot track carrying this table's sections, on the same broadcast as the catalog. * interval: how often an exporter MUST re-emit the sections at most, in milliseconds. A hint carrying the table's own repetition requirement (for DVB, the ETSI TS 101 211 maxima). When absent, an exporter SHOULD fall back to a fast cadence of its own choosing, so an unknown table degrades to a safe rate rather than being dropped. Entries are added when a table is first observed, which is acquisition-time traffic; steady-state table revisions touch only the named track, never the catalog. 8.2. Track Format An SI track is binary, without the media container framing (Section 5). Each frame is one sub-table: its complete current sections, concatenated verbatim in section_number order, each including its header and CRC. Sections are self-delimiting via section_length, so the frame needs no framing of its own. A sub-table's identity is its generic section header (table_id, table_id_extension) extended by the documented disambiguators for two DVB table families: original_network_id (bytes 8..10) for SDT other (table_id 0x46), and transport_stream_id plus original_network_id (bytes 8..12) for EIT (0x4E..0x6F), whose generic identities are only unique within one network or transport stream. Each group is a snapshot: reading its frames in order yields the table's complete current state, where a later frame replaces an earlier one with the same sub-table identity. A joiner reads only Curley Expires 28 March 2027 [Page 25] Internet-Draft hang September 2026 the newest group and is current; older groups never need to be fetched. A writer MAY append a frame to an open group to revise one sub-table incrementally; a writer that only ever cuts complete groups is trivially conformant. A publisher MUST NOT mix versions within a sub-table's frame: sections of one version are buffered until the generation is judged complete and committed atomically. Completeness is contiguity (all of 0..=last_section_number present) or one observed full transmission cycle, whichever comes first: some tables number their sections sparsely (DVB EIT schedule skips unused numbers per segment), so a repeated section within the pending generation proves the cycle wrapped and the set is complete as transmitted. A section lost before the cycle wraps is indistinguishable from a legitimately skipped number, so a committed set can transiently omit it until the next cycle re-supplies it; no section-counting receiver can do better. A publisher SHOULD carry only sections whose current_next_indicator is set. A section without a long-form header (short-form syntax: TDT/TOT and similar) has no extension, version, or numbering, so its table is a single latest-value slot: each arrival replaces the last, and a byte- identical repetition is not a change. This is what makes a time table proxyable: each tick republishes a small snapshot, a joiner reads the newest, and staleness is bounded by the source's own repetition interval plus path latency, the same bound a receiver of the original multiplex has. Carrying the source's time rather than synthesizing one keeps the clock consistent with the EPG (event times are expressed on the source's clock) and preserves TOT's local-time- offset descriptors, which are operator policy a re-multiplexer cannot invent. 9. Recording A live broadcast is bounded history: moq-lite [moql] serves the present with SUBSCRIBE and the recent past with FETCH, both ending at the publisher's cache. A _recording_ is the persistent tier, writing a broadcast to a filesystem or object store so it can be served back long after the live session ended. A recording is addressed by segment. The timeline (Section 7) names every segment and the groups that carry it. Each track's object is addressed by its smallest and largest stored group IDs; the timeline supplies those bounds without reading media. A reader that wants segment N of a track issues one whole-object GET, with no range header and no second request, which is what both an HLS or DASH origin and a player seeking a VOD recording need. Curley Expires 28 March 2027 [Page 26] Internet-Draft hang September 2026 A recording covers the tracks the timeline describes, plus the catalog and the timeline itself. A broadcast with no timeline has no segments and cannot be recorded this way. 9.1. Layout A recording is a set of objects under a common prefix: //.info //groups/. //segments/ The common prefix is application-defined and is not interpreted by this format. is the track's UTF-8 name with every byte outside A-Z a-z 0-9 _ - percent-encoded as % followed by two uppercase hexadecimal digits. For example, catalog.json becomes catalog%2Ejson. An encoded name never contains / or begins with ., so a track cannot address anything outside the prefix. .info, groups, and segments are reserved within a track directory. and are the inclusive first and last group sequence numbers in the object. Recorded group and segment IDs MUST be integers in the range 0 through 9007199254740991 (2^53 - 1), so timeline JSON preserves them exactly. Each filename field is written as exactly 19 decimal digits, padded with leading zeros, so lexical and numeric order agree. Writers and readers MUST reject IDs outside this range, including reconstructed group IDs in the binary table. A range-named object MUST contain at least one group, and its table's first and last sequences MUST match the filename. For each track, object ranges MUST be nonoverlapping and strictly increasing in timeline segment order; gaps are allowed both within and between objects. A track with no stored groups for a segment has no object or range in that record. There are no empty index objects or duplicate copies addressed by segment number. This layout applies to every recorded track, including the catalog, except the recording-owned timeline track identified by the catalog's archive field (Section 7.1). The timeline uses segments/ with the same binary envelope and at least one complete Window group per object. is the committed timeline segment ID, encoded as 19 zero-padded decimal digits within the recording ID range. After its first segment, the recording MUST commit consecutive segment IDs, including all-gap segments. A writer MUST stop before allocating an ID beyond the limit; IDs never wrap. Each track has its own objects so a reader can fetch one rendition without downloading the others. Curley Expires 28 March 2027 [Page 27] Internet-Draft hang September 2026 9.2. Track Objects /.info is a UTF-8 JSON object containing the immutable properties needed to interpret and replay the track: { "version": 1, "priority": 0, "timescale": 1000000 } All three fields are required integers. version identifies this recording format and MUST be 1. priority and timescale have the meanings of moq-lite TRACK_INFO [moql]: priority is in the range 0 through 255, and timescale is in the range 1 through 9007199254740991, so JSON consumers can preserve it exactly. A reader MUST preserve integer values exactly. Publisher Max Age is not stored; a reader supplies its own serving policy. The track object MUST be durable before its first segment object is stored, including for the timeline track. It is immutable for the lifetime of the recording. A reader MUST refuse an unknown version or invalid track properties. On an existing .info, a writer MUST validate and compare the parsed version, priority, and timescale values; JSON whitespace and member order do not affect equality. Different property values MUST fail enrollment, and the existing object MUST NOT be rewritten. 9.3. Segment Objects A segment object holds one track's complete groups for one segment: Segment Object { Version (i) = 1 Group Count (i) Group Table { Sequence Delta (i) Frame Count (i) Frame Table { Timestamp (i) Payload Offset (i) Payload Length (i) } ... } ... Payload Bytes (..) } Curley Expires 28 March 2027 [Page 28] Internet-Draft hang September 2026 Fields annotated (i) are variable-length integers using the QUIC encoding ([RFC9000], Section 16). Group Count gives the number of group entries; each Frame Count gives the number of frame entries in that group. The first Sequence Delta is the absolute group sequence number. Each subsequent value is the group sequence number minus the previous sequence number minus one; zero therefore means the next consecutive group. Groups MUST have strictly ascending sequence numbers; reconstruction MUST reject values exceeding the recording ID limit (Section 9.1). For range-named objects, the sequences MUST match the ranges in the timeline. Frame entries appear in their original order within each group. Timestamp is the frame's absolute timestamp in the track's timescale units, not a delta. It MUST be in the range 0 through 9007199254740991 (2^53 - 1), so browser replay preserves it exactly; writers and readers MUST reject larger values. Payload Offset is relative to the start of Payload Bytes, immediately after the last table entry; Payload Length is the frame's payload size in bytes. Payloads are concatenated in table order, without gaps, overlap, or trailing bytes. They are the original frame payloads, including any group-scoped compression, without moq-lite FRAME headers. A reader can locate a group or a frame index directly from the table without parsing earlier payloads. A decoder MUST validate the complete table before accessing any payload. Counts and entries MUST fit in the retrieved object, and every offset and length MUST describe a range within its payload bytes, with overflow checked before arithmetic. An unknown version or malformed table is a missing segment, not an invalid recording. A segment object MUST contain whole groups. Segmentation forbids a boundary inside a track's group (Section 7.4), so a reader never has to stitch a group across objects. Segment objects are immutable once written and are parseable without the timeline. The timeline uses the same envelope and stores only complete Window groups (Section 7.2) closed while committing that segment. Its own groups are discovered by listing, not included in the record's tracks, avoiding a record that must index itself. A reader replays their checkpoints and operations in group order, preserving group boundaries and their independent DEFLATE windows. Neither timeline nor media objects are appended to or rewritten. One writer owns a recording prefix. An existing segment object key MAY be reused only for identical object bytes; a conflicting create MUST fail. Curley Expires 28 March 2027 [Page 29] Internet-Draft hang September 2026 9.4. Writer Behavior A writer subscribes to the broadcast and buffers the in-progress segment independently of the publisher or relay cache. It writes a track's segment object once that track's groups for the span are known. Per-track objects are written independently: a track whose content is complete does not wait for a slower one. The recording contract MUST NOT depend on a relay retaining those groups while storage catches up. A writer MUST make each track's .info and segment object durable before publishing the timeline record that references them. The recording owns its timeline encoder so it can publish the ranges actually stored. If a track's object cannot be made durable, the writer MUST omit only that track from the record's tracks; successfully stored tracks retain their ranges. The segment number and timing are unchanged, including when every track is omitted. The writer pushes the resulting record and every subsequent record through its own Window encoder. It MUST NOT copy source frames whose Window position or group-local DEFLATE dictionary depends on a record it changed. A writer MUST accept new groups for each track in strictly increasing sequence order and MUST refuse a duplicate or decreasing sequence; it does not reorder arrivals. Groups already accepted MAY complete in any order; the writer buffers them and writes their table in sequence order. An incomplete group omitted by an application-forced segment cut MUST NOT be inserted into a later object if that would overlap or precede an earlier object's range. The application MAY force a cut or remove a stalled track; storage does not impose a timeout. A writer MUST NOT insert objects for earlier segments after committing a later segment. For segment N, the writer publishes the stored ranges, applies any retention pops, closes the timeline's current Window group, and stores the complete groups under the timeline track's segments/N key, with N encoded as specified in Section 9.1. It MUST make that object durable before committing the next segment or deleting expired objects. If the timeline object cannot be made durable, the recording stops at the preceding durable timeline object. On a clean end, the writer MUST flush the final partial segment, finish the timeline track, and make its final complete groups durable. There is no completion marker; object listing alone does not distinguish a clean end from an interruption. Curley Expires 28 March 2027 [Page 30] Internet-Draft hang September 2026 9.5. Retention A recording has one of two retention modes: * An _archive_ is unbounded and retains every complete segment until explicitly deleted. * A _DVR_ retains a configured duration of complete segments and expires the oldest whole segments as newer ones become durable. An application offering DVR without an explicit retention value SHOULD default to at least 30 seconds. The writer removes the oldest segment only when the remaining complete segments still cover the configured duration. Retention is measured from timeline records, not from wall-clock arrival or relay cache state. Trimming occurs only as part of committing a new segment, before that segment's timeline object is finalized (Section 9.4). A recording MUST NOT trim after its final segment is committed. Expiration first pops expired records from the timeline window and makes the resulting complete timeline groups durable, then deletes the expired segments' objects. The writer MUST retain the latest timeline object, even for an all-gap segment, and enough earlier timeline groups to recover the retained window from a checkpoint. The index therefore never advertises media the retention process has already deleted, although media objects MAY temporarily outlive the index. Relay cache eviction does not change the recording timeline. Before accepting new groups, a restarting DVR writer MUST recover the complete retained timeline and list every recorded track's groups/ prefix under exclusive ownership of the recording prefix. After waiting the configured deletion grace period from successful recovery, it MUST delete group objects whose keys are absent from the recovered retained records, including expired objects and uncommitted uploads left by a crash. It MUST NOT perform this cleanup if timeline recovery or listing fails or is incomplete, or while another writer can create or commit objects. This completes previously committed expiration; it does not pop additional records after a final segment. Timeline objects needed for checkpoint recovery and .info objects are not candidates for this cleanup. Curley Expires 28 March 2027 [Page 31] Internet-Draft hang September 2026 9.6. Bootstrap and Recovery A reader or restarting writer lists the timeline track's segments/ prefix and replays its objects in numeric segment order, including retention operations, from a retained checkpoint. The catalog supplies the timeline track's name (Section 7.1). The recovered records determine the committed track object keys; a missing or malformed referenced object MUST NOT be served. Objects not referenced by the recovered timeline do not advertise content on their own. Listing is not an atomic snapshot across tracks; absence from an earlier listing MUST NOT override a successful GET of an object referenced by a later durable timeline record. After replaying segment N below the recording ID limit, a reader follows an active recording by GET of segments/N+1, without refreshing media listings. At the limit there is no next key; this does not establish a clean recording end. Not Found does not distinguish a pending commit, an expired DVR segment, or an interrupted recording. A reader that has fallen behind retention MUST bootstrap again from a retained checkpoint. Alternatively, a reader MAY list timeline keys after its last replayed key, consume all pages, sort the results, and replay them in order. A continuation token is used only within that enumeration; the next refresh starts from the last replayed key. A reader MUST NOT advance its replay cursor past a missing segment without recovering from a retained checkpoint. Retention operations evict expired ranges from the reader's cached index; incremental listing alone does not report deletions. 9.7. Reader Behavior Reading segment N of track T is: take the minimum and maximum group IDs from T's ranges in record N, GET groups/., and parse the groups. The table MUST contain exactly the group sequences advertised for that track in the record, including its gaps. Nothing on that path reads media the consumer did not ask for, and nothing requires a second request. A reader MAY serve moq-lite FETCH from a recording. Given a group, a reader locates the candidate object from a cached filename range index or the timeline's track ranges, then uses the table for the group and any requested frame_start [moql]. A filename listing requires no media GETs. On a backend guaranteeing lexical listing order and exclusive offsets, listing after groups/ (19 padded digits, without the dot) selects the first candidate with an upper bound at least the requested group. A reader MUST check the lower bound and committed timeline membership before serving it; an internal gap is resolved by the candidate's table. An unordered Curley Expires 28 March 2027 [Page 32] Internet-Draft hang September 2026 listing MUST be collected and sorted before selecting a candidate; taking its first result is insufficient. The reader reconstructs moq-lite FRAME headers from the stored timestamps and lengths, preserving the original payload bytes. A group absent from the recording is a normal FETCH failure. Recording catalog groups does not establish which catalog update applies to a media group; this format does not define that correlation. A reader deriving a presentation-ordered format renders it from the timeline and transmuxes segment objects on demand. Nothing derived needs to be stored: the playlist or manifest is a function of the timeline, and a media segment is a function of one recorded object. An HLS segment URI can carry the track and both group bounds, allowing the handler to resolve the exact object without listing or a segment-ID index. HLS sequence numbers remain timeline metadata and need not occur in object names. 10. Rooms A room is a broadcast path prefix. A participant publishes camera and microphone at {identity}/camera.hang and a screen share at {identity}/screen.hang, relative to that prefix. Identity MUST contain at least one nonempty path segment. Consumers MAY also recognize the unsuffixed camera and screen forms. A participant MAY publish a chat track containing a JSON window of messages from the last ten seconds. Each edit opens a new group containing one uncompressed UTF-8 JSON header of the form {"offset": N, "records": ["text", ...]}. The records are the complete retained window, oldest first. The offset is the absolute index of its first record and advances as records expire. Offsets MUST be nonnegative safe JSON integers (at most 2^53 - 1). Publishers MUST retire records after ten seconds, including while idle. Consumers start at the latest group and report records entering or leaving the window; missing index ranges indicate messages that expired before being received. Consumers MUST report malformed JSON, non-string records, and transport failures as errors, distinct from the clean end of the track. Sender identity comes from the broadcast path, not the payload. This track is distinct from an application's hang/chat.json snapshot extension. Curley Expires 28 March 2027 [Page 33] Internet-Draft hang September 2026 11. Security Considerations A rendition's broadcast reference (Section 4.8.1) resolves against the consumer's root, which is the subtree it is authorized for. Clamping a reference that escapes above that root would silently redirect the subscription to an unrelated broadcast, so a consumer rejects the catalog instead. TODO Security A consumer parsing a recording (Section 9) is parsing data at rest that it did not necessarily write. It MUST validate the group/frame table against the bytes actually retrieved before allocating from its counts or accessing payloads (Section 9.3), and treat a malformed object as a missing segment rather than letting it invalidate the recording. It MUST reject a track object with a zero timescale. Varint fields are subject to the same limits as moq-lite [moql]. A recording inherits the confidentiality and integrity properties of the storage holding it; encryption at rest is transparent to the format and out of scope. 12. IANA Considerations This document has no IANA actions. 13. References 13.1. Normative References [I-D.ietf-moq-loc] Zanaty, M., Nandakumar, S., and P. Thatcher, "Low Overhead Media Container", Work in Progress, Internet-Draft, draft- ietf-moq-loc-04, 20 July 2026, . [moqflate] Curley, L., "DEFLATE Compressed Tracks for MoQ", . [moql] Curley, L., "Media over QUIC - Lite", Work in Progress, Internet-Draft, draft-lcurley-moq-lite-05, 30 June 2026, . [RFC1951] Deutsch, P., "DEFLATE Compressed Data Format Specification version 1.3", RFC 1951, DOI 10.17487/RFC1951, May 1996, . Curley Expires 28 March 2027 [Page 34] Internet-Draft hang September 2026 [RFC2119] Bradner, S., "Key words for use in RFCs to Indicate Requirement Levels", BCP 14, RFC 2119, DOI 10.17487/RFC2119, March 1997, . [RFC3986] Berners-Lee, T., Fielding, R., and L. Masinter, "Uniform Resource Identifier (URI): Generic Syntax", STD 66, RFC 3986, DOI 10.17487/RFC3986, January 2005, . [RFC4648] Josefsson, S., "The Base16, Base32, and Base64 Data Encodings", RFC 4648, DOI 10.17487/RFC4648, October 2006, . [RFC5646] Phillips, A., Ed. and M. Davis, Ed., "Tags for Identifying Languages", BCP 47, RFC 5646, DOI 10.17487/RFC5646, September 2009, . [RFC6838] Freed, N., Klensin, J., and T. Hansen, "Media Type Specifications and Registration Procedures", BCP 13, RFC 6838, DOI 10.17487/RFC6838, January 2013, . [RFC7396] Hoffman, P. and J. Snell, "JSON Merge Patch", RFC 7396, DOI 10.17487/RFC7396, October 2014, . [RFC8174] Leiba, B., "Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words", BCP 14, RFC 8174, DOI 10.17487/RFC8174, May 2017, . [RFC9000] Iyengar, J., Ed. and M. Thomson, Ed., "QUIC: A UDP-Based Multiplexed and Secure Transport", RFC 9000, DOI 10.17487/RFC9000, May 2021, . [webcodecs] W3C, "WebCodecs", . 13.2. Informative References [RFC7692] Yoshino, T., "Compression Extensions for WebSocket", RFC 7692, DOI 10.17487/RFC7692, December 2015, . Curley Expires 28 March 2027 [Page 35] Internet-Draft hang September 2026 Appendix A: Changelog moq-hang-03 * Clarified that CMAF audio samples are sync samples independently of publisher group boundaries. * Clarified that container importers can estimate jitter from batch media spans without measuring input wait time. * Specified the jitter field's computation: the publisher's own structure rather than the network, rounded up to whole milliseconds, never 0 (a consumer treats 0 as absent), and never lowered once advertised. The 30 fps and 44.1 kHz AAC examples became 34 and 24. * For video, an empty codec payload is the exclusive end of the frame before it. A video publisher SHOULD end each group with one when the exclusive end is known. Audio retains its terminal- trimming marker before codec drain packets. A publisher MAY estimate an unknown final duration from the frame cadence, but MUST NOT use batching or reorder delay as that duration. A consumer skips it and does not submit it to a decoder. Audio terminal-packet trimming is unchanged. * Specified version 1 recording objects: JSON track properties and binary group/frame tables with ascending, delta-encoded group sequences. * Addressed track objects by inclusive group bounds and timeline objects by consecutive segment IDs, with incremental discovery and per-track omission on storage failure. * Restricted retention updates to segment commits and removed completion markers. * Limited recorded group and segment IDs and frame timestamps to JSON-safe integers, including delta reconstruction. * Compared existing track properties by parsed values rather than JSON serialization. * Required exclusive DVR restart recovery to remove unreferenced group objects left by interrupted expiration. * Replaced the catalog root timeline field with archive, carrying the timeline track plus optional replay, store, and recording version. Curley Expires 28 March 2027 [Page 36] Internet-Draft hang September 2026 * A marker group of one empty frame declares a discontinuity. Empty groups mean nothing. Timestamps only move forward; a group below the live edge is malformed. A delivered sequence hole is a playhead event unless contiguous within 1 ms. * A publisher that stops producing and may resume on the same track SHOULD publish a discontinuity marker when it stops. * An audio endpoint bounds only the terminal packets that follow it in its own group. * Replaced the archive timeline wall field with a root clock section (wall plus timescale): one fixed broadcast mapping every track and the archive index convert into, independent of any archive. Zero timescales and walls past the JSON-safe integer range are refused. Acknowledgments TODO acknowledge. Author's Address Luke Curley Email: kixelated@gmail.com Curley Expires 28 March 2027 [Page 37]