Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 92 additions & 8 deletions draft-lcurley-moq-lite.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,17 @@ When UDP is unavailable, moq-lite-05 MAY also run over reliable byte-stream tran
Qmux provides a length-delimited polyfill for QUIC streams on top of TCP/TLS or WebSocket; see [Transports](#transports) for the specific bindings and ALPN negotiation.

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.
Both endpoints SHOULD begin sending and receiving streams right away to avoid an extra round-trip.

Optional capabilities and extensions are negotiated via a SETUP message (see [SETUP](#setup)).
Each endpoint MUST open a unidirectional Setup Stream at the start of the session, send a single SETUP message advertising what it supports, and immediately close the stream (FIN); an endpoint with no optional capabilities sends a SETUP with an empty parameter list.
The two SETUP messages are independent; neither endpoint waits for the peer's SETUP before opening other streams.
Because a SETUP is always sent, the buffering below is bounded: an endpoint knows the peer's full capability set has arrived once it receives that single SETUP.
An endpoint SHOULD continue to send and process non-Setup streams until a negotiated extension would change the behavior or encoding of a stream, in which case it MUST buffer that stream until the peer's SETUP has been received.
For example, if an extension adds a field to SUBSCRIBE_OK, the subscriber buffers SUBSCRIBE_OK until SETUP arrives so the new field can be parsed.

As a fallback, an endpoint that opens an extension stream the peer does not support simply sees that stream reset (see [STREAM_TYPE](#stream_type)).
A negotiated capability applies only to this hop; each session is negotiated independently and relays MUST NOT forward SETUP.

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.
Expand Down Expand Up @@ -303,14 +313,16 @@ The publisher FINs the stream after the last frame, or resets the stream on erro
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.
A subscriber opens a Probe Stream (0x4) to measure, and optionally increase, the available bitrate of the connection.
The publisher advertises its Probe level in SETUP (see [Probe Parameter](#probe-parameter)): None, Report (measure only), or Increase (measure and actively probe).

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 estimated bitrate and smoothed RTT.
If the publisher advertised the Increase capability, it SHOULD pad the connection (or send redundant data) to achieve the most recent target bitrate, without exceeding the congestion window.
A publisher that advertised Report but not Increase ignores the target and only reports; it MUST NOT pad above its current sending rate.
In either case the publisher periodically replies with PROBE messages on the same bidirectional stream containing the current estimated bitrate and smoothed RTT.

If the publisher does not support PROBE (e.g., congestion controller is not exposed), it MUST reset the stream.
If the publisher advertised no Probe capability (e.g., the congestion controller is not exposed), it MUST reset the stream.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Jun 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Probe failure behavior conflicts with the probe extension draft.

Here unsupported Probe results in stream reset, but the provided draft-lcurley-moq-probe.md snippet requires session close with PROTOCOL_VIOLATION when a subscriber opens PROBE without negotiated support. This cross-draft mismatch will cause interop splits.

Please align the normative outcome between drafts (either reset-stream or session-close) and keep both documents identical on this point.

Also applies to: 586-587

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@draft-lcurley-moq-lite.md` at line 324, The Probe failure behavior is
inconsistent between drafts: update the text that currently says "If the
publisher advertised no Probe capability ... it MUST reset the stream." to match
the probe extension draft's normative action (either change this to require
session close with PROTOCOL_VIOLATION or change the probe draft to require
stream reset) so both documents are identical; specifically, replace the
sentence mentioning "reset the stream" with the agreed outcome and use the exact
token "PROTOCOL_VIOLATION" if you choose session-close, and mirror the same
wording in the other occurrence noted near the later Probe rules so
"Probe"/"PROBE"/"PROTOCOL_VIOLATION" semantics are consistent across both
drafts.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This divergence is intentional: the two drafts target different host protocols with different error models. moq-lite resets unknown/unsupported streams non-fatally (its core STREAM_TYPE rule), so stream reset is the correct, in-philosophy outcome here. moq-probe targets moq-transport, where Section 3.3 mandates closing the session with PROTOCOL_VIOLATION when a bidirectional stream begins with an un-negotiated message type. Making them identical would violate one host spec. Leaving this thread open for a maintainer call.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kixelated, thanks for the clear explanation — that makes sense. The error model is inherited from the host protocol in each case, so the divergence is load-bearing, not accidental. I'll note this for future reviews.


✏️ Learnings added
Learnt from: kixelated
Repo: moq-dev/drafts PR: 26
File: draft-lcurley-moq-lite.md:325-325
Timestamp: 2026-06-06T01:48:35.549Z
Learning: In the moq-lite + moq-probe drafts (moq-dev/drafts), the Probe failure behavior intentionally differs between the two documents:
- draft-lcurley-moq-lite.md: when a subscriber opens a Probe Stream without the publisher advertising Probe capability, the publisher MUST reset the stream (non-fatal). This follows moq-lite's core STREAM_TYPE rule where unknown/unsupported streams are reset non-fatally.
- draft-lcurley-moq-probe.md: targets moq-transport, where Section 3.3 mandates closing the session with PROTOCOL_VIOLATION when a bidirectional stream begins with an un-negotiated message type.
Aligning the two would violate one host specification. The divergence is intentional by design.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: kixelated
Repo: moq-dev/drafts PR: 25
File: draft-lcurley-moq-lite.md:0-0
Timestamp: 2026-06-03T03:14:11.765Z
Learning: In draft-lcurley-moq-lite.md (moq-lite protocol spec), the bidirectional stream type IDs are: Announce=0x1, Subscribe=0x2, Fetch=0x3, Probe=0x4, Goaway=0x5, Track=0x6. Subscribe is 0x2 and Fetch is 0x3 (not 0x3 and 0x4 as previously misstated).


### Goaway
Either endpoint can open a Goaway Stream (0x5) to initiate a graceful session shutdown.
Expand Down Expand Up @@ -420,6 +432,17 @@ Unidirectional streams are used for data transmission.
|-------:|:---------|-------------|
| 0x0 | Group | Publisher |
| ------ | -------- | ----------- |
| 0x1 | Setup | Either |
| ------ | -------- | ----------- |

### Setup {#setup-stream}
Each endpoint MUST open a Setup Stream (0x1) at the start of the session to advertise the optional capabilities and extensions it supports.

The opener sends a single SETUP message and immediately closes the stream (FIN).
There is exactly one Setup Stream per direction; an endpoint that receives a second Setup Stream MUST close the session with a PROTOCOL_VIOLATION.
An endpoint with no optional capabilities sends a SETUP with an empty parameter list rather than omitting the stream, giving the peer a deterministic signal that no capabilities are forthcoming.

See the [Session](#session) section for how an endpoint avoids waiting on the peer's SETUP before exchanging other streams.

### Group
A publisher creates Group Streams in response to a Subscribe Stream.
Expand Down Expand Up @@ -494,7 +517,7 @@ This length field does not include the length of the varint length itself.
An implementation SHOULD close the connection with a PROTOCOL_VIOLATION if it receives a message with an unexpected length.
The version and extensions should be used to support new fields, not the message length.

## STREAM_TYPE
## STREAM_TYPE {#stream_type}
All streams start with a short header indicating the stream type.

~~~
Expand All @@ -505,7 +528,65 @@ STREAM_TYPE {

The stream ID depends on if it's a bidirectional or unidirectional stream, as indicated in the Streams section.
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.
Unknown stream types MUST NOT be treated as fatal; this is the fallback when an extension stream is opened against a peer that did not negotiate it.


## SETUP {#setup}
A SETUP message advertises the optional capabilities and extensions the sender supports for this session.
It is sent exactly once, as the only message on a [Setup Stream](#setup-stream).

~~~
SETUP Message {
Message Length (i)
Parameter Count (i)
Setup Parameter (..) ...
}

Setup Parameter {
Parameter ID (i)
Parameter Length (i)
Parameter Value (..)
}
~~~

**Parameter Count**:
The number of Setup Parameters that follow.

**Parameter ID**:
Identifies the capability or extension.
A receiver MUST ignore unknown Parameter IDs, allowing new capabilities to be added without breaking older implementations.
A Parameter ID MUST NOT appear more than once; a receiver MUST close the session with a PROTOCOL_VIOLATION if it does.

**Parameter Length**:
The length of Parameter Value in bytes.

**Parameter Value**:
The parameter-specific value, interpreted according to Parameter ID.

A capability is available for the session only if the relevant endpoint advertises it; an absent parameter means the sender does not support that capability.
The following Setup Parameters are defined:

|------|----------|-------------|
| ID | Name | Value |
|-----:|:---------|:------------|
| 0x1 | Probe | Level (i) |
|------|----------|-------------|

### Probe Parameter {#probe-parameter}
The Probe Parameter advertises the sender's capability level when acting as a publisher on a [Probe Stream](#probe).
The Parameter Value is a variable-length integer level, where each level includes the one below it:

- `0` **None**: The publisher does not support probing. Equivalent to omitting the parameter.
- `1` **Report**: The publisher can measure and periodically report its estimated bitrate.
- `2` **Increase**: The publisher can additionally pad the connection (or send redundant data) to probe for bandwidth above its current sending rate, up to the subscriber's target.

The levels are nested rather than independent: probing for more bandwidth is meaningless without measuring it, so Increase always includes Report. Reporting the current bitrate is far simpler to implement, so a publisher may support Report without Increase.

A subscriber MUST consult the publisher's advertised level before relying on a Probe Stream:

- At `None`, the subscriber SHOULD NOT open a Probe Stream; if it does, the publisher MUST reset it.
- At `Report`, the subscriber MAY open a Probe Stream to monitor the estimated bitrate but MUST NOT expect the publisher to pad above its current sending rate. A subscriber that needs to probe for additional bandwidth MUST use an alternative (e.g. speculatively switching to a higher rendition).
- At `Increase`, the subscriber MAY request a target bitrate and expect the publisher to actively probe up to it.


## ANNOUNCE_INTEREST
Expand Down Expand Up @@ -815,6 +896,7 @@ PROBE Message {

**Bitrate**:
When sent by the subscriber (stream opener): the target bitrate in bits per second that the publisher should pad up to.
The publisher only honors a target above its current sending rate if it advertised the Increase capability (see [Probe Parameter](#probe-parameter)); otherwise the target is ignored and the publisher only reports.
When sent by the publisher (responder): the current estimated bitrate in bits per second.
A value of 0 means unknown.

Expand Down Expand Up @@ -907,6 +989,8 @@ A generic library or relay MUST NOT inspect or modify the decompressed contents
# Appendix A: Changelog

## moq-lite-05
- 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.
- 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.
- Added `Frame Start` to FETCH so a subscriber can begin partway through a group instead of always at frame `0`, allowing resumption of a partially-received group.
- Renamed `Start Group`/`End Group` to `Group Start`/`Group End` in SUBSCRIBE, SUBSCRIBE_UPDATE, SUBSCRIBE_OK, and SUBSCRIBE_DROP for consistency with the entity-first naming used elsewhere (e.g. `Group Sequence`). Wire format unchanged.
- Allowed a duplicate `active` ANNOUNCE to atomically replace the prior advertisement (equivalent to UNANNOUNCE+ANNOUNCE). 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.
Expand Down Expand Up @@ -956,7 +1040,7 @@ A quick comparison of moq-lite and moq-transport-14:
- Streams instead of request IDs.
- Pull only: No unsolicited publishing.
- FETCH is HTTP-like (single request/response) vs MoqTransport FETCH (multiple groups).
- Extensions negotiated via stream probing instead of parameters.
- Capabilities negotiated via a SETUP message on a unidirectional stream that does not block other streams, instead of MoqTransport's blocking CLIENT_SETUP/SERVER_SETUP handshake on the control stream.
- 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.
Expand Down
Loading
Loading