MoQT Draft-19: What Changed Since Draft-18
By moqtap team
MoQT Draft-19: What Changed Since Draft-18
Draft-ietf-moq-transport-19 was published this week, about seven weeks after draft-18. Where draft-18 was a sprawling rewrite that touched both the control plane and the data plane, draft-19 is more targeted — but it still lands genuinely breaking wire changes, and it introduces one substantial new feature: Range Filters.
This post is a focused diff: what changed, what broke, and what implementers need to update. We assume familiarity with draft-18 (see the draft-18 changes post if you are coming from further back).
At a glance
Session and control plane
- Range Filters — a new family of filter parameters that select Objects by Subgroup ID, Object ID, Publisher Priority, or Object/Track Property value
- New
MAX_FILTER_RANGESandMAX_REQUEST_UPDATESSetup Options, and aTOO_MANY_REQUEST_UPDATESsession error - Request ID removed from
GOAWAYentirely GROUP_ORDERmoves fromPUBLISH_OKtoSUBSCRIBE_TRACKS- Multiple concurrent subscriptions per Track are now allowed —
DUPLICATE_SUBSCRIPTIONis gone PUBLISH_BLOCKEDrenamed toPUBLISH_SKIPPEDSUBSCRIPTION_FILTERrenamed toLOCATION_FILTER- New
REQUEST_ERRORcodes:CONFLICTING_FILTERSandINVALID_FILTER - An unexpected
REQUEST_UPDATEis now a session error; FIN vs. RST/STOP_SENDING semantics on request streams are defined
Data plane
- Delivery timeouts are now both Track and Object Properties
- The “which Object Statuses may carry a payload” rule is now driven by an IANA registry instead of a hard-coded “status ≠ 0 means empty”
- Datagrams take precedence over streams in cross-forwarding-preference scheduling ties
- The relay exception permitting reordering or dropping of objects is removed; relay processing rules for known Track Properties are specified
- Immutable Properties are recommended for relay-visible, unmodifiable data
Editorial / process
- The control message
Payloadfield is renamedMessage Body - Group and Subgroup terminology and the object model are clarified
Crucially, the data-stream header layout did not change in draft-19. Subgroup headers, datagrams, and the FETCH header are byte-for-byte identical to draft-18 — the data-plane changes are semantic, not structural. That is a welcome contrast to draft-18, where the FETCH stream switched to delta encoding and the SUBGROUP_HEADER grew a bit.
The rest of this post walks the changes most likely to trip implementers.
1. Range Filters
This is the headline feature. Draft-18 let a subscriber pick a starting point with a Location Filter (Largest Object, Next Group Start, AbsoluteStart, AbsoluteRange). Draft-19 adds Range Filters: parameters that select which Objects within the subscription pass, based on integer ranges over Object header fields and Properties.
There are five new parameter types:
| Parameter | Type | Selects on |
|---|---|---|
SUBGROUP_FILTER | 0x25 | Subgroup ID |
OBJECTID_FILTER | 0x26 | Object ID |
PRIORITY_FILTER | 0x27 | Publisher Priority |
OBJECT_PROPERTY_FILTER | 0x28 | an Object Property value |
TRACK_PROPERTY_FILTER | 0x29 | a Track Property value |
They can appear in SUBSCRIBE, FETCH, SUBSCRIBE_TRACKS, PUBLISH_OK, and REQUEST_UPDATE. Each is a length-prefixed parameter whose value is a SetID (8 bits), an optional Property Type (for the two property filters), and a sequence of inclusive Start/End ranges:
SUBGROUP_FILTER { Type=0x25, Length, SetID, Range... }OBJECT_PROPERTY_FILTER { Type=0x28, Length, SetID, Property Type, Range... }Range { Start, [End] }Two encoding details are easy to get wrong:
- Ranges are delta-encoded. The first
Startis a delta from 0; eachEndis a delta from the currentStart; the nextStartis a delta from the priorEnd. To express3–5and10–15, you write03 02 05 05, not03 05 10 15. A delta that overflows2^64-1is rejected withINVALID_FILTER. - The final
Endmay be omitted to mean “no upper bound.” Because the parameter is length-prefixed, the decoder knows the value ended after aStartwith noEnd.
Sets combine with a familiar boolean shape: all filter parameters sharing a SetID are AND-ed together, and the different SetIDs are OR-ed. So SetID=0 AND ... OR SetID=1 AND ..., and so on.
Range Filters are gated by a new Setup Option, MAX_FILTER_RANGES (Type 0x06), which caps the total number of Ranges a peer may use across all filters on a single subscription or fetch. Its default is 0, meaning that unless you negotiate it, a peer MUST NOT send any Range Filter at all. Exceeding the limit — or sending a malformed filter, or a PRIORITY_FILTER value above 255, or a Property Filter with an odd Property Type — draws a REQUEST_ERROR with the new INVALID_FILTER (0x36) code.
The TRACK_PROPERTY_FILTER deserves special mention: in SUBSCRIBE_TRACKS, it filters which PUBLISH messages are forwarded at all, based on their Track Properties. A relay that cannot aggregate conflicting per-subscriber filters upstream can reject with the other new code, CONFLICTING_FILTERS (0x35).
2. GOAWAY loses its Request ID
Draft-18 added an optional Request ID to GOAWAY, present only when the message was sent on the control stream, to mark the boundary between processed and unprocessed requests. Draft-19 removes it entirely. The change log is one line: “Remove Request ID from GOAWAY (#1623).”
Draft-18 GOAWAY body (control stream): New Session URI Length | New Session URI | Timeout | Request ID
Draft-19 GOAWAY body (any stream): New Session URI Length | New Session URI | TimeoutOn the wire, an empty-URI, zero-timeout control-stream GOAWAY goes from 10 0003 00 00 00 to 10 0002 00 00. The upside is real simplification: control-stream and request-stream GOAWAY now share one identical format. Draft-19 also clarifies the division of labor — a GOAWAY on the control stream signals session migration, while a GOAWAY on a request stream migrates just that one request — but there is no longer a wire field distinguishing them.
3. GROUP_ORDER moves from PUBLISH_OK to SUBSCRIBE_TRACKS
In draft-18 the GROUP_ORDER parameter (0x22) could ride along in PUBLISH_OK. Draft-19 removes it from there and makes it valid in SUBSCRIBE_TRACKS instead (#1777). The parameter’s wire encoding is unchanged — it is still a uint8 with values Ascending (0x1) or Descending (0x2) — but the set of messages it may legally appear in has shifted.
The rationale tracks the draft-18 subscribe/subscribe-tracks split: group ordering is a property of how a subscriber wants tracks delivered, so it belongs on the request that establishes track publication. A SUBSCRIBE_TRACKS carrying GROUP_ORDER propagates that value into the PUBLISH messages it generates; omitting it falls back to the publisher’s default group-order preference. If your PUBLISH_OK encoder still emits GROUP_ORDER, a strict draft-19 peer will treat it as a parameter that is not valid for that message.
4. Multiple concurrent subscriptions per Track
Draft-18 rejected a second subscription to the same Track on the same session with a DUPLICATE_SUBSCRIPTION error. Draft-19 lifts that restriction — “Allow multiple concurrent subscriptions per Track (#1775)” — and, as a direct consequence, removes the DUPLICATE_SUBSCRIPTION error code (0x19) from the REQUEST_ERROR registry entirely.
This is one to watch in a translation layer: 0x19 is now unassigned. If you have a draft-18 code path that maps some internal “already subscribed” condition to 0x19, that mapping needs to disappear rather than be renumbered. The feature it enabled — a client holding several differently-filtered subscriptions to one Track at once — is exactly what the new Range Filters make useful.
5. MAX_REQUEST_UPDATES and TOO_MANY_REQUEST_UPDATES
Draft-19 adds flow control for REQUEST_UPDATE. The new MAX_REQUEST_UPDATES Setup Option (Type 0x08) tells the peer how many unacknowledged REQUEST_UPDATE messages it will tolerate on a single request stream at once. A REQUEST_UPDATE is outstanding from the moment it is sent until its REQUEST_OK or REQUEST_ERROR comes back; each response restores one credit.
Overrun the limit and the receiver closes the session with the new TOO_MANY_REQUEST_UPDATES code (0x1B) in the session-termination registry. The default value is 0, which means “no limit.” Relatedly, draft-19 tightens the rules around REQUEST_UPDATE itself: receiving one in a context where it is not expected is now a session error (#1784), and the spec spells out when coalescing updates is allowed.
6. Renames: PUBLISH_SKIPPED and LOCATION_FILTER
Two things were renamed without changing their bytes:
PUBLISH_BLOCKED→PUBLISH_SKIPPED(#1779). Same message type (0x0F), same body — a Track Namespace Suffix plus a Track Name. “Skipped” better describes the semantics: the publisher is telling aSUBSCRIBE_TRACKSsubscriber that it will not send aPUBLISHfor a particular track in the matching namespace.SUBSCRIPTION_FILTER→LOCATION_FILTERfor the parameter at type0x21. Also no wire change; the new name simply disambiguates it from the Range Filters that also live “on a subscription.”
Neither rename moves a byte, but both move a name, which matters for anyone whose decoder surfaces symbolic parameter names or whose test suite asserts on them.
7. Data plane: same bytes, sharper rules
The data-stream wire format is unchanged, but three semantic shifts are worth internalizing:
- Delivery timeouts are now Track and Object Properties (#1476).
OBJECT_DELIVERY_TIMEOUT(0x02) andSUBGROUP_DELIVERY_TIMEOUT(0x06) were Track-scoped in draft-18; in draft-19 they may also appear as per-Object Properties in the Object header. The encoding of the property is identical — this just widens where it is legal to set one. - Object Status payloads are now registry-driven (#1760). Draft-18 said flatly that any Object with a non-zero status MUST have an empty payload. Draft-19 replaces that with a per-status “Payload” column in the Object Status IANA registry, so a future status can be defined to carry a payload. For today’s three statuses — Normal (
0x0, payload allowed), End of Group (0x3), End of Track (0x4) — behavior is unchanged. - Relays lose their reordering/dropping exception (#1762) and gain explicit processing rules for known Track Properties (#1771). Combined with the recommendation to mark relay-visible, unmodifiable data with Immutable Properties (#1759), this is the working group tightening the relay contract as caching relays mature.
8. The smaller items worth knowing
Message Payloadis renamedMessage Body(#1756) in the control-message framing. Pure editorial — theType | Length | Bodyshape is unchanged — but it is the field every control-message parser touches.- Datagrams win scheduling ties. When two objects have equal priority but different forwarding preferences, datagrams now take precedence (#1780).
- Request-stream closure semantics are defined. Draft-19 nails down FIN vs. RST_STREAM/STOP_SENDING behavior on request streams (#1698), and clarifies session-level versus per-request GOAWAY migration (#1787).
- Namespace discovery and authorization trust for namespace subscriptions are clarified (#1710, #1656).
What this means for implementers
Upgrading a draft-18 implementation to draft-19 sorts into three buckets, in order of urgency:
- Wire-incompatible changes that will cause immediate protocol violations: drop the
Request IDfromGOAWAY; stop sendingGROUP_ORDERinPUBLISH_OK(send it inSUBSCRIBE_TRACKS); removeDUPLICATE_SUBSCRIPTION(0x19) from your error handling; renamePUBLISH_BLOCKEDtoPUBLISH_SKIPPED. - New features to understand even if you do not fully implement them: parse the five Range Filter parameters (
0x25–0x29) and their delta-encoded ranges; honorMAX_FILTER_RANGESand reject overruns withINVALID_FILTER; acceptMAX_REQUEST_UPDATESand enforce it withTOO_MANY_REQUEST_UPDATES; recognize the newCONFLICTING_FILTERSandINVALID_FILTERerror codes. - Semantic shifts that will not crash anything but change behavior: delivery timeouts as Object Properties, the registry-driven Object Status payload rule, datagram scheduling precedence, and the tightened relay reordering rules.
The moqtap test vectors repo now ships a complete draft19/ directory — every control message, data-stream header, and a representative set of error responses, with hex strings that are wire-faithful to the spec. The draft-19 set adds dedicated vectors for each new Range Filter, including the delta-encoded multi-range and open-ended cases (03 02 05 05 for ranges 3–5 and 10–15), the new Setup Options, and the relocated GROUP_ORDER. If you are porting an implementation, those vectors are the fastest way to verify your decoder against the new format.
Looking ahead
Draft-19 is a smaller, more surgical release than draft-18 — no data-stream header churn, and most of the weight in one coherent feature. Range Filters are the interesting story: together with multiple-subscriptions-per-track, they push MoQT toward a model where a single connection can carry several narrowly-scoped views of the same Track, filtered by subgroup, object, priority, or property. That is a meaningful capability for selective forwarding and bandwidth-adaptive delivery.
If you are debugging a session and not sure which draft your endpoints actually speak, moqtap auto-detects the version on the wire and shows you exactly where two implementations disagree on the bytes.