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_RANGES and MAX_REQUEST_UPDATES Setup Options, and a TOO_MANY_REQUEST_UPDATES session error
  • Request ID removed from GOAWAY entirely
  • GROUP_ORDER moves from PUBLISH_OK to SUBSCRIBE_TRACKS
  • Multiple concurrent subscriptions per Track are now allowed — DUPLICATE_SUBSCRIPTION is gone
  • PUBLISH_BLOCKED renamed to PUBLISH_SKIPPED
  • SUBSCRIPTION_FILTER renamed to LOCATION_FILTER
  • New REQUEST_ERROR codes: CONFLICTING_FILTERS and INVALID_FILTER
  • An unexpected REQUEST_UPDATE is 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 Payload field is renamed Message 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:

ParameterTypeSelects on
SUBGROUP_FILTER0x25Subgroup ID
OBJECTID_FILTER0x26Object ID
PRIORITY_FILTER0x27Publisher Priority
OBJECT_PROPERTY_FILTER0x28an Object Property value
TRACK_PROPERTY_FILTER0x29a 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 Start is a delta from 0; each End is a delta from the current Start; the next Start is a delta from the prior End. To express 3–5 and 10–15, you write 03 02 05 05, not 03 05 10 15. A delta that overflows 2^64-1 is rejected with INVALID_FILTER.
  • The final End may be omitted to mean “no upper bound.” Because the parameter is length-prefixed, the decoder knows the value ended after a Start with no End.

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 | Timeout

On 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_BLOCKEDPUBLISH_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 a SUBSCRIBE_TRACKS subscriber that it will not send a PUBLISH for a particular track in the matching namespace.
  • SUBSCRIPTION_FILTERLOCATION_FILTER for the parameter at type 0x21. 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) and SUBGROUP_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 Payload is renamed Message Body (#1756) in the control-message framing. Pure editorial — the Type | Length | Body shape 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:

  1. Wire-incompatible changes that will cause immediate protocol violations: drop the Request ID from GOAWAY; stop sending GROUP_ORDER in PUBLISH_OK (send it in SUBSCRIBE_TRACKS); remove DUPLICATE_SUBSCRIPTION (0x19) from your error handling; rename PUBLISH_BLOCKED to PUBLISH_SKIPPED.
  2. New features to understand even if you do not fully implement them: parse the five Range Filter parameters (0x250x29) and their delta-encoded ranges; honor MAX_FILTER_RANGES and reject overruns with INVALID_FILTER; accept MAX_REQUEST_UPDATES and enforce it with TOO_MANY_REQUEST_UPDATES; recognize the new CONFLICTING_FILTERS and INVALID_FILTER error codes.
  3. 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.