Skip to content
Draft
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
40 changes: 38 additions & 2 deletions draft-lcurley-moq-lite.md
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,8 @@ There MAY be multiple Announce Streams, potentially containing overlapping prefi
### Subscribe
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 subscriber MUST start a Subscribe Stream with a SUBSCRIBE message followed by any number of SUBSCRIBE_UPDATE and SUBSCRIBE_STATS messages.
The opening SUBSCRIBE is identified by the stream type (0x2) and carries no message Type; every subsequent message from the subscriber begins with a Type that distinguishes SUBSCRIBE_UPDATE from SUBSCRIBE_STATS, mirroring how the publisher's messages on this stream are typed.
When a start group can be resolved, the publisher replies with a SUBSCRIBE_OK message (confirming the subscription and resolving its start group), followed by any number of SUBSCRIBE_END and SUBSCRIBE_DROP messages.
When the accepted track has already ended with no matching groups there is no start group to resolve, so the publisher sends SUBSCRIBE_END with no preceding SUBSCRIBE_OK.
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.
Expand Down Expand Up @@ -775,6 +776,7 @@ The start and end group can be changed in either direction (growing or shrinking

~~~
SUBSCRIBE_UPDATE Message {
Type (i) = 0x0
Message Length (i)
Subscriber Priority (8)
Subscriber Ordered (8)
Expand All @@ -784,7 +786,40 @@ SUBSCRIBE_UPDATE Message {
}
~~~

See [SUBSCRIBE](#subscribe) for information about each field.
**Type**:
Set to 0x0 to indicate a SUBSCRIBE_UPDATE message.

See [SUBSCRIBE](#subscribe) for information about each remaining field.


## SUBSCRIBE_STATS
A subscriber sends a SUBSCRIBE_STATS message to report telemetry about a subscription.
Unlike SUBSCRIBE_UPDATE, it does not modify the subscription and the publisher only observes it; it is kept separate so that refreshing telemetry does not re-echo the subscription's delivery parameters on every change.
A subscriber MAY send multiple SUBSCRIBE_STATS messages over the life of the subscription to refresh the values.

~~~
SUBSCRIBE_STATS Message {
Type (i) = 0x1
Message Length (i)
Subscriber Count (i)
}
~~~

**Type**:
Set to 0x1 to indicate a SUBSCRIBE_STATS message.

**Subscriber Count**:
The number of subscribers this subscription represents, encoded as the count minus one: the subscription is the implicit `1`, so a leaf subscriber sends `0`.
This is a subscriber-side value that fans *in* at a relay: a relay merging multiple downstream subscriptions into one upstream subscription sets this to the **sum** of their counts, so the value telescopes up the fan-out tree.
A publisher therefore learns its total number of downstream subscribers across any number of relay hops by reading the count on its single upstream subscription, without any per-hop coordination.
Until a SUBSCRIBE_STATS is received, the count is `1`; a leaf subscriber that represents only itself need not send the message.
A relay holding a subscription open with no live downstream subscribers (e.g. briefly retaining it for reuse) MUST still report at least `1`, since the held subscription cannot represent fewer subscribers than itself.
The count is advisory: a subscriber MAY misreport it, and a relay MUST NOT use it for delivery decisions.

A relay SHOULD rate-limit SUBSCRIBE_STATS messages (for example, coalescing changes within a short window of roughly a second), so that rapid subscriber churn does not flood the upstream with control messages.
Because the count is the latest aggregate rather than a delta, a change that reverts within the window requires no message at all.

Future revisions MAY append additional stats to this message; the `Message Length` bounds the message so a receiver can stop after the fields it understands.


## TRACK
Expand Down Expand Up @@ -1084,6 +1119,7 @@ A generic library or relay MUST NOT inspect or modify the decompressed contents
# Appendix A: Changelog

## moq-lite-05
- Added a SUBSCRIBE_STATS message carrying a telescoping `Subscriber Count` (count of downstream subscribers, encoded as the count minus one so a leaf sends `0`); a relay sums the counts of the downstream subscriptions it merges, so a publisher reads its total audience across any number of hops from its single upstream subscription. It is kept separate from SUBSCRIBE_UPDATE so that refreshing telemetry does not re-echo the subscription's delivery parameters; relays SHOULD rate-limit it to absorb subscriber churn. This also introduced a `Type` tag on the subscriber's post-SUBSCRIBE messages (`0x0` SUBSCRIBE_UPDATE, `0x1` SUBSCRIBE_STATS) to distinguish them, mirroring the publisher's typed responses.
- 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.
Expand Down
211 changes: 211 additions & 0 deletions draft-lcurley-moq-subscribe-stats.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
---
title: "MoQ Subscribe Stats Extension"
abbrev: "moq-subscribe-stats"
category: info

docname: draft-lcurley-moq-subscribe-stats-latest
submissiontype: IETF # also: "independent", "editorial", "IAB", or "IRTF"
number:
date:
v: 3
area: wit
workgroup: moq

author:
-
fullname: Luke Curley
email: kixelated@gmail.com

normative:
moqt: I-D.ietf-moq-transport

informative:

--- abstract

This document defines a SUBSCRIBE_STATS message for MoQ Transport {{moqt}}: fire-and-forget telemetry that a subscriber reports about a subscription without modifying it.
It carries one or more stats that aggregate cheaply up the relay fan-out tree.
This document defines a single stat, Subscriber Count, which telescopes so a publisher can learn its total downstream audience across any number of relay hops by reading one value on its upstream subscription.

--- middle

# Conventions and Definitions
{::boilerplate bcp14-tagged}


# Introduction
A publisher in {{moqt}} often wants to know how many subscribers are receiving a Track.
This is straightforward when a subscriber connects directly, but {{moqt}} is designed around relays: a relay aggregates many downstream subscriptions for the same Track into a single upstream subscription toward the origin (its "fan-out" tree).
The origin sees one upstream subscription per relay, not the individual subscribers behind it, so it cannot count its true audience without out-of-band coordination.

This document defines a SUBSCRIBE_STATS message that reports telemetry about a subscription.
A stat is useful here only if it aggregates cheaply up the fan-out tree: a relay merges the stats of the downstream subscriptions it serves into one value on its single upstream subscription.
This document defines a single such stat, **Subscriber Count**, which a relay reports as the **sum** of its downstream counts.
Because the count is a sum reduced up the existing subscription tree, it telescopes for free: at the origin, the count on each upstream subscription is the total number of subscribers reachable through that relay, transitively, across any number of hops.

Stats change as subscribers join and leave, so SUBSCRIBE_STATS is sent repeatedly over the life of a subscription.
This is pure telemetry: it never modifies the subscription, and the publisher only observes it.
For that reason it is a dedicated, fire-and-forget message rather than a parameter on REQUEST_UPDATE ({{moqt}} Section 10.9).
A REQUEST_UPDATE consumes a Request ID ({{moqt}} Section 10.1) and obliges the receiver to answer with a REQUEST_OK or REQUEST_ERROR ({{moqt}} Section 10.9) — a request/response transaction whose purpose is to *modify* the subscription, which is a poor fit for a value pushed once per second that changes nothing.
SUBSCRIBE_STATS instead rides the subscription's existing request stream, consumes no Request ID, and elicits no response.


# Setup Negotiation
The Subscribe Stats extension is negotiated during the SETUP exchange as defined in {{moqt}} Section 9.4.

Both endpoints indicate support by including the following Setup Option:

~~~
SUBSCRIBE_STATS Setup Option {
Option Key (vi64) = 0xC0117
Option Value Length (vi64) = 0
}
~~~

The extension is available on a hop only if both endpoints on that hop included this option.
The extension is negotiated independently on each hop: a relay MAY support it upstream but not downstream, or vice versa.

Negotiation is mandatory before the message is sent.
{{moqt}} (Section 10) requires an endpoint that receives an unknown control message type to close the session, so — unlike an optional parameter, which can be ignored — a SUBSCRIBE_STATS message cannot be sent speculatively.
An endpoint MUST NOT send SUBSCRIBE_STATS on a hop that did not negotiate this extension.


# SUBSCRIBE_STATS Message
This document defines a new control message, sent on a subscription's request stream ({{moqt}} Section 3.3) by the endpoint that opened it (the subscriber, which for an upstream subscription is the relay).

~~~
SUBSCRIBE_STATS Message {
Type (vi64) = 0xC0117
Length (16)
Number of Stats (vi64)
Stats (..) ...
}
~~~

The message MUST NOT be the first message on the request stream; it follows the SUBSCRIBE ({{moqt}} Section 10.7) that opened the stream.
It consumes no Request ID ({{moqt}} Section 10.1), and the receiver MUST NOT respond to it.
A subscriber MAY send it any number of times over the life of the subscription to refresh the stats.

**Number of Stats**:
The number of stats that follow.

**Stats**:
Each stat is encoded as a Message Parameter ({{moqt}} Section 10.2): a `Type` (drawn from the same "Message Parameters" registry) and a value, where an even `Type` carries a single varint and an odd `Type` is length-prefixed ({{moqt}} Section 1.4.3).
A receiver MUST ignore a stat whose `Type` it does not recognize, so future stats can be added without a new message type or a wire break.
This document defines one stat, [Subscriber Count](#subscriber-count).


# Subscriber Count {#subscriber-count}

~~~
SUBSCRIBER_COUNT Stat {
Type (vi64) = 0xC0116
Value (vi64) ; Subscriber Count - 1
}
~~~

The `Type` `0xC0116` is even, so per {{moqt}} Section 1.4.3 the value is a single varint with no length prefix.

**Value**:
The number of subscribers this subscription represents, including the subscriber itself, encoded as the count minus one.
The subscription is the implicit `1`, so the wire value is `Subscriber Count - 1`: a leaf encodes `0`, and a relay encodes its summed total minus one.

Encoding `count - 1` rather than the count itself makes a count of `0` impossible to represent on the wire: the minimum encodable value is `0`, which decodes to a count of `1`.
A subscription can never report fewer subscribers than itself.

Until a SUBSCRIBE_STATS carrying this stat is received for a subscription, its count is `1`.
A leaf subscriber that represents only itself therefore need not send it at all.


# Semantics
The Subscriber Count is a reduction up the subscription tree.

A **leaf subscriber** (one that is not a relay) represents itself: a count of `1`.
It need not report the stat to mean this.

A **relay** that aggregates one or more downstream subscriptions for a Track into a single upstream subscription sets the count it reports on that upstream subscription to the **sum** of the counts of the downstream subscriptions, treating a downstream subscription that has not reported as `1`.
When a downstream count changes, or a downstream subscription is added or removed, the relay recomputes the sum and, if it changed, sends a SUBSCRIBE_STATS message upstream with the new total.

A relay MUST report at least `1` for any upstream subscription it is holding open, even if it currently has no live downstream subscribers (for example, a subscription briefly retained for reuse).
The `count - 1` encoding enforces this floor automatically: a sum of `0` still encodes a wire value of `0`, which decodes upstream as `1`.
A held subscription cannot represent fewer subscribers than itself.

Because each relay reports the sum of its subtree, the value telescopes: at the origin, the count on a given upstream subscription is the total number of leaf subscribers reachable through that subscription, across any number of relay hops.
A publisher reads its total audience for a Track as the sum of the counts of the subscriptions it is serving.

SUBSCRIBE_STATS alters no delivery behavior.
It MUST NOT influence prioritization, caching, congestion response, or any other distribution decision; it is informational telemetry carried alongside the subscription.


# Rate Limiting
Subscriber churn can change a stat rapidly, and at a busy relay each change would otherwise produce an upstream SUBSCRIBE_STATS message.

A relay SHOULD rate-limit SUBSCRIBE_STATS messages per subscription, coalescing changes that occur within a short window (on the order of a second) and then sending the latest values.
Because each stat carries its current value rather than a delta, a change that reverts within the window — a subscriber that joins and leaves, or leaves and returns — requires no upstream message at all.

Because the message is independent of REQUEST_UPDATE, this rate limiting never delays a genuine subscription change: delivery-affecting updates are forwarded according to {{moqt}} without regard to the stats window.


# Security Considerations
**Audience disclosure.**
The Subscriber Count discloses aggregate viewership to the publisher and to every relay on the path toward it.
For some applications the size of an audience is sensitive (for example, it can reveal the popularity or reach of content, or that an audience has dropped to zero).
Because the extension is negotiated per hop, an endpoint that considers a stat sensitive simply does not advertise the SUBSCRIBE_STATS Setup Option, and no stats are exchanged on that hop.

**Untrusted values.**
Stats are supplied by the subscriber side and aggregated by intermediaries, none of which the publisher can fully trust.
A malicious or buggy subscriber can report an inflated or deflated count, and a malicious relay can report any sum regardless of its actual downstream subscriptions.
Stats are therefore advisory: an endpoint MUST NOT use one for any security-sensitive purpose — such as billing, admission control, rate limiting, or capacity planning that affects other subscribers — without independent verification.
A relay aggregates the values it receives but cannot attest to the honesty of its downstream peers.

**Churn amplification.**
A subscriber that rapidly joins and leaves could attempt to amplify control traffic toward the origin by forcing repeated stat updates.
The rate-limiting in [Rate Limiting](#rate-limiting) bounds this: the upstream SUBSCRIBE_STATS rate per subscription is capped regardless of downstream churn, and reverts within the window are suppressed entirely.
Because the message consumes no Request ID and elicits no response, this churn cannot exhaust an identifier space or force the origin into matching replies.

This extension introduces no other security considerations beyond those described in {{moqt}}.


# IANA Considerations

This document requests the following registrations.

## MOQT Setup Options

This document requests a registration in the "MOQT Setup Options" registry ({{moqt}} Section 15.4), whose policy is Specification Required.
moq-transport defines no private-use range for Setup Options; extensions request a (provisional) codepoint.
A high, distinctive value is chosen to avoid the low ranges reserved by {{moqt}} and to minimize collisions with provisional registrations by other extensions; it also avoids the greasing pattern (`0x7f * N + 0x9D`).

| Value | Name | Reference |
|:------|:-----|:----------|
| 0xC0117 | SUBSCRIBE_STATS | This Document |

## MOQT Message Types

This document registers a control message type.
{{moqt}} does not yet establish an IANA registry for message types, so this is a provisional codepoint pending such a registry; the value is chosen to be high and distinctive to avoid the low ranges {{moqt}} assigns and to minimize collisions with provisional registrations by other extensions, and it avoids the greasing pattern (`0x7f * N + 0x9D`).
This is the same value as the SUBSCRIBE_STATS Setup Option above; Setup Options and message types are independent namespaces, so the shared value is unambiguous.

The Stream column has the meaning defined by {{moqt}} Section 10: "Request" indicates the message is carried on a bidirectional request stream. The message is not marked "First": it never opens a request stream.

| Value | Name | Stream | Reference |
|:------|:-----|:-------|:----------|
| 0xC0117 | SUBSCRIBE_STATS | Request | This Document |

## MOQT Message Parameters

This document requests a registration in the "Message Parameters" registry ({{moqt}} Section 15.7), used here to carry stats inside SUBSCRIBE_STATS.
The value is **even** so that, per the Key-Value-Pair encoding ({{moqt}} Section 1.4.3), the count is carried as a single varint with no length prefix.
A high, distinctive value is chosen to avoid the low ranges reserved by {{moqt}} and to minimize collisions with provisional registrations by other extensions; it also avoids the greasing pattern (`0x7f * N + 0x9D`).

| Value | Name | Reference |
|:------|:-----|:----------|
| 0xC0116 | SUBSCRIBER_COUNT | This Document |


--- back

# Acknowledgments
{:numbered="false"}

This document was drafted with the assistance of Claude, an AI assistant by Anthropic.
Loading