From 33fc2934e8f21f08debaf79e3877c5dfbf432c9e Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sat, 10 Jan 2026 21:11:15 +1100 Subject: [PATCH 01/12] Draft-03: Add delivery properties --- draft-lcurley-moq-lite.md | 192 ++++++++++++++++++++++++++++++++++---- 1 file changed, 173 insertions(+), 19 deletions(-) diff --git a/draft-lcurley-moq-lite.md b/draft-lcurley-moq-lite.md index 4111662..4791a55 100644 --- a/draft-lcurley-moq-lite.md +++ b/draft-lcurley-moq-lite.md @@ -91,7 +91,7 @@ The subscriber uses the ANNOUNCE_PLEASE message to discover available broadcasts These announcements are live and can change over time, allowing for dynamic origin discovery. A broadcast consists of any number of Tracks. -The contents and relationship between these tracks is determined by the application or via an out-of-band mechanism. +The contents, relationships, and encoding of tracks are determined by the application. ## Track A Track is a series of Groups identified by a unique name within a Broadcast. @@ -103,8 +103,12 @@ The duration before an incomplete group is dropped is determined by the applicat Every subscription is scoped to a single Track. A subscription will always start at the latest Group and continues until either the publisher or subscriber cancels the subscription. -A subscriber chooses the priority of each subscription, hinting to the publisher which Track should arrive first during congestion. -This enables the most important content to arrive during network degradation while still respecting encoding dependencies. +The subscriber and publisher both indicate their delivery preference: +- `Priority` indicates if Track A should be transmitted instead of Track B. +- `Ordered` indicates if the Groups within a Track should be transmitted in order. +- `Max Latency` indicates the maximum duration (based on instants) before a Group is abandoned. + +The combination of these preferences enables the most important content to arrive during network degradation while still respecting encoding dependencies. ## Group A Group is an ordered stream of Frames within a Track. @@ -114,16 +118,14 @@ A Group is served by a dedicated QUIC stream which is closed on completion, rese This ensures that all Frames within a Group arrive reliably and in order. In contrast, Groups may arrive out of order due to network congestion and prioritization. -The application should be prepared to handle this with a jitter buffer at the group level. +The application MUST process or buffer groups out of order to avoid blocking on flow control ## Frame A Frame is a payload of bytes within a Group. -A frame is used to represent a chunk of data with a known size. -A frame should represent a single moment in time and avoid any buffering that would increase latency. - -There's no timestamp or metadata associated with a Frame, this is the responsibility of the application. - +A frame is used to represent a chunk of data with an upfront size and [instant](#instant). +The instant is relative to all other frames within the same Track, used for [expiration](#expiration) and metrics. +The application MAY use the instant as a replacement for a presentation timestamp, used for track playback and synchronization. # Flow This section outlines the flow of messages within a moq-lite session. @@ -158,7 +160,6 @@ The session is active until either endpoint closes or resets the Session Stream. This session handshake is used to negotiate the moq-lite version and any extensions. See the Extension section for more information. - # Streams moq-lite uses a bidirectional stream for each transaction. If the stream is closed, potentially with an error, the transaction is terminated. @@ -231,17 +232,136 @@ There MAY be multiple Announce Streams, potentially containing overlapping prefi A subscriber opens Subscribe Streams to request a Track. The subscriber MUST start a Subscribe Stream with a SUBSCRIBE message followed by any number of SUBSCRIBE_UPDATE messages. -The publisher MUST reply with an SUBSCRIBE_OK message. +The publisher replies with any number of SUBSCRIBE_OK messages, or closes the stream. The publisher SHOULD close the stream after the track has ended. Either endpoint MAY reset/cancel the stream at any time. +# Delivery +The most important concept in moq-lite is how to deliver a subscription. +QUIC can only improve the user experience if data is delivered out-of-order during congestion. +This is the sole reason why data is divided into Broadcasts, Tracks, Groups, and Frames. + +moq-lite consists of multiple groups being transmitted in parallel across seperate streams. +How these streams get transmitted over the network is very important, and yet has been distilled down into a few simple properties: + +## Prioritization +The Publisher and Subscriber both exhchange `Priority` and `Ordered` values: +- `Priority` determines which Track should be transmitted next. +- `Ordered` determines when Group within the Track should be transmitted next. + +A publisher SHOULD attempt to transmit streams based on these fields. +This depends on the QUIC library and it may not be possible to get fine-grained control. + +### Priority +The `Subscriber Priority` is scoped to the connection. +The `Publisher Priority` SHOULD be used to resolve conflicts or ties. + +A conflict can occur when a relay tries to serve multiple downstream subscriptions from a single upstream subscription. +Any upstream subscription SHOULD use the publisher priority, not some combination of different subscriber priorities. + +Rather than try to explain everything, here's an example: + +**Example:** +There are two people in a conference call, Ali and Bob. + +We subscribe to both of their audio tracks with priority 2 and video tracks with priority 1. +This will cause equal priority for `Ali` and `Bob` while prioritizing audio. +``` +ali/audio + bob/audio: subscriber_priority=2 publisher_priority=2 +ali/video + bob/video: subscriber_priority=1 publisher_priority=1 +``` + +If Bob starts actively speaking, they can bump their publisher priority via a SUBSCRIBE_OK message. +This would cause tracks be delivered in this order: +``` +bob/audio: subscriber_priority=2 publisher_priority=3 +ali/audio: subscriber_priority=2 publisher_priority=2 +bob/video: subscriber_priority=1 publisher_priority=2 +ali/video: subscriber_priority=1 publisher_priority=1 +``` + +The subscriber priority takes presidence, so we could override it if we decided to full screen Ali's window: +``` +ali/audio subscriber_priority=4 publisher_priority=2 +ali/video subscriber_priority=3 publisher_priority=1 +bob/audio subscriber_priority=2 publisher_priority=3 +bob/audio subscriber_priority=1 publisher_priority=2 +``` + +### Ordered +The `Subscriber Ordered` boolean signals if older (true) or newer (false) groups should be transmitted first within a Track. +The `Publisher Ordered` boolean MAY likewise be used to resolve conflicts. + +An application SHOULD use `ordered` when it wants to provide a VOD-like experience, preferring to buffer old groups rather than skip them. +An application SHOULD NOT `ordered` when it wants to provide a live experience, preferring to skip old groups rather than buffer them. + +Note that (expiration)[#expiration] is not affected by `ordered`. +An old group may still be cancelled/skipped if it's older than `max_latency` set by either peer. +An application MUST support gaps and out-of-order delivery even when `ordered` is true. + + +## Instant +Every Frame consists of an Instant (in milliseconds) scoped to the track. + +This is used for [Expiration](#expiration) at the moq-lite layer. +The instant MAY be used for track synchronization at the application layer, however many containers/formats already contain their own timestamps. +A publisher SHOULD try to preserve the instant when the frame was first created/captured, proxying it. + +Each frame within a group MUST have a monotonically increasing instant. +If the presentation timestamp goes backwards (ex. b-frames), a frame's Instant SHOULD be the maximum presentation timestamp of the group up until that point. +Duplicates are allowed, encoded as delta 0. +Unless specified by the application, a subscriber: +- SHOULD NOT assume that an instant is a wall clock time. +- SHOULD NOT assume that an instant starts at zero. + +Clock synchronization is out of scope for this draft. + +## Expiration +The Publisher and Subscriber both transmit a `Max Latency` value, indicating the maximum duration before a group is expired. + +It is not crucial to aggressively expire groups thanks to [prioritization](#prioritization). +However, a lower priority group will still consume RAM, bandwidth, and potentially flow control. +It is RECOMMENDED that an application set conservative limits and only resort to expiration when data is absolutely no longer needed. + +A subscriber SHOULD expire groups based on the `Subscriber Max Latency` in SUBSCRIBE/SUBSCRIBE_UPDATE. +A publisher SHOULD expire groups based on the `Publisher Max Latency` in SUBSCRIBE_OK. +An implementation MAY use the minimum of both when determining when to expire a group. + +Each group consists of a maximum Frame Instant, the meaning of "processed" depending on the endpoint: +- A publisher uses a frame was queued, regardless of it it has been flushed to the QUIC layer. +- A subscriber uses when a frame was received, regardless of it it has been flushed to the application. + +The entire track consists of a maximum Frame Instant using the same logic as above. +For each group, if the maximum Frame Instant is more than `Max Latency` behind the track's maximum Frame Instant, then the group is considered expired. +An expired group SHOULD BE reset at the QUIC level to avoid consuming flow control. + + +**Example:** +- Group 1: 1000 1500 (2000) +- Group 2: 2500 3000 + +Frame 2000 in this example has not been received yet by the subscriber. +If the `max_latency` was 1250, then the Subscriber SHOULD expire Group 1 but the Publisher SHOULD NOT yet. +The opposite would be true if Frame 3000 was still in transit: + +**Example:** +- Group 1: 1000 1500 2000 +- Group 2: 2500 (3000) + +**NOTE**: Individual frames within a group cannot be cancelled. +Even if `max_latency` was 0, Group 2 would not be expired until a (higher) frame in Group 3 is queued/received. + + +An implementation MAY use the broadcast's maximum Frame Instant instead of the track's maximum Frame Instant. +This is useful to account for encoding delays, ex. when video takes 300ms longer to encode than audio. +However, it SHOULD NOT expire the highest sequence number group within each track, otherwise a track may become perpetually expired. ## Unidirectional Streams Unidirectional streams are used for data transmission. -|------|--------|-----------| +|--------|----------|-------------| | ID | Stream | Creator | |-------:|:---------|-------------| | 0x0 | Group | Publisher | @@ -259,6 +379,7 @@ This is not a fatal error and the session remains active. The subscriber MAY cache the error and potentially retry later. + # Encoding This section covers the encoding of each message. @@ -410,7 +531,9 @@ SUBSCRIBE Message { Subscribe ID (i) Broadcast Path (s) Track Name (s) - Subscriber Priority (i) + Subscriber Priority (8) + Subscriber Ordered (1) + Subscriber Max Latency (i) } ~~~ @@ -419,34 +542,51 @@ A unique identifier chosen by the subscriber. A Subscribe ID MUST NOT be reused within the same session, even if the prior subscription has been closed. **Subscriber Priority**: -The transmission priority of the subscription relative to all other active subscriptions within the session. +The priority of the subscription within the session, represented as a u8. The publisher SHOULD transmit *higher* values first during congestion. +See the [Prioritization](#prioritization) section for more information. + +**Subscriber Ordered**: +A boolean representing whether groups are transmitted in ascending (true) or descending (false) order. +The publisher SHOULD transmit *older* groups first during congestion if true. +See the [Prioritization](#prioritization) section for more information. + +**Subscriber Max Latency**: +This value is encoded in milliseconds and represents the maximum age of a group. +The publisher SHOULD reset old group streams when the last processed frame is at least this much older than the newest frame. +See the [Expiration](#expiration) section for more information. ## SUBSCRIBE_UPDATE A subscriber can modify a subscription with a SUBSCRIBE_UPDATE message. +A subscriber MAY send multiple SUBSCRIBE_UPDATE messages to update the subscription. ~~~ SUBSCRIBE_UPDATE Message { Message Length (i) Subscriber Priority (i) + Subscriber Ordered (1) + Subscriber Max Latency (i) } ~~~ -**Subscriber Priority**: -The new subscriber priority; see SUBSCRIBE. +See [SUBSCRIBE](#subscribe) for information about each field. ## SUBSCRIBE_OK -The SUBSCRIBE_OK is sent in response to a SUBSCRIBE. +A SUBSCRIBE_OK message is sent in response to a SUBSCRIBE. +The publisher MAY send multiple SUBSCRIBE_OK messages to update the subscription. ~~~ SUBSCRIBE_OK Message { - Message Length = 0 + Message Length (i) + Publisher Priority (8) + Publisher Ordered (1) + Publisher Max Latency (i) } ~~~ -That's right, it's an empty message at the moment. +See [SUBSCRIBE](#subscribe) for information about each field. ## GROUP The GROUP message contains information about a Group, as well as a reference to the subscription being served. @@ -466,6 +606,7 @@ This ID is used to distinguish between multiple subscriptions for the same track **Group Sequence**: The sequence number of the group. This SHOULD increase by 1 for each new group. +A subscriber MUST handle gaps, potentially caused by congestion. ## FRAME @@ -474,10 +615,17 @@ The FRAME message is a payload at a specific point of time. ~~~ FRAME Message { Message Length (i) + Instant Delta (i) Payload (b) } ~~~ +**Instant Delta**: +The instant when the frame was created, in milliseconds, scoped to the track. +This is encoded as a delta from the previous frame in the same group. +An application SHOULD use the capture/presentation time to account for any encoding delays. +See the [Instant](#instant) section for more information. + **Payload**: An application specific payload. A generic library or relay MUST NOT inspect or modify the contents unless otherwise negotiated. @@ -485,6 +633,12 @@ A generic library or relay MUST NOT inspect or modify the contents unless otherw # Appendix A: Changelog +## moq-lite-03 +- Added `Subscriber Max Latency` and `Subscriber Ordered` to SUBSCRIBE and SUBSCRIBE_UPDATE. +- Added `Publisher Priority`, `Publisher Max Latency`, and `Publisher Ordered` to SUBSCRIBE_OK. +- Added `Instant Delta` to FRAME. +- SUBSCRIBE_OK may be sent multiple times. + ## moq-lite-02 - Added SessionCompat stream. - Editorial stuff. From 17379e46cdf2fee741e3a30ab2b4eb708a6e88b5 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sat, 10 Jan 2026 21:22:52 +1100 Subject: [PATCH 02/12] AI review. --- draft-lcurley-moq-lite.md | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/draft-lcurley-moq-lite.md b/draft-lcurley-moq-lite.md index 4791a55..c8c3a58 100644 --- a/draft-lcurley-moq-lite.md +++ b/draft-lcurley-moq-lite.md @@ -118,7 +118,7 @@ A Group is served by a dedicated QUIC stream which is closed on completion, rese This ensures that all Frames within a Group arrive reliably and in order. In contrast, Groups may arrive out of order due to network congestion and prioritization. -The application MUST process or buffer groups out of order to avoid blocking on flow control +The application MUST process or buffer groups out of order to avoid blocking on flow control. ## Frame A Frame is a payload of bytes within a Group. @@ -242,16 +242,16 @@ The most important concept in moq-lite is how to deliver a subscription. QUIC can only improve the user experience if data is delivered out-of-order during congestion. This is the sole reason why data is divided into Broadcasts, Tracks, Groups, and Frames. -moq-lite consists of multiple groups being transmitted in parallel across seperate streams. +moq-lite consists of multiple groups being transmitted in parallel across separate streams. How these streams get transmitted over the network is very important, and yet has been distilled down into a few simple properties: ## Prioritization -The Publisher and Subscriber both exhchange `Priority` and `Ordered` values: +The Publisher and Subscriber both exchange `Priority` and `Ordered` values: - `Priority` determines which Track should be transmitted next. -- `Ordered` determines when Group within the Track should be transmitted next. +- `Ordered` determines which Group within the Track should be transmitted next. A publisher SHOULD attempt to transmit streams based on these fields. -This depends on the QUIC library and it may not be possible to get fine-grained control. +This depends on the QUIC implementation and it may not be possible to get fine-grained control. ### Priority The `Subscriber Priority` is scoped to the connection. @@ -281,12 +281,12 @@ bob/video: subscriber_priority=1 publisher_priority=2 ali/video: subscriber_priority=1 publisher_priority=1 ``` -The subscriber priority takes presidence, so we could override it if we decided to full screen Ali's window: +The subscriber priority takes precedence, so we could override it if we decided to full screen Ali's window: ``` ali/audio subscriber_priority=4 publisher_priority=2 ali/video subscriber_priority=3 publisher_priority=1 bob/audio subscriber_priority=2 publisher_priority=3 -bob/audio subscriber_priority=1 publisher_priority=2 +bob/video subscriber_priority=1 publisher_priority=2 ``` ### Ordered @@ -294,9 +294,9 @@ The `Subscriber Ordered` boolean signals if older (true) or newer (false) groups The `Publisher Ordered` boolean MAY likewise be used to resolve conflicts. An application SHOULD use `ordered` when it wants to provide a VOD-like experience, preferring to buffer old groups rather than skip them. -An application SHOULD NOT `ordered` when it wants to provide a live experience, preferring to skip old groups rather than buffer them. +An application SHOULD NOT use `ordered` when it wants to provide a live experience, preferring to skip old groups rather than buffer them. -Note that (expiration)[#expiration] is not affected by `ordered`. +Note that [expiration](#expiration) is not affected by `ordered`. An old group may still be cancelled/skipped if it's older than `max_latency` set by either peer. An application MUST support gaps and out-of-order delivery even when `ordered` is true. @@ -330,8 +330,8 @@ A publisher SHOULD expire groups based on the `Publisher Max Latency` in SUBSCRI An implementation MAY use the minimum of both when determining when to expire a group. Each group consists of a maximum Frame Instant, the meaning of "processed" depending on the endpoint: -- A publisher uses a frame was queued, regardless of it it has been flushed to the QUIC layer. -- A subscriber uses when a frame was received, regardless of it it has been flushed to the application. +- A publisher uses when a frame was queued, regardless of if it has been flushed to the QUIC layer. +- A subscriber uses when a frame was received, regardless of if it has been flushed to the application. The entire track consists of a maximum Frame Instant using the same logic as above. For each group, if the maximum Frame Instant is more than `Max Latency` behind the track's maximum Frame Instant, then the group is considered expired. @@ -342,9 +342,12 @@ An expired group SHOULD BE reset at the QUIC level to avoid consuming flow contr - Group 1: 1000 1500 (2000) - Group 2: 2500 3000 -Frame 2000 in this example has not been received yet by the subscriber. -If the `max_latency` was 1250, then the Subscriber SHOULD expire Group 1 but the Publisher SHOULD NOT yet. -The opposite would be true if Frame 3000 was still in transit: +In this example, frame 2000 has been queued by the publisher but not yet received by the subscriber (shown in parentheses). +If `max_latency` is 1250: +- **Subscriber**: Track max is 3000, Group 1 max received is 1500. Delta = 1500ms > 1250ms → SHOULD expire Group 1 +- **Publisher**: Track max is 3000, Group 1 max queued is 2000. Delta = 1000ms < 1250ms → SHOULD NOT expire Group 1 + +The opposite would be true if frame 3000 was still in transit: **Example:** - Group 1: 1000 1500 2000 @@ -564,7 +567,7 @@ A subscriber MAY send multiple SUBSCRIBE_UPDATE messages to update the subscript ~~~ SUBSCRIBE_UPDATE Message { Message Length (i) - Subscriber Priority (i) + Subscriber Priority (8) Subscriber Ordered (1) Subscriber Max Latency (i) } From 56487bc591e4284487fc5d3959378d8f884f3ead Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Mon, 23 Feb 2026 19:40:30 -0800 Subject: [PATCH 03/12] Draft-03: ALPN handshake, FETCH, PROBE, group ranges, GROUP_DROP Replace SETUP handshake with ALPN-based version negotiation (moq-lite-xx). Add FETCH stream for single group download, PROBE stream for bitrate measurement, start/end group ranges to subscriptions, and GROUP_DROP for indicating unavailable groups. Unknown stream types are now non-fatal to enable extension negotiation via stream probing. Co-Authored-By: Claude Opus 4.6 --- draft-lcurley-moq-lite.md | 235 +++++++++++++++++++++++--------------- 1 file changed, 140 insertions(+), 95 deletions(-) diff --git a/draft-lcurley-moq-lite.md b/draft-lcurley-moq-lite.md index c8c3a58..4309591 100644 --- a/draft-lcurley-moq-lite.md +++ b/draft-lcurley-moq-lite.md @@ -75,8 +75,13 @@ The moq-lite layer provides fanout, prioritization, and caching even for latency A Session consists of a connection between a client and a server. There is currently no P2P support within QUIC so it's out of scope for moq-lite. -A session is established after the necessary QUIC, WebTransport, and moq-lite handshakes have completed. -The moq-lite handshake is simple and consists of version and extension negotiation. +The moq-lite version is negotiated via ALPN during the QUIC handshake. +The ALPN format is `moq-lite-xx` where `xx` is the two-digit draft version. +The latest ALPN is `moq-lite-03`. + +The session is active immediately after the QUIC/WebTransport connection is established. +No additional handshake is required. +Extensions are negotiated via stream probing: an endpoint opens a stream with an unknown type and the peer resets it if unsupported. While moq-lite is a point-to-point protocol, it's intended to work end-to-end via relays. Each client establishes a session with a CDN edge server, ideally the closest one. @@ -101,7 +106,7 @@ When a new Group is started, the previous Group is closed and may be dropped for The duration before an incomplete group is dropped is determined by the application and the publisher/subscriber's latency target. Every subscription is scoped to a single Track. -A subscription will always start at the latest Group and continues until either the publisher or subscriber cancels the subscription. +A subscription starts at a configurable Group (defaulting to the latest) and continues until a configurable end Group or until either the publisher or subscriber cancels the subscription. The subscriber and publisher both indicate their delivery preference: - `Priority` indicates if Track A should be transmitted instead of Track B. @@ -154,11 +159,9 @@ After resetting the send direction, an endpoint MAY close the recv direction (ST However, it is ultimately the other peer's responsibility to close their send direction. ## Handshake -After a connection is established, the client opens a Session Stream and sends a SESSION_CLIENT message, to which the server replies with a SESSION_SERVER message. -The session is active until either endpoint closes or resets the Session Stream. - -This session handshake is used to negotiate the moq-lite version and any extensions. -See the Extension section for more information. +The moq-lite version is negotiated via ALPN during the QUIC handshake. +The ALPN format is `moq-lite-xx` where `xx` is the two-digit draft version (latest: `moq-lite-03`). +The session is active immediately after the connection is established. # Streams moq-lite uses a bidirectional stream for each transaction. @@ -171,41 +174,15 @@ There's a 1-byte STREAM_TYPE at the beginning of each stream. |---------|--------------|-------------| | ID | Stream | Creator | |--------:|:-------------|:------------| -| 0x0 | Session | Client | -| ------- | ------------ | ----------- | | 0x1 | Announce | Subscriber | -| ------- | ------------- | ---------- | +| ------- | ------------ | ----------- | | 0x2 | Subscribe | Subscriber | | ------- | ------------- | ---------- | -| 0x20 | SessionCompat | Client | +| 0x3 | Fetch | Subscriber | +| ------- | ------------- | ---------- | +| 0x4 | Probe | Subscriber | | ------- | ------------- | ----------- | -### Session -The Session stream is used to establish the moq-lite session, negotiating the version and any extensions. -This stream remains open for the duration of the session and its closure indicates the session is closed. - -The client MUST open the Session Stream, write the Session Stream ID (0x0), and write a SESSION_CLIENT message. -If the server does not support any of the client's versions, it MUST close the stream with an error code and MAY close the connection. -Otherwise, the server replies with a SESSION_SERVER message to complete the handshake. - -Afterwards, both endpoints MAY send SESSION_UPDATE messages. -This is currently used to notify the other endpoint of a significant change in the session bitrate. - -This draft's version is combined with the constant `0xff0dad00`. -For example, moq-lite-draft-04 is identified as `0xff0dad04`. - -### SessionCompat -The SessionCompat stream exists to support moq-transport draft 11-14. -This will be removed in a future version as moq-transport draft 15 uses ALPN instead. - -The client writes a CLIENT_SETUP message on the SessionCompat stream and receives a SERVER_SETUP message in response. - -Consult the MoqTransport ([moqt]) draft for more information about the encoding. -Notably, each message contains a u16 length prefix instead of a VarInt (moq-lite). - -If a moq-lite version is negotiated, this stream becomes a normal Session stream. -If a moq-transport version is negotiated, this stream becomes the MoqTransport control stream. - ### Announce A subscriber can open a Announce Stream to discover broadcasts matching a prefix. @@ -232,11 +209,30 @@ There MAY be multiple Announce Streams, potentially containing overlapping prefi A subscriber opens Subscribe Streams to request a Track. The subscriber MUST start a Subscribe Stream with a SUBSCRIBE message followed by any number of SUBSCRIBE_UPDATE messages. -The publisher replies with any number of SUBSCRIBE_OK messages, or closes the stream. +The publisher replies with a SUBSCRIBE_OK message followed by any number of GROUP_DROP and additional SUBSCRIBE_OK messages. -The publisher SHOULD close the stream after the track has ended. +The publisher closes the stream (FIN) when every group from start to end has been accounted for, either via a completed GROUP stream or a GROUP_DROP message. +Unbounded subscriptions (no end group) stay open until the track ends or either endpoint cancels. Either endpoint MAY reset/cancel the stream at any time. +### Fetch +A subscriber opens a Fetch Stream (0x3) to request a single Group from a Track. + +The subscriber sends a FETCH message containing the broadcast path, track name, priority, and group sequence. +The publisher responds with FRAME messages on the same bidirectional stream. +The publisher FINs the stream after the last frame, or resets the stream on error. + +Fetch behaves like HTTP: a single request/response per stream. + +### Probe +A subscriber opens a Probe Stream (0x4) to measure the available bitrate of the connection. + +The subscriber sends a PROBE message with a target bitrate. +The publisher SHOULD pad the connection to achieve the target bitrate. +The publisher periodically replies with PROBE messages containing the current measured bitrate. + +If the publisher does not support PROBE (e.g., congestion controller is not exposed), it resets the stream. + # Delivery The most important concept in moq-lite is how to deliver a subscription. QUIC can only improve the user experience if data is delivered out-of-order during congestion. @@ -403,54 +399,8 @@ STREAM_TYPE { ~~~ The stream ID depends on if it's a bidirectional or unidirectional stream, as indicated in the Streams section. -A receiver MUST close the session if it receives an unknown stream type. - - -## SESSION_CLIENT -The client initiates the session by sending a SESSION_CLIENT message. - -~~~ -SESSION_CLIENT Message { - Message Length (i) - Supported Versions Count (i) - Supported Version (i) - Extension Count (i) - [ - Extension ID (i) - Extension Payload (b) - ]... -} -~~~ - - -## SESSION_SERVER -The server responds with the selected version and any extensions. - -~~~ -SESSION_SERVER Message { - Message Length (i) - Selected Version (i) - Extension Count (i) - [ - Extension ID (i) - Extension Payload (b) - ]... -} -~~~ - -## SESSION_UPDATE - -~~~ -SESSION_UPDATE Message { - Message Length (i) - Session Bitrate (i) -} -~~~ - -**Session Bitrate**: -The estimated bitrate of the QUIC connection in bits per second. -This SHOULD be sourced directly from the QUIC congestion controller. -A value of 0 indicates that this information is not available. +A receiver MUST reset the stream if it receives an unknown stream type. +Unknown stream types MUST NOT be treated as fatal; this enables extension negotiation via stream probing. ## ANNOUNCE_PLEASE @@ -537,6 +487,8 @@ SUBSCRIBE Message { Subscriber Priority (8) Subscriber Ordered (1) Subscriber Max Latency (i) + Start Group (i) + End Group (i) } ~~~ @@ -559,10 +511,21 @@ This value is encoded in milliseconds and represents the maximum age of a group. The publisher SHOULD reset old group streams when the last processed frame is at least this much older than the newest frame. See the [Expiration](#expiration) section for more information. +**Start Group**: +The first group to deliver. +A value of 0 means the latest group (default). +A non-zero value is the absolute group sequence + 1. + +**End Group**: +The last group to deliver (inclusive). +A value of 0 means unbounded (default). +A non-zero value is the absolute group sequence + 1. + ## SUBSCRIBE_UPDATE A subscriber can modify a subscription with a SUBSCRIBE_UPDATE message. A subscriber MAY send multiple SUBSCRIBE_UPDATE messages to update the subscription. +The start and end group can be changed in either direction (growing or shrinking). ~~~ SUBSCRIBE_UPDATE Message { @@ -570,6 +533,8 @@ SUBSCRIBE_UPDATE Message { Subscriber Priority (8) Subscriber Ordered (1) Subscriber Max Latency (i) + Start Group (i) + End Group (i) } ~~~ @@ -586,10 +551,84 @@ SUBSCRIBE_OK Message { Publisher Priority (8) Publisher Ordered (1) Publisher Max Latency (i) + Start Group (i) + End Group (i) } ~~~ -See [SUBSCRIBE](#subscribe) for information about each field. +**Start Group**: +The resolved absolute start group sequence + 1. +This MUST NOT be 0. + +**End Group**: +The resolved absolute end group sequence + 1, or 0 if unbounded. + +See [SUBSCRIBE](#subscribe) for information about the other fields. + +## GROUP_DROP +A GROUP_DROP message is sent by the publisher on the Subscribe Stream when groups cannot be served. + +~~~ +GROUP_DROP Message { + Message Length (i) + Start Group (i) + End Group (i) + Error Code (i) +} +~~~ + +**Start Group**: +The first group sequence in the dropped range (inclusive, absolute). + +**End Group**: +The last group sequence in the dropped range (inclusive, absolute). + +**Error Code**: +An application-specific error code. +A value of 0 indicates no error; the groups are simply unavailable. + +## FETCH +FETCH is sent by a subscriber to request a single group from a track. + +~~~ +FETCH Message { + Message Length (i) + Broadcast Path (s) + Track Name (s) + Subscriber Priority (8) + Group Sequence (i) +} +~~~ + +**Broadcast Path**: +The broadcast path of the track to fetch from. + +**Track Name**: +The name of the track to fetch from. + +**Subscriber Priority**: +The priority of the fetch within the session, represented as a u8. +See the [Prioritization](#prioritization) section for more information. + +**Group Sequence**: +The sequence number of the group to fetch. + +The publisher responds with FRAME messages on the same stream. +The publisher FINs the stream after the last frame, or resets on error. + +## PROBE +PROBE is used to measure the available bitrate of the connection. + +~~~ +PROBE Message { + Message Length (i) + Bitrate (i) +} +~~~ + +**Bitrate**: +When sent by the subscriber (stream opener): the target bitrate in bits per second that the publisher should pad up to. +When sent by the publisher (responder): the current measured bitrate in bits per second. ## GROUP The GROUP message contains information about a Group, as well as a reference to the subscription being served. @@ -637,6 +676,14 @@ A generic library or relay MUST NOT inspect or modify the contents unless otherw # Appendix A: Changelog ## moq-lite-03 +- Version negotiated via ALPN (`moq-lite-xx`, latest `moq-lite-03`) instead of SETUP messages. +- Removed Session, SessionCompat streams and SESSION_CLIENT/SESSION_SERVER/SESSION_UPDATE messages. +- Unknown stream types reset instead of fatal; enables extension negotiation via stream probing. +- Added FETCH stream for single group download. +- Added Start Group and End Group (+1 encoded) to SUBSCRIBE, SUBSCRIBE_UPDATE, and SUBSCRIBE_OK. +- Added GROUP_DROP on Subscribe stream. +- Subscribe stream closed (FIN) when all groups accounted for. +- Added PROBE stream replacing SESSION_UPDATE bitrate. - Added `Subscriber Max Latency` and `Subscriber Ordered` to SUBSCRIBE and SUBSCRIBE_UPDATE. - Added `Publisher Priority`, `Publisher Max Latency`, and `Publisher Ordered` to SUBSCRIBE_OK. - Added `Instant Delta` to FRAME. @@ -655,11 +702,12 @@ A quick comparison of moq-lite and moq-transport-14: - Streams instead of request IDs. - Pull only: No unsolicited publishing. -- Uses HTTP for VOD instead of FETCH. -- Extensions instead of parameters. +- FETCH is HTTP-like (single request/response) vs MoqTransport FETCH (multiple groups). +- Extensions negotiated via stream probing instead of parameters. +- Both moq-lite and MoqTransport use ALPN for version identification. - Names use utf-8 strings instead of byte arrays. - Track Namespace is a string, not an array of any array of bytes. -- Subscriptions start at the latest group, not the latest object. +- Subscriptions default to the latest group, not the latest object. - No subgroups - No group/object ID gaps - No object properties @@ -676,7 +724,6 @@ A quick comparison of moq-lite and moq-transport-14: - PUBLISH - PUBLISH_OK - PUBLISH_ERROR -- FETCH - FETCH_OK - FETCH_ERROR - FETCH_CANCEL @@ -704,9 +751,7 @@ Some of these fields occur in multiple messages. - Track Alias - Group Order - Filter Type -- StartGroup - StartObject -- EndGroup - Expires - ContentExists - Largest Group ID From 23e4405052d25962adf34d9a83757b1227754cfe Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Mon, 23 Feb 2026 19:56:10 -0800 Subject: [PATCH 04/12] Remove instants; base expiration on arrival timestamps Remove Instant Delta from FRAME and the Instant section from Delivery. Expiration is now based on when the first byte of a group arrives rather than per-frame instant values. Co-Authored-By: Claude Opus 4.6 --- draft-lcurley-moq-lite.md | 76 +++++++-------------------------------- 1 file changed, 13 insertions(+), 63 deletions(-) diff --git a/draft-lcurley-moq-lite.md b/draft-lcurley-moq-lite.md index 4309591..5bc96f7 100644 --- a/draft-lcurley-moq-lite.md +++ b/draft-lcurley-moq-lite.md @@ -66,7 +66,7 @@ moq-lite consists of: - **Broadcast**: A collection of Tracks from a single publisher. - **Track**: An series of Groups, each of which can be delivered and decoded *out-of-order*. - **Group**: An 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. +- **Frame**: A sized payload of bytes within a Group. The application determines how to split data into broadcast, tracks, groups, and frames. The moq-lite layer provides fanout, prioritization, and caching even for latency sensitive applications. @@ -111,7 +111,7 @@ A subscription starts at a configurable Group (defaulting to the latest) and con The subscriber and publisher both indicate their delivery preference: - `Priority` indicates if Track A should be transmitted instead of Track B. - `Ordered` indicates if the Groups within a Track should be transmitted in order. -- `Max Latency` indicates the maximum duration (based on instants) before a Group is abandoned. +- `Max Latency` indicates the maximum duration before a Group is abandoned. The combination of these preferences enables the most important content to arrive during network degradation while still respecting encoding dependencies. @@ -128,9 +128,8 @@ The application MUST process or buffer groups out of order to avoid blocking on ## Frame A Frame is a payload of bytes within a Group. -A frame is used to represent a chunk of data with an upfront size and [instant](#instant). -The instant is relative to all other frames within the same Track, used for [expiration](#expiration) and metrics. -The application MAY use the instant as a replacement for a presentation timestamp, used for track playback and synchronization. +A frame is used to represent a chunk of data with an upfront size. +The contents are opaque to the moq-lite layer. # Flow This section outlines the flow of messages within a moq-lite session. @@ -293,27 +292,10 @@ An application SHOULD use `ordered` when it wants to provide a VOD-like experien An application SHOULD NOT use `ordered` when it wants to provide a live experience, preferring to skip old groups rather than buffer them. Note that [expiration](#expiration) is not affected by `ordered`. -An old group may still be cancelled/skipped if it's older than `max_latency` set by either peer. +An old group may still be cancelled/skipped if it exceeds `max_latency` set by either peer. An application MUST support gaps and out-of-order delivery even when `ordered` is true. -## Instant -Every Frame consists of an Instant (in milliseconds) scoped to the track. - -This is used for [Expiration](#expiration) at the moq-lite layer. -The instant MAY be used for track synchronization at the application layer, however many containers/formats already contain their own timestamps. -A publisher SHOULD try to preserve the instant when the frame was first created/captured, proxying it. - -Each frame within a group MUST have a monotonically increasing instant. -If the presentation timestamp goes backwards (ex. b-frames), a frame's Instant SHOULD be the maximum presentation timestamp of the group up until that point. -Duplicates are allowed, encoded as delta 0. - -Unless specified by the application, a subscriber: -- SHOULD NOT assume that an instant is a wall clock time. -- SHOULD NOT assume that an instant starts at zero. - -Clock synchronization is out of scope for this draft. - ## Expiration The Publisher and Subscriber both transmit a `Max Latency` value, indicating the maximum duration before a group is expired. @@ -325,37 +307,12 @@ A subscriber SHOULD expire groups based on the `Subscriber Max Latency` in SUBSC A publisher SHOULD expire groups based on the `Publisher Max Latency` in SUBSCRIBE_OK. An implementation MAY use the minimum of both when determining when to expire a group. -Each group consists of a maximum Frame Instant, the meaning of "processed" depending on the endpoint: -- A publisher uses when a frame was queued, regardless of if it has been flushed to the QUIC layer. -- A subscriber uses when a frame was received, regardless of if it has been flushed to the application. - -The entire track consists of a maximum Frame Instant using the same logic as above. -For each group, if the maximum Frame Instant is more than `Max Latency` behind the track's maximum Frame Instant, then the group is considered expired. +Expiration is based on arrival timestamps. +Each group has an arrival timestamp, defined as the time the first byte was received (subscriber) or queued (publisher). +If the time elapsed since a group's arrival exceeds `Max Latency`, and a newer group has since arrived, the older group is considered expired. An expired group SHOULD BE reset at the QUIC level to avoid consuming flow control. - -**Example:** -- Group 1: 1000 1500 (2000) -- Group 2: 2500 3000 - -In this example, frame 2000 has been queued by the publisher but not yet received by the subscriber (shown in parentheses). -If `max_latency` is 1250: -- **Subscriber**: Track max is 3000, Group 1 max received is 1500. Delta = 1500ms > 1250ms → SHOULD expire Group 1 -- **Publisher**: Track max is 3000, Group 1 max queued is 2000. Delta = 1000ms < 1250ms → SHOULD NOT expire Group 1 - -The opposite would be true if frame 3000 was still in transit: - -**Example:** -- Group 1: 1000 1500 2000 -- Group 2: 2500 (3000) - -**NOTE**: Individual frames within a group cannot be cancelled. -Even if `max_latency` was 0, Group 2 would not be expired until a (higher) frame in Group 3 is queued/received. - - -An implementation MAY use the broadcast's maximum Frame Instant instead of the track's maximum Frame Instant. -This is useful to account for encoding delays, ex. when video takes 300ms longer to encode than audio. -However, it SHOULD NOT expire the highest sequence number group within each track, otherwise a track may become perpetually expired. +An implementation SHOULD NOT expire the highest sequence number group within each track, otherwise a track may become perpetually expired. ## Unidirectional Streams Unidirectional streams are used for data transmission. @@ -507,8 +464,8 @@ The publisher SHOULD transmit *older* groups first during congestion if true. See the [Prioritization](#prioritization) section for more information. **Subscriber Max Latency**: -This value is encoded in milliseconds and represents the maximum age of a group. -The publisher SHOULD reset old group streams when the last processed frame is at least this much older than the newest frame. +This value is encoded in milliseconds and represents the maximum age of a group based on arrival time. +The publisher SHOULD reset old group streams when they have been pending for longer than this duration. See the [Expiration](#expiration) section for more information. **Start Group**: @@ -652,22 +609,15 @@ A subscriber MUST handle gaps, potentially caused by congestion. ## FRAME -The FRAME message is a payload at a specific point of time. +The FRAME message is a payload within a group. ~~~ FRAME Message { Message Length (i) - Instant Delta (i) Payload (b) } ~~~ -**Instant Delta**: -The instant when the frame was created, in milliseconds, scoped to the track. -This is encoded as a delta from the previous frame in the same group. -An application SHOULD use the capture/presentation time to account for any encoding delays. -See the [Instant](#instant) section for more information. - **Payload**: An application specific payload. A generic library or relay MUST NOT inspect or modify the contents unless otherwise negotiated. @@ -684,9 +634,9 @@ A generic library or relay MUST NOT inspect or modify the contents unless otherw - Added GROUP_DROP on Subscribe stream. - Subscribe stream closed (FIN) when all groups accounted for. - Added PROBE stream replacing SESSION_UPDATE bitrate. +- Removed `Instant Delta` from FRAME; expiration is now based on arrival timestamps. - Added `Subscriber Max Latency` and `Subscriber Ordered` to SUBSCRIBE and SUBSCRIBE_UPDATE. - Added `Publisher Priority`, `Publisher Max Latency`, and `Publisher Ordered` to SUBSCRIBE_OK. -- Added `Instant Delta` to FRAME. - SUBSCRIBE_OK may be sent multiple times. ## moq-lite-02 From 01b0a2560cb6e41ef8286a500c990bdbf50961a4 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Mon, 23 Feb 2026 20:01:17 -0800 Subject: [PATCH 05/12] Clarify expiration: group age relative to latest group Expiration is computed by comparing a group's arrival time to the latest group's arrival time. A group is never expired until a newer group (by sequence number) exists. Co-Authored-By: Claude Opus 4.6 --- draft-lcurley-moq-lite.md | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/draft-lcurley-moq-lite.md b/draft-lcurley-moq-lite.md index 5bc96f7..d6fefa1 100644 --- a/draft-lcurley-moq-lite.md +++ b/draft-lcurley-moq-lite.md @@ -307,13 +307,12 @@ A subscriber SHOULD expire groups based on the `Subscriber Max Latency` in SUBSC A publisher SHOULD expire groups based on the `Publisher Max Latency` in SUBSCRIBE_OK. An implementation MAY use the minimum of both when determining when to expire a group. -Expiration is based on arrival timestamps. -Each group has an arrival timestamp, defined as the time the first byte was received (subscriber) or queued (publisher). -If the time elapsed since a group's arrival exceeds `Max Latency`, and a newer group has since arrived, the older group is considered expired. +Group age is computed relative to the latest group by sequence number. +A group is never expired until at least the next group (by sequence number) has been received or queued. +Once a newer group exists, a group is considered expired if the time between its arrival and the latest group's arrival exceeds `Max Latency`. +The arrival time is when the first byte of a group is received (subscriber) or queued (publisher). An expired group SHOULD BE reset at the QUIC level to avoid consuming flow control. -An implementation SHOULD NOT expire the highest sequence number group within each track, otherwise a track may become perpetually expired. - ## Unidirectional Streams Unidirectional streams are used for data transmission. @@ -464,8 +463,8 @@ The publisher SHOULD transmit *older* groups first during congestion if true. See the [Prioritization](#prioritization) section for more information. **Subscriber Max Latency**: -This value is encoded in milliseconds and represents the maximum age of a group based on arrival time. -The publisher SHOULD reset old group streams when they have been pending for longer than this duration. +This value is encoded in milliseconds and represents the maximum age of a group relative to the latest group. +The publisher SHOULD reset old group streams when the difference in arrival time between the group and the latest group exceeds this duration. See the [Expiration](#expiration) section for more information. **Start Group**: @@ -634,7 +633,6 @@ A generic library or relay MUST NOT inspect or modify the contents unless otherw - Added GROUP_DROP on Subscribe stream. - Subscribe stream closed (FIN) when all groups accounted for. - Added PROBE stream replacing SESSION_UPDATE bitrate. -- Removed `Instant Delta` from FRAME; expiration is now based on arrival timestamps. - Added `Subscriber Max Latency` and `Subscriber Ordered` to SUBSCRIBE and SUBSCRIBE_UPDATE. - Added `Publisher Priority`, `Publisher Max Latency`, and `Publisher Ordered` to SUBSCRIBE_OK. - SUBSCRIBE_OK may be sent multiple times. From 561b43d56812eff8faa909606fde7627b1ac5f3e Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Mon, 23 Feb 2026 20:08:09 -0800 Subject: [PATCH 06/12] Use sequence + count for SUBSCRIBE_OK and GROUP_DROP Replace start/end group with Group Sequence + Group Count for fewer bytes on the wire. Count of 0 means unbounded. Co-Authored-By: Claude Opus 4.6 --- draft-lcurley-moq-lite.md | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/draft-lcurley-moq-lite.md b/draft-lcurley-moq-lite.md index d6fefa1..34b2970 100644 --- a/draft-lcurley-moq-lite.md +++ b/draft-lcurley-moq-lite.md @@ -507,17 +507,17 @@ SUBSCRIBE_OK Message { Publisher Priority (8) Publisher Ordered (1) Publisher Max Latency (i) - Start Group (i) - End Group (i) + Group Sequence (i) + Group Count (i) } ~~~ -**Start Group**: -The resolved absolute start group sequence + 1. -This MUST NOT be 0. +**Group Sequence**: +The resolved absolute start group sequence. -**End Group**: -The resolved absolute end group sequence + 1, or 0 if unbounded. +**Group Count**: +The number of consecutive groups in the subscription, starting from Group Sequence. +A value of 0 means unbounded. See [SUBSCRIBE](#subscribe) for information about the other fields. @@ -527,17 +527,18 @@ A GROUP_DROP message is sent by the publisher on the Subscribe Stream when group ~~~ GROUP_DROP Message { Message Length (i) - Start Group (i) - End Group (i) + Group Sequence (i) + Group Count (i) Error Code (i) } ~~~ -**Start Group**: -The first group sequence in the dropped range (inclusive, absolute). +**Group Sequence**: +The first group sequence in the dropped range. -**End Group**: -The last group sequence in the dropped range (inclusive, absolute). +**Group Count**: +The number of consecutive groups being dropped, starting from Group Sequence. +A value of 0 means all remaining groups in the subscription. **Error Code**: An application-specific error code. From f1f91c0e05749d0ce6e6ce10116e29588d5bd6bd Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 24 Feb 2026 08:39:41 -0800 Subject: [PATCH 07/12] Remove ANNOUNCE_INIT, add Hops to ANNOUNCE Remove ANNOUNCE_INIT to simplify the protocol and avoid large messages. The publisher now sends individual ANNOUNCE messages for all matching broadcasts. Add Hops field to ANNOUNCE as a tiebreaker for multiple paths. Co-Authored-By: Claude Opus 4.6 --- draft-lcurley-moq-lite.md | 53 ++++++++++----------------------------- 1 file changed, 13 insertions(+), 40 deletions(-) diff --git a/draft-lcurley-moq-lite.md b/draft-lcurley-moq-lite.md index 34b2970..e138b5e 100644 --- a/draft-lcurley-moq-lite.md +++ b/draft-lcurley-moq-lite.md @@ -186,23 +186,18 @@ There's a 1-byte STREAM_TYPE at the beginning of each stream. A subscriber can open a Announce Stream to discover broadcasts matching a prefix. The subscriber creates the stream with a ANNOUNCE_PLEASE message. -The publisher replies with an ANNOUNCE_INIT message containing all currently active broadcasts that currently match the prefix, followed by ANNOUNCE messages for any changes. - -The ANNOUNCE_INIT message contains an array of all currently active broadcast paths encoded as a suffix. -Each path in ANNOUNCE_INIT can be treated as if it were an ANNOUNCE message with status `active`. - -After ANNOUNCE_INIT, the publisher sends ANNOUNCE messages for any changes also encoded as a suffix. +The publisher replies with ANNOUNCE messages for any matching broadcasts and any future changes. Each ANNOUNCE message contains one of the following statuses: - `active`: a matching broadcast is available. - `ended`: a previously `active` broadcast is no longer available. -Each broadcast starts as `ended` (unless included in ANNOUNCE_INIT) and MUST alternate between `active` and `ended`. +Each broadcast starts as `ended` and MUST alternate between `active` and `ended`. The subscriber MUST reset the stream if it receives a duplicate status, such as two `active` statuses in a row or an `ended` without `active`. When the stream is closed, the subscriber MUST assume that all broadcasts are now `ended`. Path prefix matching and equality is done on a byte-by-byte basis. -There MAY be multiple Announce Streams, potentially containing overlapping prefixes, that get their own ANNOUNCE_INIT and ANNOUNCE messages. +There MAY be multiple Announce Streams, potentially containing overlapping prefixes, that get their own ANNOUNCE messages. ### Subscribe A subscriber opens Subscribe Streams to request a Track. @@ -372,45 +367,16 @@ ANNOUNCE_PLEASE Message { **Broadcast Path Prefix**: Indicate interest for any broadcasts with a path that starts with this prefix. -The publisher MUST respond with an ANNOUNCE_INIT message containing any matching and active broadcasts, followed by ANNOUNCE messages for any updates. +The publisher MUST respond with ANNOUNCE messages for any matching and active broadcasts, followed by ANNOUNCE messages for any future updates. Implementations SHOULD consider reasonable limits on the number of matching broadcasts to prevent resource exhaustion. -## ANNOUNCE_INIT -A publisher sends an ANNOUNCE_INIT message immediately after receiving an ANNOUNCE_PLEASE to communicate all currently active broadcasts that match the requested prefix. -Only the suffixes are encoded on the wire, as the full path can be constructed by prepending the requested prefix. - -This message is useful to avoid race conditions, as ANNOUNCE_INIT does not trickle in like ANNOUNCE messages. -For example, an API server that wants to list the current participants could issue an ANNOUNCE_PLEASE and immediately return the ANNOUNCE_INIT response. -Without ANNOUNCE_INIT, the API server would have use a timer to wait until ANNOUNCE to guess when all ANNOUNCE messages have been received. - -~~~ -ANNOUNCE_INIT Message { - Message Length (i) - Suffix Count (i), - [ - Broadcast Path Suffix (s), - ]... -} -~~~ - -**Suffix Count**: -The number of active broadcast path suffixes that follow. -This can be 0. -A publisher MUST NOT include duplicate suffixes in a single ANNOUNCE_INIT message. - -**Broadcast Path Suffix**: -Each suffix is combined with the broadcast path prefix from ANNOUNCE_PLEASE to form the full broadcast path. -This includes all currently active broadcasts matching the prefix. - - - ## ANNOUNCE A publisher sends an ANNOUNCE message to advertise a change in broadcast availability. Only the suffix is encoded on the wire, as the full path can be constructed by prepending the requested prefix. -The status is relative to the ANNOUNCE_INIT and all prior ANNOUNCE messages combined. +The status is relative to all prior ANNOUNCE messages on the same stream. A client MUST ONLY alternate between status values (from active to ended or vice versa). ~~~ @@ -418,6 +384,7 @@ ANNOUNCE Message { Message Length (i) Announce Status (i), Broadcast Path Suffix (s), + Hops (i), } ~~~ @@ -430,6 +397,11 @@ A flag indicating the announce status. **Broadcast Path Suffix**: This is combined with the broadcast path prefix to form the full broadcast path. +**Hops**: +The number of hops from the origin publisher. +This is used as a tiebreaker when there are multiple paths to the same broadcast. +A relay SHOULD increment this value when forwarding an announcement. + ## SUBSCRIBE SUBSCRIBE is sent by a subscriber to start a subscription. @@ -634,6 +606,8 @@ A generic library or relay MUST NOT inspect or modify the contents unless otherw - Added GROUP_DROP on Subscribe stream. - Subscribe stream closed (FIN) when all groups accounted for. - Added PROBE stream replacing SESSION_UPDATE bitrate. +- Removed ANNOUNCE_INIT message. +- Added `Hops` to ANNOUNCE. - Added `Subscriber Max Latency` and `Subscriber Ordered` to SUBSCRIBE and SUBSCRIBE_UPDATE. - Added `Publisher Priority`, `Publisher Max Latency`, and `Publisher Ordered` to SUBSCRIBE_OK. - SUBSCRIBE_OK may be sent multiple times. @@ -643,7 +617,6 @@ A generic library or relay MUST NOT inspect or modify the contents unless otherw - Editorial stuff. ## moq-lite-01 -- Added ANNOUNCE_INIT. - Added Message Length (i) to all messages. # Appendix B: Upstream Differences From db669f396bcaa99e015b1a12cb3169f44c04413b Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 24 Feb 2026 11:36:23 -0800 Subject: [PATCH 08/12] Rename GROUP_DROP to SUBSCRIBE_DROP, add type discriminator Add a type field before message length to distinguish SUBSCRIBE_OK (0x0) and SUBSCRIBE_DROP (0x1) on the subscribe response stream. SUBSCRIBE_OK must be the first message; SUBSCRIBE_DROP before it is invalid. Co-Authored-By: Claude Opus 4.6 --- draft-lcurley-moq-lite.md | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/draft-lcurley-moq-lite.md b/draft-lcurley-moq-lite.md index e138b5e..00fcc64 100644 --- a/draft-lcurley-moq-lite.md +++ b/draft-lcurley-moq-lite.md @@ -77,10 +77,8 @@ There is currently no P2P support within QUIC so it's out of scope for moq-lite. The moq-lite version is negotiated via ALPN during the QUIC handshake. The ALPN format is `moq-lite-xx` where `xx` is the two-digit draft version. -The latest ALPN is `moq-lite-03`. The session is active immediately after the QUIC/WebTransport connection is established. -No additional handshake is required. Extensions are negotiated via stream probing: an endpoint opens a stream with an unknown type and the peer resets it if unsupported. While moq-lite is a point-to-point protocol, it's intended to work end-to-end via relays. @@ -123,7 +121,7 @@ A Group is served by a dedicated QUIC stream which is closed on completion, rese This ensures that all Frames within a Group arrive reliably and in order. In contrast, Groups may arrive out of order due to network congestion and prioritization. -The application MUST process or buffer groups out of order to avoid blocking on flow control. +The application SHOULD process or buffer groups out of order to avoid blocking on flow control. ## Frame A Frame is a payload of bytes within a Group. @@ -159,7 +157,7 @@ However, it is ultimately the other peer's responsibility to close their send di ## Handshake The moq-lite version is negotiated via ALPN during the QUIC handshake. -The ALPN format is `moq-lite-xx` where `xx` is the two-digit draft version (latest: `moq-lite-03`). +The ALPN format is `moq-lite-xx` where `xx` is the two-digit draft version. The session is active immediately after the connection is established. # Streams @@ -203,9 +201,10 @@ There MAY be multiple Announce Streams, potentially containing overlapping prefi A subscriber opens Subscribe Streams to request a Track. The subscriber MUST start a Subscribe Stream with a SUBSCRIBE message followed by any number of SUBSCRIBE_UPDATE messages. -The publisher replies with a SUBSCRIBE_OK message followed by any number of GROUP_DROP and additional SUBSCRIBE_OK messages. +The publisher replies with a SUBSCRIBE_OK message followed by any number of SUBSCRIBE_DROP and additional SUBSCRIBE_OK messages. +The first message on the response stream MUST be a SUBSCRIBE_OK; it is not valid to send a SUBSCRIBE_DROP before SUBSCRIBE_OK. -The publisher closes the stream (FIN) when every group from start to end has been accounted for, either via a completed GROUP stream or a GROUP_DROP message. +The publisher closes the stream (FIN) when every group from start to end has been accounted for, either via a completed GROUP stream or a SUBSCRIBE_DROP message. Unbounded subscriptions (no end group) stay open until the track ends or either endpoint cancels. Either endpoint MAY reset/cancel the stream at any time. @@ -472,9 +471,11 @@ See [SUBSCRIBE](#subscribe) for information about each field. ## SUBSCRIBE_OK A SUBSCRIBE_OK message is sent in response to a SUBSCRIBE. The publisher MAY send multiple SUBSCRIBE_OK messages to update the subscription. +The first message on the response stream MUST be a SUBSCRIBE_OK; a SUBSCRIBE_DROP MUST NOT precede it. ~~~ SUBSCRIBE_OK Message { + Type (i) = 0x0 Message Length (i) Publisher Priority (8) Publisher Ordered (1) @@ -484,6 +485,9 @@ SUBSCRIBE_OK Message { } ~~~ +**Type**: +Set to 0x0 to indicate a SUBSCRIBE_OK message. + **Group Sequence**: The resolved absolute start group sequence. @@ -493,11 +497,12 @@ A value of 0 means unbounded. See [SUBSCRIBE](#subscribe) for information about the other fields. -## GROUP_DROP -A GROUP_DROP message is sent by the publisher on the Subscribe Stream when groups cannot be served. +## SUBSCRIBE_DROP +A SUBSCRIBE_DROP message is sent by the publisher on the Subscribe Stream when groups cannot be served. ~~~ -GROUP_DROP Message { +SUBSCRIBE_DROP Message { + Type (i) = 0x1 Message Length (i) Group Sequence (i) Group Count (i) @@ -505,6 +510,9 @@ GROUP_DROP Message { } ~~~ +**Type**: +Set to 0x1 to indicate a SUBSCRIBE_DROP message. + **Group Sequence**: The first group sequence in the dropped range. @@ -598,12 +606,12 @@ A generic library or relay MUST NOT inspect or modify the contents unless otherw # Appendix A: Changelog ## moq-lite-03 -- Version negotiated via ALPN (`moq-lite-xx`, latest `moq-lite-03`) instead of SETUP messages. +- Version negotiated via ALPN (`moq-lite-xx`) instead of SETUP messages. - Removed Session, SessionCompat streams and SESSION_CLIENT/SESSION_SERVER/SESSION_UPDATE messages. - Unknown stream types reset instead of fatal; enables extension negotiation via stream probing. - Added FETCH stream for single group download. -- Added Start Group and End Group (+1 encoded) to SUBSCRIBE, SUBSCRIBE_UPDATE, and SUBSCRIBE_OK. -- Added GROUP_DROP on Subscribe stream. +- Added Start Group and End Group to SUBSCRIBE, SUBSCRIBE_UPDATE, and SUBSCRIBE_OK. +- Added SUBSCRIBE_DROP on Subscribe stream. - Subscribe stream closed (FIN) when all groups accounted for. - Added PROBE stream replacing SESSION_UPDATE bitrate. - Removed ANNOUNCE_INIT message. From 9c7780b3ededeed327f2d7979f17c553f7dc70d8 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 24 Feb 2026 13:59:05 -0800 Subject: [PATCH 09/12] More tweaks. --- draft-lcurley-moq-lite.md | 39 ++++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/draft-lcurley-moq-lite.md b/draft-lcurley-moq-lite.md index 00fcc64..cc00161 100644 --- a/draft-lcurley-moq-lite.md +++ b/draft-lcurley-moq-lite.md @@ -75,8 +75,9 @@ The moq-lite layer provides fanout, prioritization, and caching even for latency A Session consists of a connection between a client and a server. There is currently no P2P support within QUIC so it's out of scope for moq-lite. -The moq-lite version is negotiated via ALPN during the QUIC handshake. -The ALPN format is `moq-lite-xx` where `xx` is the two-digit draft version. +The moq-lite version identifier is `moq-lite-xx` where `xx` is the two-digit draft version. +For bare QUIC, this is negotiated as an ALPN token during the QUIC handshake. +For WebTransport over HTTP/3, the QUIC ALPN remains `h3` and the moq-lite version is advertised via the `WT-Available-Protocols` and `WT-Protocol` CONNECT headers. The session is active immediately after the QUIC/WebTransport connection is established. Extensions are negotiated via stream probing: an endpoint opens a stream with an unknown type and the peer resets it if unsupported. @@ -156,9 +157,7 @@ After resetting the send direction, an endpoint MAY close the recv direction (ST However, it is ultimately the other peer's responsibility to close their send direction. ## Handshake -The moq-lite version is negotiated via ALPN during the QUIC handshake. -The ALPN format is `moq-lite-xx` where `xx` is the two-digit draft version. -The session is active immediately after the connection is established. +See the [Session](#session) section for ALPN negotiation and session activation details. # Streams moq-lite uses a bidirectional stream for each transaction. @@ -204,8 +203,8 @@ The subscriber MUST start a Subscribe Stream with a SUBSCRIBE message followed b The publisher replies with a SUBSCRIBE_OK message followed by any number of SUBSCRIBE_DROP and additional SUBSCRIBE_OK messages. The first message on the response stream MUST be a SUBSCRIBE_OK; it is not valid to send a SUBSCRIBE_DROP before SUBSCRIBE_OK. -The publisher closes the stream (FIN) when every group from start to end has been accounted for, either via a completed GROUP stream or a SUBSCRIBE_DROP message. -Unbounded subscriptions (no end group) stay open until the track ends or either endpoint cancels. +The publisher closes the stream (FIN) when every group from start to end has been accounted for, either via a GROUP stream (completed or reset) or a SUBSCRIBE_DROP message. +Unbounded subscriptions (no end group) stay open until the publisher closes the stream to indicate the track has ended, or either endpoint resets. Either endpoint MAY reset/cancel the stream at any time. ### Fetch @@ -270,7 +269,7 @@ bob/video: subscriber_priority=1 publisher_priority=2 ali/video: subscriber_priority=1 publisher_priority=1 ``` -The subscriber priority takes precedence, so we could override it if we decided to full screen Ali's window: +The subscriber priority takes precedence, so we could override it if we decided to full-screen Ali's window: ``` ali/audio subscriber_priority=4 publisher_priority=2 ali/video subscriber_priority=3 publisher_priority=1 @@ -279,8 +278,8 @@ bob/video subscriber_priority=1 publisher_priority=2 ``` ### Ordered -The `Subscriber Ordered` boolean signals if older (true) or newer (false) groups should be transmitted first within a Track. -The `Publisher Ordered` boolean MAY likewise be used to resolve conflicts. +The `Subscriber Ordered` field signals if older (0x1) or newer (0x0) groups should be transmitted first within a Track. +The `Publisher Ordered` field MAY likewise be used to resolve conflicts. An application SHOULD use `ordered` when it wants to provide a VOD-like experience, preferring to buffer old groups rather than skip them. An application SHOULD NOT use `ordered` when it wants to provide a live experience, preferring to skip old groups rather than buffer them. @@ -376,7 +375,7 @@ A publisher sends an ANNOUNCE message to advertise a change in broadcast availab Only the suffix is encoded on the wire, as the full path can be constructed by prepending the requested prefix. The status is relative to all prior ANNOUNCE messages on the same stream. -A client MUST ONLY alternate between status values (from active to ended or vice versa). +A publisher MUST ONLY alternate between status values (from active to ended or vice versa). ~~~ ANNOUNCE Message { @@ -412,7 +411,7 @@ SUBSCRIBE Message { Broadcast Path (s) Track Name (s) Subscriber Priority (8) - Subscriber Ordered (1) + Subscriber Ordered (8) Subscriber Max Latency (i) Start Group (i) End Group (i) @@ -429,7 +428,7 @@ The publisher SHOULD transmit *higher* values first during congestion. See the [Prioritization](#prioritization) section for more information. **Subscriber Ordered**: -A boolean representing whether groups are transmitted in ascending (true) or descending (false) order. +A single byte representing whether groups are transmitted in ascending (0x1) or descending (0x0) order. The publisher SHOULD transmit *older* groups first during congestion if true. See the [Prioritization](#prioritization) section for more information. @@ -458,7 +457,7 @@ The start and end group can be changed in either direction (growing or shrinking SUBSCRIBE_UPDATE Message { Message Length (i) Subscriber Priority (8) - Subscriber Ordered (1) + Subscriber Ordered (8) Subscriber Max Latency (i) Start Group (i) End Group (i) @@ -478,9 +477,9 @@ SUBSCRIBE_OK Message { Type (i) = 0x0 Message Length (i) Publisher Priority (8) - Publisher Ordered (1) + Publisher Ordered (8) Publisher Max Latency (i) - Group Sequence (i) + Start Group (i) Group Count (i) } ~~~ @@ -488,11 +487,13 @@ SUBSCRIBE_OK Message { **Type**: Set to 0x0 to indicate a SUBSCRIBE_OK message. -**Group Sequence**: +**Start Group**: The resolved absolute start group sequence. +A value of 0 means the start group is not yet known; the publisher MUST send a subsequent SUBSCRIBE_OK with a resolved value. +A non-zero value is the absolute group sequence + 1. **Group Count**: -The number of consecutive groups in the subscription, starting from Group Sequence. +The number of consecutive groups in the subscription, starting from Start Group. A value of 0 means unbounded. See [SUBSCRIBE](#subscribe) for information about the other fields. @@ -518,7 +519,7 @@ The first group sequence in the dropped range. **Group Count**: The number of consecutive groups being dropped, starting from Group Sequence. -A value of 0 means all remaining groups in the subscription. +A value of 0 means unbounded. **Error Code**: An application-specific error code. From 9c19e7bc91504640bf5c30ae7a99be4ece392377 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 24 Feb 2026 14:21:39 -0800 Subject: [PATCH 10/12] Go back to end group to be consistent. --- draft-lcurley-moq-lite.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/draft-lcurley-moq-lite.md b/draft-lcurley-moq-lite.md index cc00161..584c468 100644 --- a/draft-lcurley-moq-lite.md +++ b/draft-lcurley-moq-lite.md @@ -480,7 +480,7 @@ SUBSCRIBE_OK Message { Publisher Ordered (8) Publisher Max Latency (i) Start Group (i) - Group Count (i) + End Group (i) } ~~~ @@ -492,9 +492,10 @@ The resolved absolute start group sequence. A value of 0 means the start group is not yet known; the publisher MUST send a subsequent SUBSCRIBE_OK with a resolved value. A non-zero value is the absolute group sequence + 1. -**Group Count**: -The number of consecutive groups in the subscription, starting from Start Group. +**End Group**: +The resolved absolute end group sequence (inclusive). A value of 0 means unbounded. +A non-zero value is the absolute group sequence + 1. See [SUBSCRIBE](#subscribe) for information about the other fields. From 58e2f51f1342d7a9c60f556f5719f1bfd9b4a353 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 24 Feb 2026 19:03:33 -0800 Subject: [PATCH 11/12] Use start/end group for SUBSCRIBE_DROP. --- draft-lcurley-moq-lite.md | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/draft-lcurley-moq-lite.md b/draft-lcurley-moq-lite.md index 584c468..c172dd9 100644 --- a/draft-lcurley-moq-lite.md +++ b/draft-lcurley-moq-lite.md @@ -506,8 +506,8 @@ A SUBSCRIBE_DROP message is sent by the publisher on the Subscribe Stream when g SUBSCRIBE_DROP Message { Type (i) = 0x1 Message Length (i) - Group Sequence (i) - Group Count (i) + Start Group (i) + End Group (i) Error Code (i) } ~~~ @@ -515,12 +515,11 @@ SUBSCRIBE_DROP Message { **Type**: Set to 0x1 to indicate a SUBSCRIBE_DROP message. -**Group Sequence**: -The first group sequence in the dropped range. +**Start Group**: +The first absolute group sequence in the dropped range. -**Group Count**: -The number of consecutive groups being dropped, starting from Group Sequence. -A value of 0 means unbounded. +**End Group**: +The last absolute group sequence in the dropped range (inclusive). **Error Code**: An application-specific error code. From 54047e211705a1be3024abdfbc31f00e50aad842 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 24 Feb 2026 19:20:08 -0800 Subject: [PATCH 12/12] PR review --- draft-lcurley-moq-lite.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/draft-lcurley-moq-lite.md b/draft-lcurley-moq-lite.md index c172dd9..89709fa 100644 --- a/draft-lcurley-moq-lite.md +++ b/draft-lcurley-moq-lite.md @@ -211,7 +211,8 @@ Either endpoint MAY reset/cancel the stream at any time. A subscriber opens a Fetch Stream (0x3) to request a single Group from a Track. The subscriber sends a FETCH message containing the broadcast path, track name, priority, and group sequence. -The publisher responds with FRAME messages on the same bidirectional stream. +Unlike Group Streams (which MUST start with a GROUP message), the publisher responds with FRAME messages directly on the same bidirectional stream — there is no preceding GROUP header. +The Subscribe ID and Group Sequence for the returned FRAME messages are implicit, taken from the original FETCH request. The publisher FINs the stream after the last frame, or resets the stream on error. Fetch behaves like HTTP: a single request/response per stream. @@ -219,11 +220,12 @@ Fetch behaves like HTTP: a single request/response per stream. ### Probe A subscriber opens a Probe Stream (0x4) to measure the available bitrate of the connection. -The subscriber sends a PROBE message with a target bitrate. -The publisher SHOULD pad the connection to achieve the target bitrate. -The publisher periodically replies with PROBE messages containing the current measured bitrate. +The subscriber sends a PROBE message with a target bitrate on the bidirectional stream. +The subscriber MAY send additional PROBE messages on the same stream to update the target bitrate; the publisher MUST treat each PROBE as a new target to attempt. +The publisher SHOULD pad the connection to achieve the most recent target bitrate. +The publisher periodically replies with PROBE messages on the same bidirectional stream containing the current measured bitrate. -If the publisher does not support PROBE (e.g., congestion controller is not exposed), it resets the stream. +If the publisher does not support PROBE (e.g., congestion controller is not exposed), it MUST reset the stream. # Delivery The most important concept in moq-lite is how to deliver a subscription.