Skip to content

Commit dabb8a8

Browse files
kixelatedclaude
andcommitted
moq-lite-05: make Timescale/Timestamp mandatory; drop one-sided prefixes
moq-lite: every Track now has a media timeline. `Timescale` MUST be non-zero and the per-frame `Timestamp Delta` and per-datagram `Timestamp` are unconditional on the wire, removing the timescale-0 fallback that forced both endpoints to handle present/absent timestamp fields. Expiration is always timestamp-based; only empty groups (which carry no timestamp) fall back to wall-clock. Also dropped the `Publisher`/`Subscriber` prefix from fields that exist on only one side: `Publisher Timescale`/`Publisher Compression` -> `Timescale`/`Compression`, `Subscriber Max Latency` -> `Max Latency`. `Priority` and `Ordered` keep the prefix since both variants exist and the prose distinguishes them. Wire format unchanged by the rename. moq-timestamp extension: keep graceful degradation since moq-transport objects are independently droppable. Removed SETUP negotiation (the properties are self-describing KVPs). A missing Timescale now defaults to milliseconds and an object with no Timestamp falls back to wall-clock arrival time. Documented that the object Timestamp is absolute, not delta-encoded, because moqt provides no sub-group reliability. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 8d9973a commit dabb8a8

2 files changed

Lines changed: 39 additions & 64 deletions

File tree

draft-lcurley-moq-lite.md

Lines changed: 28 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -159,8 +159,8 @@ A frame is used to represent a chunk of data with an upfront size.
159159
The contents are opaque to the moq-lite layer.
160160

161161
Each frame carries a presentation timestamp expressed in the parent Track's `Timescale` (units per second, part of the [TRACK_INFO](#track-info)).
162+
Every Track has a media timeline — the `Timescale` is always non-zero and every frame is timestamped.
162163
The timestamp is the source-of-truth for media time and is used by the moq-lite layer for [expiration](#expiration) decisions instead of wall-clock arrival time.
163-
A Track with a `Timescale` of 0 (unspecified) carries no meaningful timestamps and falls back to wall-clock arrival time for expiration.
164164

165165
# Flow
166166
This section outlines the flow of messages within a moq-lite session.
@@ -309,7 +309,7 @@ When the accepted track has already ended with no matching groups there is no st
309309
A rejection is a stream reset: if the publisher cannot serve the subscription — the track does not exist, or it otherwise refuses — it MUST reset the stream rather than leave it pending, and SHOULD do so promptly (within roughly a round trip) so the subscriber is not left waiting.
310310
A subscription the publisher accepts but has no groups for yet is not a rejection: for a live track the publisher MAY withhold SUBSCRIBE_OK until the first matching group resolves the start. A subscriber therefore distinguishes "pending" from "refused" by the stream reset, not by a timeout.
311311
The Subscribe Stream does not carry the track's publisher properties — those are immutable and fetched once via a [Track Stream](#track-stream) (see [TRACK_INFO](#track-info)).
312-
The subscriber MUST have the track's TRACK_INFO before it can parse the FRAME messages that arrive on Group Streams, since the timescale and compression determine the frame wire format; it MAY open the Track and Subscribe streams concurrently and buffer frames until TRACK_INFO arrives.
312+
The subscriber MUST have the track's TRACK_INFO before it can parse the FRAME messages that arrive on Group Streams, since compression determines the frame wire format and the timescale is needed to interpret each timestamp; it MAY open the Track and Subscribe streams concurrently and buffer frames until TRACK_INFO arrives.
313313

314314
The publisher sends SUBSCRIBE_OK once the absolute start group is resolved, and SUBSCRIBE_END once no further groups will be produced (see [SUBSCRIBE_OK](#subscribe-ok) and [SUBSCRIBE_END](#subscribe-end)).
315315
The publisher closes the stream (FIN) only once every group from start to end has been accounted for, either via a GROUP stream (completed or reset) or a SUBSCRIBE_DROP message.
@@ -419,7 +419,7 @@ An application SHOULD use `ordered` when it wants to provide a VOD-like experien
419419
An application SHOULD NOT use `ordered` when it wants to provide a live experience, preferring to skip old groups rather than buffer them.
420420

421421
Note that [expiration](#expiration) is not affected by `ordered`.
422-
An old group may still be cancelled/skipped if it exceeds the `Subscriber Max Latency`.
422+
An old group may still be cancelled/skipped if it exceeds the `Max Latency`.
423423
An application MUST support gaps and out-of-order delivery even when `ordered` is true.
424424

425425

@@ -431,20 +431,19 @@ It is not crucial to aggressively expire groups thanks to [prioritization](#prio
431431
However, a lower priority group will still consume RAM, bandwidth, and potentially flow control.
432432
It is RECOMMENDED that an application set conservative limits and only resort to expiration when data is absolutely no longer needed.
433433

434-
The publisher SHOULD reset Group Streams for non-latest groups whose age relative to the latest group exceeds the `Subscriber Max Latency` value in SUBSCRIBE/SUBSCRIBE_UPDATE.
434+
The publisher SHOULD reset Group Streams for non-latest groups whose age relative to the latest group exceeds the `Max Latency` value in SUBSCRIBE/SUBSCRIBE_UPDATE.
435435
The subscriber MAY also locally drop such groups for its own resource accounting.
436436
Expiration only removes the group from the live subscription's stream; the publisher MAY still retain it for FETCH or new subscriptions.
437437

438438
Group age is computed relative to the latest group by sequence number.
439439
A group is never expired until at least the next group (by sequence number) has been received or queued.
440-
Once a newer group exists, a group is considered expired if the time between its first frame and the latest group's first frame exceeds `Subscriber Max Latency`.
440+
Once a newer group exists, a group is considered expired if the time between its first frame and the latest group's first frame exceeds `Max Latency`.
441441

442-
If the Track's negotiated `Timescale` is non-zero, the time delta is computed from per-frame timestamps (see [Frame](#frame)).
443-
Otherwise the delta is computed from wall-clock arrival time: the first byte of a group received (subscriber) or queued (publisher).
444-
Timestamp-based expiration is preferred because it remains consistent across relays and is unaffected by buffering or jitter.
442+
The time delta is computed from per-frame timestamps (see [Frame](#frame)).
443+
Timestamp-based expiration remains consistent across relays and is unaffected by buffering or jitter, unlike wall-clock arrival time.
445444

446445
A group that contains zero frames has no timestamp.
447-
For expiration purposes its effective time is the wall-clock arrival/queue time of the group itself, regardless of the Track's `Timescale`.
446+
For expiration purposes its effective time is the wall-clock arrival/queue time of the group itself: the first byte of the group received (subscriber) or queued (publisher).
448447
This avoids stalling expiration on tracks that intentionally emit empty groups as keep-alives or gap markers.
449448

450449
An expired group SHOULD be reset at the QUIC level to avoid consuming flow control.
@@ -497,14 +496,11 @@ Each datagram body has the following encoding (note: there is no message length
497496
DATAGRAM Body {
498497
Subscribe ID (i)
499498
Group Sequence (i)
500-
[Timestamp (i)]
499+
Timestamp (i)
501500
Payload (b)
502501
}
503502
~~~
504503

505-
`Timestamp` is present only when the Track's `Publisher Timescale` (see [TRACK_INFO](#track-info)) is non-zero.
506-
When `Publisher Timescale` is 0, the field is omitted from the wire and the datagram body consists of just `Subscribe ID`, `Group Sequence`, and `Payload`.
507-
508504
**Subscribe ID**:
509505
The Subscribe ID of an active subscription on the same session.
510506
A subscriber receiving a datagram with an unknown Subscribe ID MUST silently drop it.
@@ -519,7 +515,7 @@ Any varint value (including 0) is a valid absolute timestamp.
519515

520516
**Payload**:
521517
The frame payload, extending to the end of the datagram.
522-
If the Track's `Publisher Compression` is non-zero, the payload is compressed using the negotiated algorithm (see [TRACK_INFO](#track-info)).
518+
If the Track's `Compression` is non-zero, the payload is compressed using the negotiated algorithm (see [TRACK_INFO](#track-info)).
523519
The total datagram body (including all header fields above and the compressed payload if applicable) MUST NOT exceed 1200 bytes.
524520
This limit ensures the datagram fits within the minimum QUIC path MTU without IP-layer fragmentation.
525521
Payloads that would not fit MUST be sent as a Group Stream instead.
@@ -742,7 +738,7 @@ SUBSCRIBE Message {
742738
Track Name (s)
743739
Subscriber Priority (8)
744740
Subscriber Ordered (8)
745-
Subscriber Max Latency (i)
741+
Max Latency (i)
746742
Group Start (i)
747743
Group End (i)
748744
}
@@ -762,7 +758,7 @@ A single byte representing whether groups are transmitted in ascending (0x1) or
762758
The publisher SHOULD transmit *older* groups first during congestion if true.
763759
See the [Prioritization](#prioritization) section for more information.
764760

765-
**Subscriber Max Latency**:
761+
**Max Latency**:
766762
The subscriber's preference, in milliseconds, for how long a non-latest group may remain in flight before being considered stale and dropped from live delivery.
767763
The publisher SHOULD reset (at the QUIC level) Group Streams for groups whose age relative to the latest group exceeds this duration.
768764
Applies only to non-latest groups; the latest group is never dropped on staleness grounds.
@@ -791,7 +787,7 @@ SUBSCRIBE_UPDATE Message {
791787
Message Length (i)
792788
Subscriber Priority (8)
793789
Subscriber Ordered (8)
794-
Subscriber Max Latency (i)
790+
Max Latency (i)
795791
Group Start (i)
796792
Group End (i)
797793
}
@@ -827,8 +823,8 @@ TRACK_INFO Message {
827823
Message Length (i)
828824
Publisher Priority (8)
829825
Publisher Ordered (8)
830-
Publisher Timescale (i)
831-
Publisher Compression (i)
826+
Timescale (i)
827+
Compression (i)
832828
}
833829
~~~
834830

@@ -845,13 +841,13 @@ See the [Prioritization](#prioritization) section for more information.
845841
The publisher's group ordering preference (ascending `0x1` or descending `0x0`), used only to resolve ties.
846842
See the [Prioritization](#prioritization) section for more information.
847843

848-
**Publisher Timescale**:
844+
**Timescale**:
849845
The number of timestamp units per second for frame timestamps on this Track.
850-
A value of 0 means unspecified; the subscriber MUST treat per-frame timestamps as opaque and fall back to wall-clock arrival time for [expiration](#expiration).
851-
When `Publisher Timescale` is 0, the per-frame `Timestamp Delta` field is omitted from FRAME messages and the `Timestamp` field is omitted from datagram bodies (see [FRAME](#frame) and [Datagrams](#datagrams)).
846+
It MUST be non-zero: every Track has a media timeline, so every FRAME carries a `Timestamp Delta` and every datagram body carries a `Timestamp` (see [FRAME](#frame) and [Datagrams](#datagrams)).
847+
A subscriber that receives a `Timescale` of 0 MUST reset the Subscribe or Fetch stream with a protocol violation.
852848
Common values include `1000` (milliseconds), `1000000` (microseconds), `48000` (audio sample rate), and `90000` (RTP video clock).
853849

854-
**Publisher Compression**:
850+
**Compression**:
855851
The compression algorithm applied to every Frame `Payload` on this Track.
856852

857853
- `none` (0): payloads are transmitted verbatim (default).
@@ -1034,15 +1030,12 @@ The FRAME message is a payload within a group.
10341030

10351031
~~~
10361032
FRAME Message {
1037-
[Timestamp Delta (i)]
1033+
Timestamp Delta (i)
10381034
Message Length (i)
10391035
Payload (b)
10401036
}
10411037
~~~
10421038

1043-
`Timestamp Delta` is present only when the Track's `Publisher Timescale` (see [TRACK_INFO](#track-info)) is non-zero.
1044-
When `Publisher Timescale` is 0, the field is omitted from the wire and the FRAME consists of just `Message Length` and `Payload`.
1045-
10461039
**Timestamp Delta**:
10471040
A signed delta from the previous frame's timestamp, in the Track's negotiated `Timescale`.
10481041
Encoded as a zigzag-mapped variable-length integer:
@@ -1055,7 +1048,7 @@ The first frame of a group is delta-encoded from `0`, so its `Timestamp Delta` i
10551048

10561049
**Payload**:
10571050
An application-specific payload.
1058-
If the Track's `Publisher Compression` is non-zero, the payload is compressed using the negotiated algorithm (see [TRACK_INFO](#track-info)) and the `Message Length` describes the compressed size.
1051+
If the Track's `Compression` is non-zero, the payload is compressed using the negotiated algorithm (see [TRACK_INFO](#track-info)) and the `Message Length` describes the compressed size.
10591052
A generic library or relay MUST NOT inspect or modify the decompressed contents unless otherwise negotiated; recompression that preserves the decompressed bytes exactly is allowed (see [TRACK_INFO](#track-info)).
10601053

10611054

@@ -1066,20 +1059,21 @@ A generic library or relay MUST NOT inspect or modify the decompressed contents
10661059
- Renamed ANNOUNCE_INTEREST to ANNOUNCE_REQUEST (the subscriber's request to receive announcements) and ANNOUNCE to ANNOUNCE_BROADCAST (the publisher's per-broadcast advertisement). ANNOUNCE_OK is unchanged. Wire format otherwise unchanged.
10671060
- Added a SETUP message, sent once on a unidirectional Setup Stream (0x1) at the start of the session and FIN'd immediately. It carries a list of Setup Parameters for negotiating optional capabilities and extensions per-hop, replacing the prior stream-probing approach (version is still negotiated via ALPN, not SETUP). Endpoints keep exchanging non-Setup streams without waiting for SETUP, buffering only a stream whose encoding a negotiated extension would change; unknown stream types are still reset as a fallback.
10681061
- Added a SETUP `Probe` parameter advertising the publisher's capability level: `None`, `Report` (measure and report the estimated bitrate), or `Increase` (additionally pad to probe for bandwidth above the current sending rate). The levels are nested since probing without measuring is meaningless. A subscriber must not rely on a level the publisher did not advertise.
1069-
- Added a Track Stream (0x6): a TRACK request that the publisher answers with a single TRACK_INFO message and then FINs. TRACK_INFO carries the Track's immutable publisher properties (`Publisher Priority`, `Publisher Ordered`, `Publisher Timescale`, `Publisher Compression`). It is fetched once and cached, so the properties are no longer echoed on every response — notably, group-by-group FETCHes reuse one lookup.
1062+
- Added a Track Stream (0x6): a TRACK request that the publisher answers with a single TRACK_INFO message and then FINs. TRACK_INFO carries the Track's immutable publisher properties (`Publisher Priority`, `Publisher Ordered`, `Timescale`, `Compression`). It is fetched once and cached, so the properties are no longer echoed on every response — notably, group-by-group FETCHes reuse one lookup.
10701063
- Removed FETCH_OK and trimmed SUBSCRIBE_OK down to a single resolved start group. Publisher properties moved to TRACK_INFO; a FETCH returns bare FRAME messages. All publisher properties are immutable for the lifetime of the Track — a publisher-side change would otherwise have to fan *out* to every downstream of a relay, whereas subscriber properties fan *in* and may still change via SUBSCRIBE_UPDATE.
10711064
- Split the resolved group range across SUBSCRIBE_OK and a new SUBSCRIBE_END. SUBSCRIBE_OK resolves the absolute start (`>=` the requested start; a larger value implicitly drops the leading range), and SUBSCRIBE_END signals that no group will follow a given sequence (stragglers within the range may still be dropped before FIN). SUBSCRIBE_OK keeps the MoqTransport name and its role as the publisher's positive response.
10721065
- Renamed `Start Group`/`End Group` to `Group Start`/`Group End` in SUBSCRIBE, SUBSCRIBE_UPDATE, and SUBSCRIBE_DROP for consistency with the entity-first naming used elsewhere (e.g. `Group Sequence`). Wire format unchanged.
10731066
- Allowed a duplicate `active` ANNOUNCE_BROADCAST to atomically replace the prior advertisement (equivalent to UNANNOUNCE+ANNOUNCE_BROADCAST). Used when only the origin or hop path changes (e.g. relay failover) without interrupting the broadcast. No new wire enum value — the existing `active` status carries the new metadata.
10741067
- Added ANNOUNCE_OK message, sent once at the head of the Announce Stream response. Carries the publisher's `Hop ID` (hoisted out of every ANNOUNCE_BROADCAST's Hop ID list) and an `Active Count` so subscribers can batch the initial set instead of reporting each ANNOUNCE_BROADCAST as it trickles in.
10751068
- Encoded `Hop ID` (in ANNOUNCE_BROADCAST and ANNOUNCE_OK) and `Exclude Hop` (in ANNOUNCE_REQUEST) as fixed-width 64-bit integers instead of varints. Hop IDs are random, so a varint would almost never be shorter, and the fixed width restores the 2 bits a 62-bit varint would have cost.
1076-
- Added `Publisher Timescale` to TRACK_INFO for per-track timestamp negotiation. When `Publisher Timescale` is 0, the per-frame timestamp field is omitted entirely from FRAME and datagram bodies.
1077-
- Added `Timestamp Delta` to FRAME, a zigzag-encoded signed varint (present only when timescale is non-zero).
1078-
- Added `Timestamp` to the QUIC datagram body (absolute, present only when timescale is non-zero).
1069+
- Added a mandatory `Timescale` to TRACK_INFO: the units (ticks per second) for every frame timestamp on the Track. It MUST be non-zero — every Track has a media timeline, so the timestamp fields are never conditional on the wire.
1070+
- Added `Timestamp Delta` to FRAME, a zigzag-encoded signed varint delta from the previous frame's timestamp (the first frame's delta is its absolute timestamp).
1071+
- Added `Timestamp` to the QUIC datagram body: the absolute timestamp of the group's single frame.
10791072
- Removed `Publisher Max Latency`. The publisher's retention guarantee is no longer part of the wire format; retention for FETCH and future subscriptions is best-effort and left to the publisher.
1080-
- Timestamp-based expiration replaces wall-clock arrival time when a Track timescale is negotiated.
1073+
- Timestamp-based expiration replaces wall-clock arrival time; only empty groups (which carry no timestamp) fall back to wall-clock.
10811074
- Added QUIC datagram delivery for groups, sharing Subscribe IDs with existing subscriptions (no separate control stream).
1082-
- Added `Publisher Compression` to TRACK_INFO for per-frame payload compression (`none` or `deflate`).
1075+
- Added `Compression` to TRACK_INFO for per-frame payload compression (`none` or `deflate`).
1076+
- Dropped the `Publisher`/`Subscriber` prefix from fields that exist on only one side: `Publisher Compression` → `Compression` and `Subscriber Max Latency` → `Max Latency`. `Priority` and `Ordered` keep the prefix since both a publisher and a subscriber variant exist and the prose distinguishes them. Wire format unchanged.
10831077
- Added Qmux [qmux] transport bindings for TCP/TLS and WebSocket, for environments where UDP is unavailable. The WebSocket binding uses the WebSocket message framing in place of the Qmux Record `Size` field.
10841078

10851079
## moq-lite-04

0 commit comments

Comments
 (0)