Skip to content

Commit 16b0aa7

Browse files
kixelatedclaude
andcommitted
Add a telescoping SUBSCRIBE_DEMAND to moq-lite and a moq-transport extension
Report the downstream demand for a subscription back up the relay fan-out tree, so an origin learns its true audience and what those subscribers need across any number of hops. moq-lite: a new SUBSCRIBE_DEMAND message on the Subscribe Stream carrying Subscriptions Created and Subscriptions Closed (cumulative counts whose difference is the current subscriber count; a relay sums each across its downstreams, so demand telescopes for free) and a Group Request (the minimum group the subscriber wants produced, encoded like Group Start). The group request is a level, not a one-shot "new group now" trigger -- already satisfied if a group at or beyond it exists -- so it is idempotent and aggregates as the maximum of downstream requests; a publisher MAY also treat a rising Subscriptions Created as an implicit new-group request. A Type tag is added to the subscriber's post-SUBSCRIBE messages (0x0 SUBSCRIBE_UPDATE, 0x1 SUBSCRIBE_DEMAND). moq-transport: draft-lcurley-moq-demand expresses the same in moq-transport's idiom -- a fire-and-forget SUBSCRIBE_DEMAND control message on the request stream (no Request ID, no response), negotiated per hop via a SUBSCRIBE_DEMAND Setup Option. Replaces the earlier subscribe-stats framing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 0a66e5c commit 16b0aa7

2 files changed

Lines changed: 247 additions & 2 deletions

File tree

draft-lcurley-moq-demand.md

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
---
2+
title: "MoQ Demand Extension"
3+
abbrev: "moq-demand"
4+
category: info
5+
6+
docname: draft-lcurley-moq-demand-latest
7+
submissiontype: IETF # also: "independent", "editorial", "IAB", or "IRTF"
8+
number:
9+
date:
10+
v: 3
11+
area: wit
12+
workgroup: moq
13+
14+
author:
15+
-
16+
fullname: Luke Curley
17+
email: kixelated@gmail.com
18+
19+
normative:
20+
moqt: I-D.ietf-moq-transport
21+
22+
informative:
23+
24+
--- abstract
25+
26+
This document defines a SUBSCRIBE_DEMAND message for MoQ Transport {{moqt}}: fire-and-forget feedback that a subscriber reports about a subscription describing the downstream demand for it.
27+
It carries how many subscribers a subscription represents — as a pair of cumulative counts whose difference telescopes up the relay fan-out tree, letting a publisher learn its total audience across any number of hops — and an optional Group Request asking the publisher to produce a new group once the subscriber has fallen behind.
28+
29+
--- middle
30+
31+
# Conventions and Definitions
32+
{::boilerplate bcp14-tagged}
33+
34+
35+
# Introduction
36+
A publisher in {{moqt}} often wants to know the demand for a Track: how many subscribers are receiving it, and whether any of them needs a fresh group to make progress.
37+
Both are 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).
38+
The origin sees one upstream subscription per relay, not the individual subscribers behind it, so it can neither count its true audience nor learn what those subscribers need without out-of-band coordination.
39+
40+
This document defines a SUBSCRIBE_DEMAND message that reports this demand back up the subscription path.
41+
It carries two kinds of information, each chosen so that it aggregates cheaply up the fan-out tree:
42+
43+
- **Audience size**, as a pair of cumulative counts, `Subscriptions Created` and `Subscriptions Closed`. A relay reports the **sum** of each across the downstream subscriptions it serves, so the difference — the current number of subscribers — telescopes for free. At the origin, the demand on each upstream subscription is the total number of subscribers reachable through that relay, transitively, across any number of hops.
44+
- A **Group Request**, the minimum group a subscriber wants the publisher to produce. A relay reports the **maximum** across its downstreams (less any it can already satisfy from cache). It is expressed as a *level* — "I want a group at least this new" — not a one-shot trigger, so it is idempotent and deduplicates naturally as it aggregates.
45+
46+
Demand changes as subscribers join, leave, and fall behind, so SUBSCRIBE_DEMAND is sent repeatedly over the life of a subscription.
47+
It is a dedicated, fire-and-forget message rather than a parameter on REQUEST_UPDATE ({{moqt}} Section 10.9).
48+
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's delivery range or priority, which is a poor fit for feedback pushed repeatedly that changes neither.
49+
SUBSCRIBE_DEMAND instead rides the subscription's existing request stream, consumes no Request ID, and elicits no response.
50+
51+
52+
# Setup Negotiation
53+
The Demand extension is negotiated during the SETUP exchange as defined in {{moqt}} Section 9.4.
54+
55+
Both endpoints indicate support by including the following Setup Option:
56+
57+
~~~
58+
SUBSCRIBE_DEMAND Setup Option {
59+
Option Key (vi64) = 0xC0117
60+
Option Value Length (vi64) = 0
61+
}
62+
~~~
63+
64+
The extension is available on a hop only if both endpoints on that hop included this option.
65+
The extension is negotiated independently on each hop: a relay MAY support it upstream but not downstream, or vice versa.
66+
67+
Negotiation is mandatory before the message is sent.
68+
{{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_DEMAND message cannot be sent speculatively.
69+
An endpoint MUST NOT send SUBSCRIBE_DEMAND on a hop that did not negotiate this extension.
70+
71+
72+
# SUBSCRIBE_DEMAND Message
73+
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).
74+
75+
~~~
76+
SUBSCRIBE_DEMAND Message {
77+
Type (vi64) = 0xC0117
78+
Length (16)
79+
Subscriptions Created (vi64)
80+
Subscriptions Closed (vi64)
81+
Group Request (vi64)
82+
}
83+
~~~
84+
85+
The message MUST NOT be the first message on the request stream; it follows the SUBSCRIBE ({{moqt}} Section 10.7) that opened the stream.
86+
It consumes no Request ID ({{moqt}} Section 10.1), and the receiver MUST NOT respond to it.
87+
A subscriber MAY send it any number of times over the life of the subscription to refresh the values.
88+
89+
**Subscriptions Created** and **Subscriptions Closed**:
90+
Cumulative counts, over the life of this subscription, of the downstream subscriptions it represents that have been created and closed respectively.
91+
The current demand — the number of subscribers presently receiving the Track through this subscription — is `Subscriptions Created - Subscriptions Closed`.
92+
A leaf subscriber represents only itself: `Subscriptions Created` is `1` and `Subscriptions Closed` is `0`.
93+
These are the defaults until a SUBSCRIBE_DEMAND is received, so a subscriber that represents only itself need not send the message.
94+
95+
**Group Request**:
96+
The minimum group the subscriber wants the publisher to produce.
97+
A value of `0` means no request: the publisher produces groups at its own cadence.
98+
A non-zero value `N` requests that the publisher produce a group with Group ID at least `N - 1`; the offset by one keeps `0` available as "no request" while leaving Group ID `0` requestable.
99+
See [Group Requests](#group-requests) for the semantics.
100+
101+
102+
# Semantics
103+
104+
## Audience Size
105+
The audience size is a reduction up the subscription tree.
106+
107+
A **leaf subscriber** (one that is not a relay) represents itself: `Subscriptions Created` of `1` and `Subscriptions Closed` of `0`, a demand of `1`.
108+
It need not report this default.
109+
110+
A **relay** that aggregates one or more downstream subscriptions for a Track into a single upstream subscription reports, on that upstream subscription, the **sum** of the `Subscriptions Created` of its downstreams and the **sum** of their `Subscriptions Closed`, treating a downstream that has not reported as `1` created and `0` closed.
111+
It increments `Subscriptions Created` as downstream subscriptions are created and `Subscriptions Closed` as they are closed, and SHOULD keep both counts non-decreasing over the upstream subscription's life — accounting a fully-departed downstream's outstanding demand (its last-reported `Created - Closed`) as newly closed — so that neither count moves backward when a downstream detaches.
112+
When either sum changes, the relay sends a SUBSCRIBE_DEMAND message upstream with the new totals.
113+
114+
Because each relay reports the sum of its subtree, the difference telescopes: at the origin, `Created - Closed` on a given upstream subscription is the total number of leaf subscribers reachable through that subscription, across any number of relay hops.
115+
A publisher reads its total audience for a Track as the sum of `Created - Closed` over the subscriptions it is serving.
116+
117+
Reporting the two counts separately, rather than a single current-demand gauge, lets a publisher distinguish *churn* from a *new arrival*: a rising `Subscriptions Created` means at least one new subscriber has joined, which a publisher MAY treat as an implicit [Group Request](#group-requests), since a newly-joined subscriber generally needs a fresh group (e.g. a keyframe) to begin decoding.
118+
119+
## Group Requests {#group-requests}
120+
A subscriber raises `Group Request` to ask the publisher to start a new group once it has fallen too far behind the live edge to catch up — for example, after missing the group with ID `5` it requests `6` to jump to the next group rather than wait for it to be produced naturally.
121+
122+
The request is a **level**, not an edge: it names the minimum group the subscriber needs, and is satisfied the moment a group at or beyond it exists.
123+
A publisher that has already produced a group with Group ID at or above the request takes no action — the request is already met.
124+
This is the key difference from a one-shot "produce a new group now" signal: because the request is idempotent, it can be retransmitted, coalesced, and aggregated without a publisher producing one redundant group per copy it receives.
125+
126+
`Group Request` fans *in* at a relay as the **maximum** of its downstreams' requests, minus any the relay can already satisfy itself: a relay that holds a group at or beyond a downstream's request serves it from cache and does not propagate it; it forwards a request upstream only when it lacks a group at or beyond the highest value its downstreams want.
127+
Once the publisher produces a group satisfying the highest request, every lower request is satisfied at once.
128+
129+
A publisher SHOULD honor a `Group Request` by producing a new group as soon as it can (subject to its own encoding constraints, such as a keyframe boundary), but MAY decline or defer it; the request does not override the publisher's control of its own Track.
130+
`Group Request` is the only field of this message that affects delivery. The audience-size counts MUST NOT influence prioritization, caching, congestion response, or any other distribution decision beyond the optional new-group hint above.
131+
132+
133+
# Rate Limiting
134+
Subscriber churn can change the audience size rapidly, and at a busy relay each change would otherwise produce an upstream SUBSCRIBE_DEMAND message.
135+
136+
A relay SHOULD rate-limit SUBSCRIBE_DEMAND messages per subscription, coalescing audience-size changes that occur within a short window (on the order of a second) and then sending the latest values.
137+
Because each message carries current values rather than deltas, a change that reverts within the window — a subscriber that joins and leaves, or leaves and returns — requires no upstream message at all.
138+
139+
A `Group Request` increase is latency-sensitive — the subscriber is stalled waiting for a group it can decode — and SHOULD be forwarded promptly rather than held for the audience-size window.
140+
Because the message is independent of REQUEST_UPDATE, neither kind of update delays a genuine subscription change: delivery-affecting updates are forwarded according to {{moqt}} without regard to the demand window.
141+
142+
143+
# Security Considerations
144+
**Audience disclosure.**
145+
`Subscriptions Created` and `Subscriptions Closed` disclose aggregate viewership to the publisher and to every relay on the path toward it.
146+
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).
147+
Because the extension is negotiated per hop, an endpoint that considers this sensitive simply does not advertise the SUBSCRIBE_DEMAND Setup Option, and no demand is exchanged on that hop.
148+
149+
**Untrusted values.**
150+
The values are supplied by the subscriber side and aggregated by intermediaries, none of which the publisher can fully trust.
151+
A malicious or buggy subscriber can report inflated or deflated counts, and a malicious relay can report any sums regardless of its actual downstream subscriptions.
152+
The audience-size counts 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.
153+
A `Group Request` can at most ask the publisher to produce a group it could already have produced for any subscriber, and a publisher MAY decline it; honoring requests at an unbounded rate would let a subscriber drive group production, so a publisher SHOULD bound the rate at which it acts on requests.
154+
155+
**Churn amplification.**
156+
A subscriber that rapidly joins and leaves, or repeatedly raises its `Group Request`, could attempt to amplify control traffic toward the origin.
157+
The rate-limiting in [Rate Limiting](#rate-limiting) bounds the audience-size case, and the idempotent, level-based `Group Request` collapses repeated identical requests into a single upstream value and a single produced group.
158+
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.
159+
160+
This extension introduces no other security considerations beyond those described in {{moqt}}.
161+
162+
163+
# IANA Considerations
164+
165+
This document requests the following registrations.
166+
167+
## MOQT Setup Options
168+
169+
This document requests a registration in the "MOQT Setup Options" registry ({{moqt}} Section 15.4), whose policy is Specification Required.
170+
moq-transport defines no private-use range for Setup Options; extensions request a (provisional) codepoint.
171+
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`).
172+
173+
| Value | Name | Reference |
174+
|:------|:-----|:----------|
175+
| 0xC0117 | SUBSCRIBE_DEMAND | This Document |
176+
177+
## MOQT Message Types
178+
179+
This document registers a control message type.
180+
{{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`).
181+
This is the same value as the SUBSCRIBE_DEMAND Setup Option above; Setup Options and message types are independent namespaces, so the shared value is unambiguous.
182+
183+
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.
184+
185+
| Value | Name | Stream | Reference |
186+
|:------|:-----|:-------|:----------|
187+
| 0xC0117 | SUBSCRIBE_DEMAND | Request | This Document |
188+
189+
190+
--- back
191+
192+
# Acknowledgments
193+
{:numbered="false"}
194+
195+
This document was drafted with the assistance of Claude, an AI assistant by Anthropic.

0 commit comments

Comments
 (0)