You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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>
Copy file name to clipboardExpand all lines: draft-lcurley-moq-lite.md
+28-34Lines changed: 28 additions & 34 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -159,8 +159,8 @@ A frame is used to represent a chunk of data with an upfront size.
159
159
The contents are opaque to the moq-lite layer.
160
160
161
161
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.
162
163
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.
164
164
165
165
# Flow
166
166
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
309
309
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.
310
310
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.
311
311
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.
313
313
314
314
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)).
315
315
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
419
419
An application SHOULD NOT use `ordered` when it wants to provide a live experience, preferring to skip old groups rather than buffer them.
420
420
421
421
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`.
423
423
An application MUST support gaps and out-of-order delivery even when `ordered` is true.
424
424
425
425
@@ -431,20 +431,19 @@ It is not crucial to aggressively expire groups thanks to [prioritization](#prio
431
431
However, a lower priority group will still consume RAM, bandwidth, and potentially flow control.
432
432
It is RECOMMENDED that an application set conservative limits and only resort to expiration when data is absolutely no longer needed.
433
433
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.
435
435
The subscriber MAY also locally drop such groups for its own resource accounting.
436
436
Expiration only removes the group from the live subscription's stream; the publisher MAY still retain it for FETCH or new subscriptions.
437
437
438
438
Group age is computed relative to the latest group by sequence number.
439
439
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`.
441
441
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.
445
444
446
445
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).
448
447
This avoids stalling expiration on tracks that intentionally emit empty groups as keep-alives or gap markers.
449
448
450
449
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
497
496
DATAGRAM Body {
498
497
Subscribe ID (i)
499
498
Group Sequence (i)
500
-
[Timestamp (i)]
499
+
Timestamp (i)
501
500
Payload (b)
502
501
}
503
502
~~~
504
503
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
-
508
504
**Subscribe ID**:
509
505
The Subscribe ID of an active subscription on the same session.
510
506
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.
519
515
520
516
**Payload**:
521
517
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)).
523
519
The total datagram body (including all header fields above and the compressed payload if applicable) MUST NOT exceed 1200 bytes.
524
520
This limit ensures the datagram fits within the minimum QUIC path MTU without IP-layer fragmentation.
525
521
Payloads that would not fit MUST be sent as a Group Stream instead.
@@ -742,7 +738,7 @@ SUBSCRIBE Message {
742
738
Track Name (s)
743
739
Subscriber Priority (8)
744
740
Subscriber Ordered (8)
745
-
Subscriber Max Latency (i)
741
+
Max Latency (i)
746
742
Group Start (i)
747
743
Group End (i)
748
744
}
@@ -762,7 +758,7 @@ A single byte representing whether groups are transmitted in ascending (0x1) or
762
758
The publisher SHOULD transmit *older* groups first during congestion if true.
763
759
See the [Prioritization](#prioritization) section for more information.
764
760
765
-
**Subscriber Max Latency**:
761
+
**Max Latency**:
766
762
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.
767
763
The publisher SHOULD reset (at the QUIC level) Group Streams for groups whose age relative to the latest group exceeds this duration.
768
764
Applies only to non-latest groups; the latest group is never dropped on staleness grounds.
@@ -791,7 +787,7 @@ SUBSCRIBE_UPDATE Message {
791
787
Message Length (i)
792
788
Subscriber Priority (8)
793
789
Subscriber Ordered (8)
794
-
Subscriber Max Latency (i)
790
+
Max Latency (i)
795
791
Group Start (i)
796
792
Group End (i)
797
793
}
@@ -827,8 +823,8 @@ TRACK_INFO Message {
827
823
Message Length (i)
828
824
Publisher Priority (8)
829
825
Publisher Ordered (8)
830
-
Publisher Timescale (i)
831
-
Publisher Compression (i)
826
+
Timescale (i)
827
+
Compression (i)
832
828
}
833
829
~~~
834
830
@@ -845,13 +841,13 @@ See the [Prioritization](#prioritization) section for more information.
845
841
The publisher's group ordering preference (ascending `0x1` or descending `0x0`), used only to resolve ties.
846
842
See the [Prioritization](#prioritization) section for more information.
847
843
848
-
**Publisher Timescale**:
844
+
**Timescale**:
849
845
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.
852
848
Common values include `1000` (milliseconds), `1000000` (microseconds), `48000` (audio sample rate), and `90000` (RTP video clock).
853
849
854
-
**Publisher Compression**:
850
+
**Compression**:
855
851
The compression algorithm applied to every Frame `Payload` on this Track.
856
852
857
853
- `none` (0): payloads are transmitted verbatim (default).
@@ -1034,15 +1030,12 @@ The FRAME message is a payload within a group.
1034
1030
1035
1031
~~~
1036
1032
FRAME Message {
1037
-
[Timestamp Delta (i)]
1033
+
Timestamp Delta (i)
1038
1034
Message Length (i)
1039
1035
Payload (b)
1040
1036
}
1041
1037
~~~
1042
1038
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
-
1046
1039
**Timestamp Delta**:
1047
1040
A signed delta from the previous frame's timestamp, in the Track's negotiated `Timescale`.
1048
1041
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
1055
1048
1056
1049
**Payload**:
1057
1050
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.
1059
1052
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)).
1060
1053
1061
1054
@@ -1066,20 +1059,21 @@ A generic library or relay MUST NOT inspect or modify the decompressed contents
1066
1059
- 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.
1067
1060
- 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.
1068
1061
- 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.
1070
1063
- 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.
1071
1064
- 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.
1072
1065
- 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.
1073
1066
- 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.
1074
1067
- 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.
1075
1068
- 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.
1079
1072
- 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.
1081
1074
- 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.
1083
1077
- 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.
0 commit comments