From 09b31d6694e392351671117d44fff8fc3fe72a0a Mon Sep 17 00:00:00 2001 From: Manish Date: Tue, 10 Mar 2026 16:51:04 +0530 Subject: [PATCH 01/16] feat: base types updated for draft-16 --- lib/transport/base_data.ts | 430 +++++++++++++----- lib/transport/connection.ts | 13 +- lib/transport/control/client_setup.ts | 53 +-- lib/transport/control/fetch.ts | 253 +++++------ lib/transport/control/fetch_error.ts | 36 -- lib/transport/control/fetch_ok.ts | 88 ++-- lib/transport/control/index.ts | 321 ++++++------- lib/transport/control/max_request_id.ts | 12 +- lib/transport/control/namespace.ts | 27 ++ lib/transport/control/namespace_done.ts | 27 ++ lib/transport/control/publish.ts | 106 ++--- lib/transport/control/publish_done.ts | 59 +-- lib/transport/control/publish_error.ts | 35 -- lib/transport/control/publish_namespace.ts | 57 +-- .../control/publish_namespace_cancel.ts | 35 ++ .../control/publish_namespace_done.ts | 36 +- .../control/publish_namespace_error.ts | 36 -- lib/transport/control/publish_namespace_ok.ts | 26 -- lib/transport/control/publish_ok.ts | 90 ++-- lib/transport/control/request_error.ts | 41 ++ lib/transport/control/request_ok.ts | 36 ++ lib/transport/control/requests_block.ts | 27 -- lib/transport/control/requests_blocked.ts | 30 ++ lib/transport/control/server_setup.ts | 41 +- lib/transport/control/subscribe.ts | 197 ++++---- lib/transport/control/subscribe_error.ts | 35 -- lib/transport/control/subscribe_namespace.ts | 70 +-- .../control/subscribe_namespace_error.ts | 36 -- .../control/subscribe_namespace_ok.ts | 28 -- lib/transport/control/subscribe_ok.ts | 102 ++--- lib/transport/control/subscribe_update.ts | 75 ++- lib/transport/control/track_status.ts | 42 ++ lib/transport/objects.ts | 158 ++++--- lib/transport/stream.ts | 111 ++--- lib/transport/subgroup.ts | 122 +++-- 35 files changed, 1522 insertions(+), 1369 deletions(-) delete mode 100644 lib/transport/control/fetch_error.ts create mode 100644 lib/transport/control/namespace.ts create mode 100644 lib/transport/control/namespace_done.ts delete mode 100644 lib/transport/control/publish_error.ts create mode 100644 lib/transport/control/publish_namespace_cancel.ts delete mode 100644 lib/transport/control/publish_namespace_error.ts delete mode 100644 lib/transport/control/publish_namespace_ok.ts create mode 100644 lib/transport/control/request_error.ts create mode 100644 lib/transport/control/request_ok.ts delete mode 100644 lib/transport/control/requests_block.ts create mode 100644 lib/transport/control/requests_blocked.ts delete mode 100644 lib/transport/control/subscribe_error.ts delete mode 100644 lib/transport/control/subscribe_namespace_error.ts delete mode 100644 lib/transport/control/subscribe_namespace_ok.ts create mode 100644 lib/transport/control/track_status.ts diff --git a/lib/transport/base_data.ts b/lib/transport/base_data.ts index dc90d56..43ff993 100644 --- a/lib/transport/base_data.ts +++ b/lib/transport/base_data.ts @@ -4,140 +4,338 @@ export type Tuple = TupleField[] export type TupleField = T // can be any type export namespace Tuple { - // Serialize implementation - only works for types where TupleField.serialize is implemented - export function serialize(tuple: Tuple): Uint8Array { - const buf = new MutableBytesBuffer(new Uint8Array()) - buf.putVarInt(tuple.length) - tuple.forEach(field => { - const serialized = TupleField.serialize(field) - buf.putBytes(serialized) - }) - return buf.Uint8Array - } - - // Deserialize implementation - only works for types where TupleField.deserialize is implemented - export function deserialize(buffer: ImmutableBytesBuffer): Tuple { - const tuple: T[] = [] - const len = buffer.getVarInt() - for (let i = 0; i < len; i++) { - const field = TupleField.deserialize(buffer) - tuple.push(field) - } - return tuple - } + // Serialize implementation - only works for types where TupleField.serialize is implemented + export function serialize(tuple: Tuple): Uint8Array { + const buf = new MutableBytesBuffer(new Uint8Array()) + buf.putVarInt(tuple.length) + tuple.forEach(field => { + const serialized = TupleField.serialize(field) + buf.putBytes(serialized) + }) + return buf.Uint8Array + } + + // Deserialize implementation - only works for types where TupleField.deserialize is implemented + export function deserialize(buffer: ImmutableBytesBuffer): Tuple { + const tuple: T[] = [] + const len = buffer.getVarInt() + for (let i = 0; i < len; i++) { + const field = TupleField.deserialize(buffer) + tuple.push(field) + } + return tuple + } } export namespace TupleField { - // Serialize implementation for string fields only - export function serialize(field: T): Uint8Array { - const buf = new MutableBytesBuffer(new Uint8Array()) - const encoded = new TextEncoder().encode(field) - buf.putVarInt(encoded.length) - buf.putBytes(encoded) - return buf.Uint8Array - } - - // Deserialize implementation for string fields only - export function deserialize(buffer: ImmutableBytesBuffer): T { - const field = buffer.getVarBytes() - return new TextDecoder().decode(field) as T - } + // Serialize implementation for string fields only + export function serialize(field: T): Uint8Array { + const buf = new MutableBytesBuffer(new Uint8Array()) + const encoded = new TextEncoder().encode(field) + buf.putVarInt(encoded.length) + buf.putBytes(encoded) + return buf.Uint8Array + } + + // Deserialize implementation for string fields only + export function deserialize(buffer: ImmutableBytesBuffer): T { + const field = buffer.getVarBytes() + return new TextDecoder().decode(field) as T + } } export type Location = { - group: bigint - object: bigint + group: bigint + object: bigint } export namespace Location { - export function serialize(location: Location): Uint8Array { - const buf = new MutableBytesBuffer(new Uint8Array()) - buf.putVarInt(location.group) - buf.putVarInt(location.object) - return buf.Uint8Array - } - - export function deserialize(buffer: ImmutableBytesBuffer): Location { - const group = buffer.getVarInt() - const object = buffer.getVarInt() - return { group, object } - } + export function serialize(location: Location): Uint8Array { + const buf = new MutableBytesBuffer(new Uint8Array()) + buf.putVarInt(location.group) + buf.putVarInt(location.object) + return buf.Uint8Array + } + + export function deserialize(buffer: ImmutableBytesBuffer): Location { + const group = buffer.getVarInt() + const object = buffer.getVarInt() + return { group, object } + } } -// TODO(itzmanish): have checks for key type for even or odd +// Draft-16: Key-Value-Pairs use delta-encoded types (Section 1.4.2) +// Delta Type is delta from previous type (or 0 if first) +// Type even => value is varint (no length prefix) +// Type odd => value is length-prefixed bytes export type KeyValuePairs = Map export type Parameters = KeyValuePairs export namespace KeyValuePairs { - export function valueIsVarInt(key: bigint): boolean { - return (key & 1n) === 0n - } - - export function serialize(pairs: Parameters): Uint8Array { - const buf = new MutableBytesBuffer(new Uint8Array()) - buf.putVarInt(pairs.size) - pairs.forEach((value, key) => { - buf.putVarInt(key) - if (valueIsVarInt(key)) { - buf.putVarInt(value as bigint) - } else { - buf.putBytes(value as Uint8Array) - } - }) - - return buf.Uint8Array - } - - export function deserialize(buffer: ImmutableBytesBuffer): KeyValuePairs { - const size = buffer.getNumberVarInt() - return deserialize_with_size(buffer, size) - } - - export function deserialize_with_size(buffer: ImmutableBytesBuffer, size: number): KeyValuePairs { - const pairs = new Map() - for (let i = 0; i < size; i++) { - const key = buffer.getVarInt() - const value = valueIsVarInt(key) ? buffer.getVarInt() : buffer.getVarBytes() - pairs.set(key, value) - } - - return pairs - } - - export async function deserialize_with_reader(reader: Reader): Promise { - const size = await reader.getNumberVarInt() - const pairs = new Map() - for (let i = 0; i < size; i++) { - const key = await reader.getVarInt() - const value = valueIsVarInt(key) ? await reader.getVarInt() : await reader.getVarBytes() - pairs.set(key, value) - } - - return pairs - } + export function valueIsVarInt(key: bigint): boolean { + return (key & 1n) === 0n + } + + export function serialize(pairs: KeyValuePairs): Uint8Array { + const buf = new MutableBytesBuffer(new Uint8Array()) + let prevType = 0n + // Sort keys to ensure ascending order for delta encoding + const sortedEntries = [...pairs.entries()].sort((a, b) => { + if (a[0] < b[0]) return -1 + if (a[0] > b[0]) return 1 + return 0 + }) + for (const [key, value] of sortedEntries) { + const delta = key - prevType + buf.putVarInt(delta) + if (valueIsVarInt(key)) { + buf.putVarInt(value as bigint) + } else { + const bytes = value as Uint8Array + buf.putVarInt(bytes.length) + buf.putBytes(bytes) + } + prevType = key + } + return buf.Uint8Array + } + + export function deserialize(buffer: ImmutableBytesBuffer): KeyValuePairs { + const pairs = new Map() + let prevType = 0n + while (buffer.remaining > 0) { + const delta = buffer.getVarInt() + const resolvedType = prevType + delta + if (valueIsVarInt(resolvedType)) { + const value = buffer.getVarInt() + pairs.set(resolvedType, value) + } else { + const length = buffer.getNumberVarInt() + const value = buffer.getBytes(length) + pairs.set(resolvedType, value) + } + prevType = resolvedType + } + return pairs + } + + export function deserialize_with_count(buffer: ImmutableBytesBuffer, count: number): KeyValuePairs { + const pairs = new Map() + let prevType = 0n + for (let i = 0; i < count; i++) { + const delta = buffer.getVarInt() + const resolvedType = prevType + delta + if (valueIsVarInt(resolvedType)) { + const value = buffer.getVarInt() + pairs.set(resolvedType, value) + } else { + const length = buffer.getNumberVarInt() + const value = buffer.getBytes(length) + pairs.set(resolvedType, value) + } + prevType = resolvedType + } + return pairs + } + + export async function deserialize_with_reader(reader: Reader): Promise { + const pairs = new Map() + let prevType = 0n + while (!(await reader.done())) { + const delta = await reader.getVarInt() + const resolvedType = prevType + delta + if (valueIsVarInt(resolvedType)) { + const value = await reader.getVarInt() + pairs.set(resolvedType, value) + } else { + const length = await reader.getNumberVarInt() + const value = await reader.read(length) + pairs.set(resolvedType, value) + } + prevType = resolvedType + } + return pairs + } + + export async function deserialize_with_reader_count(reader: Reader, count: number): Promise { + const pairs = new Map() + let prevType = 0n + for (let i = 0; i < count; i++) { + const delta = await reader.getVarInt() + const resolvedType = prevType + delta + if (valueIsVarInt(resolvedType)) { + const value = await reader.getVarInt() + pairs.set(resolvedType, value) + } else { + const length = await reader.getNumberVarInt() + const value = await reader.read(length) + pairs.set(resolvedType, value) + } + prevType = resolvedType + } + return pairs + } } export namespace Parameters { - export function valueIsVarInt(key: bigint): boolean { - return KeyValuePairs.valueIsVarInt(key) - } - - export function serialize(pairs: Parameters): Uint8Array { - return KeyValuePairs.serialize(pairs) - } - - export function deserialize(buffer: ImmutableBytesBuffer): Parameters { - return KeyValuePairs.deserialize(buffer) - } - - export function deserialize_with_size(buffer: ImmutableBytesBuffer, size: number): Parameters { - return KeyValuePairs.deserialize_with_size(buffer, size) - } - - export async function deserialize_with_reader(reader: Reader): Promise { - return KeyValuePairs.deserialize_with_reader(reader) - } -} \ No newline at end of file + export function valueIsVarInt(key: bigint): boolean { + return KeyValuePairs.valueIsVarInt(key) + } + + export function serialize(pairs: Parameters): Uint8Array { + return KeyValuePairs.serialize(pairs) + } + + export function deserialize(buffer: ImmutableBytesBuffer): Parameters { + return KeyValuePairs.deserialize(buffer) + } + + export function deserialize_with_count(buffer: ImmutableBytesBuffer, count: number): Parameters { + return KeyValuePairs.deserialize_with_count(buffer, count) + } + + export async function deserialize_with_reader(reader: Reader): Promise { + return KeyValuePairs.deserialize_with_reader(reader) + } + + export async function deserialize_with_reader_count(reader: Reader, count: number): Promise { + return KeyValuePairs.deserialize_with_reader_count(reader, count) + } +} + + +// Draft-16: Reason Phrase structure (Section 1.4.3) +// Max length is 1024 bytes +export const REASON_PHRASE_MAX_LENGTH = 1024 + +export type ReasonPhrase = string + +export namespace ReasonPhrase { + export function serialize(reason: ReasonPhrase): Uint8Array { + const buf = new MutableBytesBuffer(new Uint8Array()) + const encoded = new TextEncoder().encode(reason) + if (encoded.length > REASON_PHRASE_MAX_LENGTH) { + throw new Error(`Reason phrase exceeds max length of ${REASON_PHRASE_MAX_LENGTH} bytes`) + } + buf.putVarInt(encoded.length) + buf.putBytes(encoded) + return buf.Uint8Array + } + + export function deserialize(buffer: ImmutableBytesBuffer): ReasonPhrase { + const length = buffer.getNumberVarInt() + if (length > REASON_PHRASE_MAX_LENGTH) { + throw new Error(`Reason phrase length ${length} exceeds max of ${REASON_PHRASE_MAX_LENGTH}`) + } + const bytes = buffer.getBytes(length) + return new TextDecoder().decode(bytes) + } + + export async function deserialize_with_reader(reader: Reader): Promise { + const length = await reader.getNumberVarInt() + if (length > REASON_PHRASE_MAX_LENGTH) { + throw new Error(`Reason phrase length ${length} exceeds max of ${REASON_PHRASE_MAX_LENGTH}`) + } + const bytes = await reader.read(length) + return new TextDecoder().decode(bytes) + } +} + + +// Draft-16: Parameter Type constants (Section 13.2, Table 8) +export enum ParameterType { + DELIVERY_TIMEOUT = 0x02, + AUTHORIZATION_TOKEN = 0x03, + EXPIRES = 0x08, + LARGEST_OBJECT = 0x09, + FORWARD = 0x10, + SUBSCRIBER_PRIORITY = 0x20, + SUBSCRIPTION_FILTER = 0x21, + GROUP_ORDER = 0x22, + NEW_GROUP_REQUEST = 0x32, +} + + +// Draft-16: Extension Header Type constants (Section 13.3, Table 9) +export enum ExtensionHeaderType { + DELIVERY_TIMEOUT = 0x02, + MAX_CACHE_DURATION = 0x04, + IMMUTABLE_EXTENSIONS = 0x0B, + DEFAULT_PUBLISHER_PRIORITY = 0x0E, + DEFAULT_PUBLISHER_GROUP_ORDER = 0x22, + DYNAMIC_GROUPS = 0x30, + PRIOR_GROUP_ID_GAP = 0x3C, + PRIOR_OBJECT_ID_GAP = 0x3E, +} + + +// Draft-16: Session Termination Error Codes (Section 13.4.1, Table 10) +export enum SessionTerminationError { + NO_ERROR = 0x0, + INTERNAL_ERROR = 0x1, + UNAUTHORIZED = 0x2, + PROTOCOL_VIOLATION = 0x3, + INVALID_REQUEST_ID = 0x4, + DUPLICATE_TRACK_ALIAS = 0x5, + KEY_VALUE_FORMATTING_ERROR = 0x6, + TOO_MANY_REQUESTS = 0x7, + INVALID_PATH = 0x8, + MALFORMED_PATH = 0x9, + GOAWAY_TIMEOUT = 0x10, + CONTROL_MESSAGE_TIMEOUT = 0x11, + DATA_STREAM_TIMEOUT = 0x12, + AUTH_TOKEN_CACHE_OVERFLOW = 0x13, + DUPLICATE_AUTH_TOKEN_ALIAS = 0x14, + VERSION_NEGOTIATION_FAILED = 0x15, + MALFORMED_AUTH_TOKEN = 0x16, + UNKNOWN_AUTH_TOKEN_ALIAS = 0x17, + EXPIRED_AUTH_TOKEN = 0x18, + INVALID_AUTHORITY = 0x19, + MALFORMED_AUTHORITY = 0x1A, +} + + +// Draft-16: REQUEST_ERROR Codes (Section 13.4.2, Table 11) +export enum RequestErrorCode { + INTERNAL_ERROR = 0x0, + UNAUTHORIZED = 0x1, + TIMEOUT = 0x2, + NOT_SUPPORTED = 0x3, + MALFORMED_AUTH_TOKEN = 0x4, + EXPIRED_AUTH_TOKEN = 0x5, + DOES_NOT_EXIST = 0x10, + INVALID_RANGE = 0x11, + MALFORMED_TRACK = 0x12, + DUPLICATE_SUBSCRIPTION = 0x19, + UNINTERESTED = 0x20, + PREFIX_OVERLAP = 0x30, + INVALID_JOINING_REQUEST_ID = 0x32, +} + + +// Draft-16: PUBLISH_DONE Codes (Section 13.4.3, Table 12) +export enum PublishDoneCode { + INTERNAL_ERROR = 0x0, + UNAUTHORIZED = 0x1, + TRACK_ENDED = 0x2, + SUBSCRIPTION_ENDED = 0x3, + GOING_AWAY = 0x4, + EXPIRED = 0x5, + TOO_FAR_BEHIND = 0x6, + UPDATE_FAILED = 0x8, + MALFORMED_TRACK = 0x12, +} + + +// Draft-16: Data Stream Reset Error Codes (Section 13.4.4, Table 13) +export enum DataStreamResetCode { + INTERNAL_ERROR = 0x0, + CANCELLED = 0x1, + DELIVERY_TIMEOUT = 0x2, + SESSION_CLOSED = 0x3, + UNKNOWN_OBJECT_STATUS = 0x4, + MALFORMED_TRACK = 0x12, +} diff --git a/lib/transport/connection.ts b/lib/transport/connection.ts index 2c0f74f..418f376 100644 --- a/lib/transport/connection.ts +++ b/lib/transport/connection.ts @@ -95,7 +95,18 @@ export class Connection { } async #recv(msg: Control.MessageWithType) { - if (Control.isPublisher(msg.type)) { + // RequestOk and RequestError can be sent by either side, + // so route based on request ID parity (even=subscriber-initiated, odd=publisher-initiated) + if (msg.type === Control.ControlMessageType.RequestOk || msg.type === Control.ControlMessageType.RequestError) { + const id = (msg.message as { id: bigint }).id + if (id % 2n === 0n) { + // Even request ID = subscriber-initiated request, response goes to subscriber + await this.#subscriber.recv(msg) + } else { + // Odd request ID = publisher-initiated request, response goes to publisher + await this.#publisher.recv(msg) + } + } else if (Control.isPublisher(msg.type)) { await this.#subscriber.recv(msg) } else { await this.#publisher.recv(msg) diff --git a/lib/transport/control/client_setup.ts b/lib/transport/control/client_setup.ts index 24c190c..a1ac3f8 100644 --- a/lib/transport/control/client_setup.ts +++ b/lib/transport/control/client_setup.ts @@ -1,38 +1,31 @@ import { ControlMessageType, Version } from "." -import { KeyValuePairs, Parameters } from "../base_data" +import { Parameters } from "../base_data" import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" export interface ClientSetup { - versions: Version[] - params: Parameters + params: Parameters } export namespace ClientSetup { - export function serialize(v: ClientSetup): Uint8Array { - const mainBuf = new MutableBytesBuffer(new Uint8Array()) - mainBuf.putVarInt(ControlMessageType.ClientSetup) - const payloadBuf = new MutableBytesBuffer(new Uint8Array()) - payloadBuf.putVarInt(v.versions.length) - v.versions.forEach((version) => { - payloadBuf.putVarInt(version) - }) - payloadBuf.putBytes(Parameters.serialize(v.params)) - mainBuf.putU16(payloadBuf.byteLength) - mainBuf.putBytes(payloadBuf.Uint8Array) - console.log("client setup: payload len:", payloadBuf.length, "msg len:", mainBuf.length) - return mainBuf.Uint8Array - } + export function serialize(v: ClientSetup): Uint8Array { + const mainBuf = new MutableBytesBuffer(new Uint8Array()) + mainBuf.putVarInt(ControlMessageType.ClientSetup) + const payloadBuf = new MutableBytesBuffer(new Uint8Array()) + // Draft-16: Number of Parameters + delta-encoded parameters only + const paramsBytes = Parameters.serialize(v.params) + payloadBuf.putVarInt(v.params.size) + payloadBuf.putBytes(paramsBytes) + mainBuf.putU16(payloadBuf.byteLength) + mainBuf.putBytes(payloadBuf.Uint8Array) + return mainBuf.Uint8Array + } - export function deserialize(reader: ImmutableBytesBuffer): ClientSetup { - const supportedVersionLen = reader.getNumberVarInt() - const versions: Version[] = [] - for (let i = 0; i < supportedVersionLen; i++) { - versions.push(reader.getNumberVarInt() as Version) - } - const params = Parameters.deserialize(reader) - return { - versions, - params - } - } -} \ No newline at end of file + export function deserialize(reader: ImmutableBytesBuffer): ClientSetup { + // Draft-16: Number of Parameters + parameters + const numParams = reader.getNumberVarInt() + const params = Parameters.deserialize_with_count(reader, numParams) + return { + params + } + } +} diff --git a/lib/transport/control/fetch.ts b/lib/transport/control/fetch.ts index 2d0e466..088bd63 100644 --- a/lib/transport/control/fetch.ts +++ b/lib/transport/control/fetch.ts @@ -1,158 +1,153 @@ -import { ControlMessageType, GroupOrder } from "." +import { ControlMessageType } from "." import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" import { Location, Tuple, Parameters } from "../base_data" export enum FetchType { - Standalone = 0x1, - Relative = 0x2, - Absolute = 0x3, + Standalone = 0x1, + Relative = 0x2, + Absolute = 0x3, } export namespace FetchType { - export function serialize(v: FetchType): Uint8Array { - const buf = new MutableBytesBuffer(new Uint8Array()) - buf.putVarInt(v) - return buf.Uint8Array - } - - export function deserialize(buffer: ImmutableBytesBuffer): FetchType { - const order = buffer.getVarInt() - switch (order) { - case 1n: - return FetchType.Standalone - case 2n: - return FetchType.Relative - case 3n: - return FetchType.Absolute - default: - throw new Error(`Invalid FetchType value: ${order}`) - } - } + export function serialize(v: FetchType): Uint8Array { + const buf = new MutableBytesBuffer(new Uint8Array()) + buf.putVarInt(v) + return buf.Uint8Array + } + + export function deserialize(buffer: ImmutableBytesBuffer): FetchType { + const order = buffer.getVarInt() + switch (order) { + case 1n: + return FetchType.Standalone + case 2n: + return FetchType.Relative + case 3n: + return FetchType.Absolute + default: + throw new Error(`Invalid FetchType value: ${order}`) + } + } } export interface StandaloneFetch { - namespace: Tuple - name: string - start_location: Location - end_location: Location + namespace: Tuple + name: string + start_location: Location + end_location: Location } export namespace StandaloneFetch { - export function serialize(v: StandaloneFetch): Uint8Array { - const mainBuf = new MutableBytesBuffer(new Uint8Array()) - mainBuf.putVarInt(ControlMessageType.Fetch) - const payloadBuf = new MutableBytesBuffer(new Uint8Array()) - payloadBuf.putBytes(Tuple.serialize(v.namespace)) - payloadBuf.putUtf8String(v.name) - payloadBuf.putBytes(Location.serialize(v.start_location)) - payloadBuf.putBytes(Location.serialize(v.end_location)) - - mainBuf.putU16(payloadBuf.byteLength) - mainBuf.putBytes(payloadBuf.Uint8Array) - return mainBuf.Uint8Array - } - - export function deserialize(r: ImmutableBytesBuffer): StandaloneFetch { - const namespace = Tuple.deserialize(r) - const name = r.getUtf8String() - const start_location = Location.deserialize(r) - const end_location = Location.deserialize(r) - return { - namespace, - name, - start_location, - end_location - } - } + export function serialize(v: StandaloneFetch): Uint8Array { + const mainBuf = new MutableBytesBuffer(new Uint8Array()) + mainBuf.putVarInt(ControlMessageType.Fetch) + const payloadBuf = new MutableBytesBuffer(new Uint8Array()) + payloadBuf.putBytes(Tuple.serialize(v.namespace)) + payloadBuf.putUtf8String(v.name) + payloadBuf.putBytes(Location.serialize(v.start_location)) + payloadBuf.putBytes(Location.serialize(v.end_location)) + + mainBuf.putU16(payloadBuf.byteLength) + mainBuf.putBytes(payloadBuf.Uint8Array) + return mainBuf.Uint8Array + } + + export function deserialize(r: ImmutableBytesBuffer): StandaloneFetch { + const namespace = Tuple.deserialize(r) + const name = r.getUtf8String() + const start_location = Location.deserialize(r) + const end_location = Location.deserialize(r) + return { + namespace, + name, + start_location, + end_location + } + } } export interface JoiningFetch { - id: bigint - start: bigint + id: bigint + start: bigint } export namespace JoiningFetch { - export function serialize(v: JoiningFetch): Uint8Array { - const mainBuf = new MutableBytesBuffer(new Uint8Array()) - mainBuf.putVarInt(ControlMessageType.Fetch) - const payloadBuf = new MutableBytesBuffer(new Uint8Array()) - payloadBuf.putVarInt(v.id) - payloadBuf.putVarInt(v.start) - - mainBuf.putU16(payloadBuf.byteLength) - mainBuf.putBytes(payloadBuf.Uint8Array) - return mainBuf.Uint8Array - } - - export function deserialize(r: ImmutableBytesBuffer): JoiningFetch { - const id = r.getVarInt() - const start = r.getVarInt() - return { - id, - start - } - } + export function serialize(v: JoiningFetch): Uint8Array { + const mainBuf = new MutableBytesBuffer(new Uint8Array()) + mainBuf.putVarInt(ControlMessageType.Fetch) + const payloadBuf = new MutableBytesBuffer(new Uint8Array()) + payloadBuf.putVarInt(v.id) + payloadBuf.putVarInt(v.start) + + mainBuf.putU16(payloadBuf.byteLength) + mainBuf.putBytes(payloadBuf.Uint8Array) + return mainBuf.Uint8Array + } + + export function deserialize(r: ImmutableBytesBuffer): JoiningFetch { + const id = r.getVarInt() + const start = r.getVarInt() + return { + id, + start + } + } } export interface Fetch { - id: bigint - subscriber_priority: number - group_order: GroupOrder - fetch_type: FetchType - standalone?: StandaloneFetch - joining?: JoiningFetch - params?: Parameters + id: bigint + fetch_type: FetchType + standalone?: StandaloneFetch + joining?: JoiningFetch + params?: Parameters } export namespace Fetch { - export function serialize(v: Fetch): Uint8Array { - const mainBuf = new MutableBytesBuffer(new Uint8Array()) - mainBuf.putVarInt(ControlMessageType.Fetch) - const payloadBuf = new MutableBytesBuffer(new Uint8Array()) - payloadBuf.putVarInt(v.id) - payloadBuf.putU8(v.subscriber_priority) - payloadBuf.putBytes(GroupOrder.serialize(v.group_order)) - payloadBuf.putBytes(FetchType.serialize(v.fetch_type)) - if (v.standalone) { - payloadBuf.putBytes(StandaloneFetch.serialize(v.standalone)) - } - if (v.joining) { - payloadBuf.putBytes(JoiningFetch.serialize(v.joining)) - } - payloadBuf.putBytes(Parameters.serialize(v.params ?? new Map())) - - mainBuf.putU16(payloadBuf.byteLength) - mainBuf.putBytes(payloadBuf.Uint8Array) - return mainBuf.Uint8Array - } - - export function deserialize(reader: ImmutableBytesBuffer): Fetch { - const id = reader.getVarInt() - const subscriber_priority = reader.getU8() - const group_order = GroupOrder.deserialize(reader) - const fetch_type = FetchType.deserialize(reader) - let standalone: StandaloneFetch | undefined - let joining: JoiningFetch | undefined - let params: Parameters | undefined - if (fetch_type == FetchType.Standalone) { - standalone = StandaloneFetch.deserialize(reader) - } else if (fetch_type == FetchType.Relative || fetch_type == FetchType.Absolute) { - joining = JoiningFetch.deserialize(reader) - } - if (reader.remaining > 0) { - params = Parameters.deserialize(reader) - } - return { - id, - subscriber_priority, - group_order, - fetch_type, - standalone, - joining, - params - } - } + export function serialize(v: Fetch): Uint8Array { + const mainBuf = new MutableBytesBuffer(new Uint8Array()) + mainBuf.putVarInt(ControlMessageType.Fetch) + const payloadBuf = new MutableBytesBuffer(new Uint8Array()) + payloadBuf.putVarInt(v.id) + payloadBuf.putBytes(FetchType.serialize(v.fetch_type)) + if (v.standalone) { + payloadBuf.putBytes(StandaloneFetch.serialize(v.standalone)) + } + if (v.joining) { + payloadBuf.putBytes(JoiningFetch.serialize(v.joining)) + } + const params = v.params ?? new Map() + payloadBuf.putVarInt(BigInt(params.size)) + payloadBuf.putBytes(Parameters.serialize(params)) + + mainBuf.putU16(payloadBuf.byteLength) + mainBuf.putBytes(payloadBuf.Uint8Array) + return mainBuf.Uint8Array + } + + export function deserialize(reader: ImmutableBytesBuffer): Fetch { + const id = reader.getVarInt() + const fetch_type = FetchType.deserialize(reader) + let standalone: StandaloneFetch | undefined + let joining: JoiningFetch | undefined + let params: Parameters | undefined + if (fetch_type == FetchType.Standalone) { + standalone = StandaloneFetch.deserialize(reader) + } else if (fetch_type == FetchType.Relative || fetch_type == FetchType.Absolute) { + joining = JoiningFetch.deserialize(reader) + } + const numParams = reader.getNumberVarInt() + if (numParams > 0) { + params = Parameters.deserialize_with_count(reader, Number(numParams)) + } + return { + id, + fetch_type, + standalone, + joining, + params + } + } } diff --git a/lib/transport/control/fetch_error.ts b/lib/transport/control/fetch_error.ts deleted file mode 100644 index c061514..0000000 --- a/lib/transport/control/fetch_error.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { ControlMessageType } from "." -import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" - - -export interface FetchError { - id: bigint - code: bigint - reason: string -} - - -export namespace FetchError { - export function serialize(v: FetchError): Uint8Array { - const mainBuf = new MutableBytesBuffer(new Uint8Array()) - mainBuf.putVarInt(ControlMessageType.FetchError) - const payloadBuf = new MutableBytesBuffer(new Uint8Array()) - payloadBuf.putVarInt(v.id) - payloadBuf.putVarInt(v.code) - payloadBuf.putUtf8String(v.reason) - - mainBuf.putU16(payloadBuf.byteLength) - mainBuf.putBytes(payloadBuf.Uint8Array) - return mainBuf.Uint8Array - } - - export function deserialize(reader: ImmutableBytesBuffer): FetchError { - const id = reader.getVarInt() - const code = reader.getVarInt() - const reason = reader.getUtf8String() - return { - id, - code, - reason - } - } -} \ No newline at end of file diff --git a/lib/transport/control/fetch_ok.ts b/lib/transport/control/fetch_ok.ts index be2fa31..984fad2 100644 --- a/lib/transport/control/fetch_ok.ts +++ b/lib/transport/control/fetch_ok.ts @@ -1,47 +1,57 @@ -import { ControlMessageType, GroupOrder } from "." +import { ControlMessageType } from "." import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" -import { Parameters, Location } from "../base_data" +import { Parameters, Location, KeyValuePairs } from "../base_data" export interface FetchOk { - id: bigint - group_order: GroupOrder - end_of_track: number // u8 - end_location: Location - params?: Parameters + id: bigint + end_of_track: number // u8 + end_location: Location + track_extensions?: KeyValuePairs + params?: Parameters } - export namespace FetchOk { - export function serialize(v: FetchOk): Uint8Array { - const mainBuf = new MutableBytesBuffer(new Uint8Array()) - mainBuf.putVarInt(ControlMessageType.FetchOk) - const payloadBuf = new MutableBytesBuffer(new Uint8Array()) - payloadBuf.putVarInt(v.id) - payloadBuf.putBytes(GroupOrder.serialize(v.group_order)) - payloadBuf.putU8(v.end_of_track) - payloadBuf.putBytes(Location.serialize(v.end_location)) - payloadBuf.putBytes(Parameters.serialize(v.params ?? new Map())) + export function serialize(v: FetchOk): Uint8Array { + const mainBuf = new MutableBytesBuffer(new Uint8Array()) + mainBuf.putVarInt(ControlMessageType.FetchOk) + const payloadBuf = new MutableBytesBuffer(new Uint8Array()) + payloadBuf.putVarInt(v.id) + payloadBuf.putU8(v.end_of_track) + payloadBuf.putBytes(Location.serialize(v.end_location)) + const params = v.params ?? new Map() + payloadBuf.putVarInt(BigInt(params.size)) + payloadBuf.putBytes(Parameters.serialize(params)) + const extensions = v.track_extensions ?? new Map() + const extBytes = KeyValuePairs.serialize(extensions) + payloadBuf.putVarInt(BigInt(extBytes.length)) + payloadBuf.putBytes(extBytes) - mainBuf.putU16(payloadBuf.byteLength) - mainBuf.putBytes(payloadBuf.Uint8Array) - return mainBuf.Uint8Array - } + mainBuf.putU16(payloadBuf.byteLength) + mainBuf.putBytes(payloadBuf.Uint8Array) + return mainBuf.Uint8Array + } - export function deserialize(reader: ImmutableBytesBuffer): FetchOk { - const id = reader.getVarInt() - const group_order = GroupOrder.deserialize(reader) - const end_of_track = reader.getU8() - const end_location = Location.deserialize(reader) - let params: Parameters | undefined - if (reader.remaining > 0) { - params = Parameters.deserialize(reader) - } - return { - id, - group_order, - end_of_track, - end_location, - params - } - } -} \ No newline at end of file + export function deserialize(reader: ImmutableBytesBuffer): FetchOk { + const id = reader.getVarInt() + const end_of_track = reader.getU8() + const end_location = Location.deserialize(reader) + const numParams = reader.getNumberVarInt() + let params: Parameters | undefined + if (numParams > 0) { + params = Parameters.deserialize_with_count(reader, Number(numParams)) + } + const extLength = reader.getNumberVarInt() + let track_extensions: KeyValuePairs | undefined + if (extLength > 0) { + const extData = reader.getBytes(Number(extLength)) + track_extensions = KeyValuePairs.deserialize(new ImmutableBytesBuffer(extData)) + } + return { + id, + end_of_track, + end_location, + track_extensions, + params + } + } +} diff --git a/lib/transport/control/index.ts b/lib/transport/control/index.ts index fe80f58..e78b666 100644 --- a/lib/transport/control/index.ts +++ b/lib/transport/control/index.ts @@ -1,197 +1,212 @@ import { Subscribe, GroupOrder, FilterType } from "./subscribe" import { SubscribeOk } from "./subscribe_ok" -import { SubscribeError } from "./subscribe_error" import { SubscribeUpdate } from "./subscribe_update" -import { SubscribeNamespace } from "./subscribe_namespace" -import { SubscribeNamespaceOk } from "./subscribe_namespace_ok" -import { SubscribeNamespaceError } from "./subscribe_namespace_error" +import { SubscribeNamespace, SubscribeOptions } from "./subscribe_namespace" import { Unsubscribe } from "./unsubscribe" import { Publish } from "./publish" import { PublishOk } from "./publish_ok" -import { PublishError } from "./publish_error" import { PublishDone } from "./publish_done" import { PublishNamespace } from "./publish_namespace" -import { PublishNamespaceOk } from "./publish_namespace_ok" -import { PublishNamespaceError } from "./publish_namespace_error" import { PublishNamespaceDone } from "./publish_namespace_done" +import { PublishNamespaceCancel } from "./publish_namespace_cancel" +import { Namespace } from "./namespace" +import { NamespaceDone } from "./namespace_done" +import { TrackStatus } from "./track_status" import { Fetch } from "./fetch" import { FetchOk } from "./fetch_ok" -import { FetchError } from "./fetch_error" import { FetchCancel } from "./fetch_cancel" import { GoAway } from "./go_away" import { ClientSetup } from "./client_setup" import { ServerSetup } from "./server_setup" +import { RequestOk } from "./request_ok" +import { RequestError } from "./request_error" +import { MaxRequestId } from "./max_request_id" +import { RequestsBlocked } from "./requests_blocked" enum Version { - DRAFT_00 = 0xff000000, - DRAFT_01 = 0xff000001, - DRAFT_02 = 0xff000002, - DRAFT_03 = 0xff000003, - DRAFT_04 = 0xff000004, - DRAFT_05 = 0xff000005, - DRAFT_06 = 0xff000006, - DRAFT_07 = 0xff000007, - DRAFT_14 = 0xff00000e, - KIXEL_00 = 0xbad00, - KIXEL_01 = 0xbad01, + DRAFT_00 = 0xff000000, + DRAFT_01 = 0xff000001, + DRAFT_02 = 0xff000002, + DRAFT_03 = 0xff000003, + DRAFT_04 = 0xff000004, + DRAFT_05 = 0xff000005, + DRAFT_06 = 0xff000006, + DRAFT_07 = 0xff000007, + DRAFT_14 = 0xff00000e, + DRAFT_16 = 0xff000010, + KIXEL_00 = 0xbad00, + KIXEL_01 = 0xbad01, } // Discriminated union where data type matches the control message type type MessageWithType = - | { type: ControlMessageType.Publish; message: Publish } - | { type: ControlMessageType.PublishOk; message: PublishOk } - | { type: ControlMessageType.PublishError; message: PublishError } - | { type: ControlMessageType.PublishDone; message: PublishDone } - | { type: ControlMessageType.PublishNamespace; message: PublishNamespace } - | { type: ControlMessageType.PublishNamespaceOk; message: PublishNamespaceOk } - | { type: ControlMessageType.PublishNamespaceError; message: PublishNamespaceError } - | { type: ControlMessageType.PublishNamespaceDone; message: PublishNamespaceDone } - | { type: ControlMessageType.Fetch; message: Fetch } - | { type: ControlMessageType.FetchOk; message: FetchOk } - | { type: ControlMessageType.FetchError; message: FetchError } - | { type: ControlMessageType.FetchCancel; message: FetchCancel } - | { type: ControlMessageType.Subscribe; message: Subscribe } - | { type: ControlMessageType.SubscribeOk; message: SubscribeOk } - | { type: ControlMessageType.SubscribeError; message: SubscribeError } - | { type: ControlMessageType.SubscribeUpdate; message: SubscribeUpdate } - | { type: ControlMessageType.SubscribeNamespace; message: SubscribeNamespace } - | { type: ControlMessageType.SubscribeNamespaceOk; message: SubscribeNamespaceOk } - | { type: ControlMessageType.SubscribeNamespaceError; message: SubscribeNamespaceError } - | { type: ControlMessageType.Unsubscribe; message: Unsubscribe } + | { type: ControlMessageType.Publish; message: Publish } + | { type: ControlMessageType.PublishOk; message: PublishOk } + | { type: ControlMessageType.PublishDone; message: PublishDone } + | { type: ControlMessageType.PublishNamespace; message: PublishNamespace } + | { type: ControlMessageType.PublishNamespaceDone; message: PublishNamespaceDone } + | { type: ControlMessageType.PublishNamespaceCancel; message: PublishNamespaceCancel } + | { type: ControlMessageType.Namespace; message: Namespace } + | { type: ControlMessageType.NamespaceDone; message: NamespaceDone } + | { type: ControlMessageType.TrackStatus; message: TrackStatus } + | { type: ControlMessageType.Fetch; message: Fetch } + | { type: ControlMessageType.FetchOk; message: FetchOk } + | { type: ControlMessageType.FetchCancel; message: FetchCancel } + | { type: ControlMessageType.Subscribe; message: Subscribe } + | { type: ControlMessageType.SubscribeOk; message: SubscribeOk } + | { type: ControlMessageType.SubscribeUpdate; message: SubscribeUpdate } + | { type: ControlMessageType.SubscribeNamespace; message: SubscribeNamespace } + | { type: ControlMessageType.Unsubscribe; message: Unsubscribe } + | { type: ControlMessageType.RequestOk; message: RequestOk } + | { type: ControlMessageType.RequestError; message: RequestError } + | { type: ControlMessageType.MaxRequestId; message: MaxRequestId } + | { type: ControlMessageType.RequestsBlocked; message: RequestsBlocked } type Message = Subscriber | Publisher // Sent by subscriber type Subscriber = Subscribe | SubscribeUpdate | SubscribeNamespace | - Unsubscribe | PublishOk | PublishError | - PublishNamespaceOk | PublishNamespaceError | Fetch | FetchCancel + Unsubscribe | PublishOk | Fetch | FetchCancel | PublishNamespaceCancel | + TrackStatus | MaxRequestId | RequestsBlocked | RequestOk | RequestError // Sent by publisher -type Publisher = SubscribeOk | SubscribeError - | SubscribeNamespaceOk | SubscribeNamespaceError | - PublishDone | Publish | PublishNamespace | PublishNamespaceDone - | FetchOk | FetchError +type Publisher = SubscribeOk | PublishDone | Publish | PublishNamespace | PublishNamespaceDone + | Namespace | NamespaceDone | FetchOk | MaxRequestId | RequestsBlocked | RequestOk | RequestError function isSubscriber(m: ControlMessageType): boolean { - return ( - m == ControlMessageType.Subscribe || - m == ControlMessageType.SubscribeUpdate || - m == ControlMessageType.Unsubscribe || - m == ControlMessageType.PublishOk || - m == ControlMessageType.PublishError || - m == ControlMessageType.PublishNamespaceOk || - m == ControlMessageType.PublishNamespaceError - ) + return ( + m == ControlMessageType.Subscribe || + m == ControlMessageType.SubscribeUpdate || + m == ControlMessageType.Unsubscribe || + m == ControlMessageType.PublishOk || + m == ControlMessageType.PublishNamespaceCancel || + m == ControlMessageType.TrackStatus || + m == ControlMessageType.MaxRequestId || + m == ControlMessageType.RequestsBlocked || + m == ControlMessageType.RequestOk || + m == ControlMessageType.RequestError + ) } function isPublisher(m: ControlMessageType): boolean { - return ( - m == ControlMessageType.SubscribeOk || - m == ControlMessageType.SubscribeError || - m == ControlMessageType.PublishDone || - m == ControlMessageType.Publish || - m == ControlMessageType.PublishNamespace || - m == ControlMessageType.PublishNamespaceDone - ) + return ( + m == ControlMessageType.SubscribeOk || + m == ControlMessageType.PublishDone || + m == ControlMessageType.Publish || + m == ControlMessageType.PublishNamespace || + m == ControlMessageType.PublishNamespaceDone || + m == ControlMessageType.Namespace || + m == ControlMessageType.NamespaceDone || + m == ControlMessageType.MaxRequestId || + m == ControlMessageType.RequestsBlocked || + m == ControlMessageType.RequestOk || + m == ControlMessageType.RequestError + ) } +// Draft-16: Control Message Types (Table 1) export enum ControlMessageType { - ReservedSetupV00 = 0x1, - GoAway = 0x10, - MaxRequestId = 0x15, - RequestsBlocked = 0x1a, - - SubscribeUpdate = 0x2, - Subscribe = 0x3, - SubscribeOk = 0x4, - SubscribeError = 0x5, - Unsubscribe = 0xa, - PublishDone = 0xb, - - Publish = 0x1d, - PublishOk = 0x1e, - PublishError = 0x1f, - PublishNamespace = 0x6, - PublishNamespaceOk = 0x7, - PublishNamespaceError = 0x8, - PublishNamespaceDone = 0x9, - SubscribeNamespace = 0x11, - SubscribeNamespaceOk = 0x12, - SubscribeNamespaceError = 0x13, - Fetch = 0x16, - FetchCancel = 0x17, - FetchOk = 0x18, - FetchError = 0x19, - - ClientSetup = 0x20, - ServerSetup = 0x21, + ReservedSetupV00 = 0x1, + + RequestUpdate = 0x2, + Subscribe = 0x3, + SubscribeOk = 0x4, + RequestError = 0x5, + PublishNamespace = 0x6, + RequestOk = 0x7, + Namespace = 0x8, + PublishNamespaceDone = 0x9, + Unsubscribe = 0xa, + PublishDone = 0xb, + PublishNamespaceCancel = 0xc, + TrackStatus = 0xd, + NamespaceDone = 0xe, + + GoAway = 0x10, + SubscribeNamespace = 0x11, + + MaxRequestId = 0x15, + Fetch = 0x16, + FetchCancel = 0x17, + FetchOk = 0x18, + RequestsBlocked = 0x1a, + + Publish = 0x1d, + PublishOk = 0x1e, + + ClientSetup = 0x20, + ServerSetup = 0x21, + + // Legacy aliases for backward compat during transition + SubscribeUpdate = 0x2, // Same as RequestUpdate in draft-16 } export namespace ControlMessageType { - export function toString(t: ControlMessageType): string { - switch (t) { - case ControlMessageType.ReservedSetupV00: return "ReservedSetupV00" - case ControlMessageType.GoAway: return "GoAway" - case ControlMessageType.MaxRequestId: return "MaxRequestId" - case ControlMessageType.RequestsBlocked: return "RequestsBlocked" - case ControlMessageType.SubscribeUpdate: return "SubscribeUpdate" - case ControlMessageType.Subscribe: return "Subscribe" - case ControlMessageType.SubscribeOk: return "SubscribeOk" - case ControlMessageType.SubscribeError: return "SubscribeError" - case ControlMessageType.Unsubscribe: return "Unsubscribe" - case ControlMessageType.PublishDone: return "PublishDone" - case ControlMessageType.Publish: return "Publish" - case ControlMessageType.PublishOk: return "PublishOk" - case ControlMessageType.PublishError: return "PublishError" - case ControlMessageType.PublishNamespace: return "PublishNamespace" - case ControlMessageType.PublishNamespaceOk: return "PublishNamespaceOk" - case ControlMessageType.PublishNamespaceError: return "PublishNamespaceError" - case ControlMessageType.PublishNamespaceDone: return "PublishNamespaceDone" - case ControlMessageType.SubscribeNamespace: return "SubscribeNamespace" - case ControlMessageType.SubscribeNamespaceOk: return "SubscribeNamespaceOk" - case ControlMessageType.SubscribeNamespaceError: return "SubscribeNamespaceError" - case ControlMessageType.Fetch: return "Fetch" - case ControlMessageType.FetchCancel: return "FetchCancel" - case ControlMessageType.FetchOk: return "FetchOk" - case ControlMessageType.FetchError: return "FetchError" - case ControlMessageType.ClientSetup: return "ClientSetup" - case ControlMessageType.ServerSetup: return "ServerSetup" - } - } + export function toString(t: ControlMessageType): string { + switch (t) { + case ControlMessageType.ReservedSetupV00: return "ReservedSetupV00" + case ControlMessageType.GoAway: return "GoAway" + case ControlMessageType.MaxRequestId: return "MaxRequestId" + case ControlMessageType.RequestsBlocked: return "RequestsBlocked" + case ControlMessageType.RequestUpdate: return "RequestUpdate" + case ControlMessageType.Subscribe: return "Subscribe" + case ControlMessageType.SubscribeOk: return "SubscribeOk" + case ControlMessageType.RequestError: return "RequestError" + case ControlMessageType.Unsubscribe: return "Unsubscribe" + case ControlMessageType.PublishDone: return "PublishDone" + case ControlMessageType.PublishNamespaceCancel: return "PublishNamespaceCancel" + case ControlMessageType.TrackStatus: return "TrackStatus" + case ControlMessageType.NamespaceDone: return "NamespaceDone" + case ControlMessageType.Publish: return "Publish" + case ControlMessageType.PublishOk: return "PublishOk" + case ControlMessageType.PublishNamespace: return "PublishNamespace" + case ControlMessageType.RequestOk: return "RequestOk" + case ControlMessageType.Namespace: return "Namespace" + case ControlMessageType.PublishNamespaceDone: return "PublishNamespaceDone" + case ControlMessageType.SubscribeNamespace: return "SubscribeNamespace" + case ControlMessageType.Fetch: return "Fetch" + case ControlMessageType.FetchCancel: return "FetchCancel" + case ControlMessageType.FetchOk: return "FetchOk" + case ControlMessageType.ClientSetup: return "ClientSetup" + case ControlMessageType.ServerSetup: return "ServerSetup" + } + } } export { - Subscribe, - SubscribeOk, - SubscribeError, - SubscribeUpdate, - SubscribeNamespace, - SubscribeNamespaceOk, - SubscribeNamespaceError, - Unsubscribe, - Publish, - PublishOk, - PublishError, - PublishDone, - PublishNamespace, - PublishNamespaceOk, - PublishNamespaceError, - PublishNamespaceDone, - Fetch, - FetchOk, - FetchError, - FetchCancel, - GoAway, - ClientSetup, - ServerSetup, - - Version, - isSubscriber, - isPublisher, - MessageWithType, - Message, - GroupOrder, - FilterType, -} \ No newline at end of file + Subscribe, + SubscribeOk, + SubscribeUpdate, + SubscribeNamespace, + SubscribeOptions, + Unsubscribe, + Publish, + PublishOk, + PublishDone, + PublishNamespace, + PublishNamespaceDone, + PublishNamespaceCancel, + Namespace, + NamespaceDone, + TrackStatus, + Fetch, + FetchOk, + FetchCancel, + GoAway, + ClientSetup, + ServerSetup, + MaxRequestId, + RequestsBlocked, + RequestOk, + RequestError, + + Version, + isSubscriber, + isPublisher, + MessageWithType, + Message, + GroupOrder, + FilterType, +} diff --git a/lib/transport/control/max_request_id.ts b/lib/transport/control/max_request_id.ts index 5311b75..2c58538 100644 --- a/lib/transport/control/max_request_id.ts +++ b/lib/transport/control/max_request_id.ts @@ -2,8 +2,11 @@ import { ControlMessageType } from "." import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" +// Draft-16: MAX_REQUEST_ID (Section 9.5, type 0x15) +// Sent to increase the number of requests the peer can send within a session. +// The Maximum Request ID MUST only increase within a session. export interface MaxRequestId { - id: bigint + max_request_id: bigint } export namespace MaxRequestId { @@ -11,17 +14,16 @@ export namespace MaxRequestId { const mainBuf = new MutableBytesBuffer(new Uint8Array()) mainBuf.putVarInt(ControlMessageType.MaxRequestId) const payloadBuf = new MutableBytesBuffer(new Uint8Array()) - payloadBuf.putVarInt(v.id) - + payloadBuf.putVarInt(v.max_request_id) mainBuf.putU16(payloadBuf.byteLength) mainBuf.putBytes(payloadBuf.Uint8Array) return mainBuf.Uint8Array } export function deserialize(reader: ImmutableBytesBuffer): MaxRequestId { - const id = reader.getVarInt() + const max_request_id = reader.getVarInt() return { - id, + max_request_id, } } } \ No newline at end of file diff --git a/lib/transport/control/namespace.ts b/lib/transport/control/namespace.ts new file mode 100644 index 0000000..a381645 --- /dev/null +++ b/lib/transport/control/namespace.ts @@ -0,0 +1,27 @@ +import { ControlMessageType } from "." +import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" +import { Tuple } from "../base_data" + + +export interface Namespace { + namespace_suffix: string[] +} + +export namespace Namespace { + export function serialize(v: Namespace): Uint8Array { + const mainBuf = new MutableBytesBuffer(new Uint8Array()) + mainBuf.putVarInt(ControlMessageType.Namespace) + const payloadBuf = new MutableBytesBuffer(new Uint8Array()) + payloadBuf.putBytes(Tuple.serialize(v.namespace_suffix)) + mainBuf.putU16(payloadBuf.byteLength) + mainBuf.putBytes(payloadBuf.Uint8Array) + return mainBuf.Uint8Array + } + + export function deserialize(reader: ImmutableBytesBuffer): Namespace { + const namespace_suffix = Tuple.deserialize(reader) + return { + namespace_suffix, + } + } +} diff --git a/lib/transport/control/namespace_done.ts b/lib/transport/control/namespace_done.ts new file mode 100644 index 0000000..66da359 --- /dev/null +++ b/lib/transport/control/namespace_done.ts @@ -0,0 +1,27 @@ +import { ControlMessageType } from "." +import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" +import { Tuple } from "../base_data" + + +export interface NamespaceDone { + namespace_suffix: string[] +} + +export namespace NamespaceDone { + export function serialize(v: NamespaceDone): Uint8Array { + const mainBuf = new MutableBytesBuffer(new Uint8Array()) + mainBuf.putVarInt(ControlMessageType.NamespaceDone) + const payloadBuf = new MutableBytesBuffer(new Uint8Array()) + payloadBuf.putBytes(Tuple.serialize(v.namespace_suffix)) + mainBuf.putU16(payloadBuf.byteLength) + mainBuf.putBytes(payloadBuf.Uint8Array) + return mainBuf.Uint8Array + } + + export function deserialize(reader: ImmutableBytesBuffer): NamespaceDone { + const namespace_suffix = Tuple.deserialize(reader) + return { + namespace_suffix, + } + } +} diff --git a/lib/transport/control/publish.ts b/lib/transport/control/publish.ts index 5acd9d0..383ece1 100644 --- a/lib/transport/control/publish.ts +++ b/lib/transport/control/publish.ts @@ -1,61 +1,61 @@ -import { ControlMessageType, GroupOrder } from "." +import { ControlMessageType } from "." import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" -import { Parameters, Location, Tuple } from "../base_data" +import { Parameters, Location, Tuple, KeyValuePairs } from "../base_data" export interface Publish { - id: bigint - track_alias: bigint // Publisher-specified - namespace: Tuple - name: string - content_exists: number // 0 or 1 - group_order: GroupOrder - largest_location?: Location // largest location of group or object if content_exists == 1 - forward: number // 0 or 1 - params?: Parameters + id: bigint + track_alias: bigint // Publisher-specified + namespace: Tuple + name: string + track_extensions?: KeyValuePairs + params?: Parameters } - export namespace Publish { - export function serialize(v: Publish): Uint8Array { - const mainBuf = new MutableBytesBuffer(new Uint8Array()) - mainBuf.putVarInt(ControlMessageType.Publish) - const payloadBuf = new MutableBytesBuffer(new Uint8Array()) - payloadBuf.putVarInt(v.id) - payloadBuf.putVarInt(v.track_alias) - payloadBuf.putBytes(Tuple.serialize(v.namespace)) - payloadBuf.putUtf8String(v.name) - payloadBuf.putU8(v.content_exists) - payloadBuf.putBytes(GroupOrder.serialize(v.group_order)) - if (v.largest_location) { - payloadBuf.putBytes(Location.serialize(v.largest_location)) - } - payloadBuf.putU8(v.forward) - payloadBuf.putBytes(Parameters.serialize(v.params ?? new Map())) - mainBuf.putU16(payloadBuf.byteLength) - mainBuf.putBytes(payloadBuf.Uint8Array) - return mainBuf.Uint8Array - } + export function serialize(v: Publish): Uint8Array { + const mainBuf = new MutableBytesBuffer(new Uint8Array()) + mainBuf.putVarInt(ControlMessageType.Publish) + const payloadBuf = new MutableBytesBuffer(new Uint8Array()) + payloadBuf.putVarInt(v.id) + payloadBuf.putBytes(Tuple.serialize(v.namespace)) + payloadBuf.putUtf8String(v.name) + payloadBuf.putVarInt(v.track_alias) + const params = v.params ?? new Map() + payloadBuf.putVarInt(BigInt(params.size)) + payloadBuf.putBytes(Parameters.serialize(params)) + const extensions = v.track_extensions ?? new Map() + const extBytes = KeyValuePairs.serialize(extensions) + payloadBuf.putVarInt(BigInt(extBytes.length)) + payloadBuf.putBytes(extBytes) + + mainBuf.putU16(payloadBuf.byteLength) + mainBuf.putBytes(payloadBuf.Uint8Array) + return mainBuf.Uint8Array + } - export function deserialize(reader: ImmutableBytesBuffer): Publish { - const id = reader.getVarInt() - const track_alias = reader.getVarInt() - const namespace = Tuple.deserialize(reader) - const name = reader.getUtf8String() - const content_exists = reader.getU8() - const group_order = GroupOrder.deserialize(reader) - const largest_location = Location.deserialize(reader) - const forward = reader.getU8() - const params = Parameters.deserialize(reader) - return { - id, - track_alias, - namespace, - name, - content_exists, - group_order, - largest_location, - forward, - params - } - } + export function deserialize(reader: ImmutableBytesBuffer): Publish { + const id = reader.getVarInt() + const namespace = Tuple.deserialize(reader) + const name = reader.getUtf8String() + const track_alias = reader.getVarInt() + const numParams = reader.getNumberVarInt() + let params: Parameters | undefined + if (numParams > 0) { + params = Parameters.deserialize_with_count(reader, Number(numParams)) + } + const extLength = reader.getNumberVarInt() + let track_extensions: KeyValuePairs | undefined + if (extLength > 0) { + const extData = reader.getBytes(Number(extLength)) + track_extensions = KeyValuePairs.deserialize(new ImmutableBytesBuffer(extData)) + } + return { + id, + track_alias, + namespace, + name, + track_extensions, + params + } + } } diff --git a/lib/transport/control/publish_done.ts b/lib/transport/control/publish_done.ts index f2927fa..ccb6aa8 100644 --- a/lib/transport/control/publish_done.ts +++ b/lib/transport/control/publish_done.ts @@ -1,39 +1,40 @@ import { ControlMessageType } from "." import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" +import { ReasonPhrase } from "../base_data" export interface PublishDone { - id: bigint - code: bigint - stream_count: bigint - reason: string + id: bigint + code: bigint + stream_count: bigint + reason: string } export namespace PublishDone { - export function serialize(v: PublishDone): Uint8Array { - const mainBuf = new MutableBytesBuffer(new Uint8Array()) - mainBuf.putVarInt(ControlMessageType.PublishDone) - const payloadBuf = new MutableBytesBuffer(new Uint8Array()) - payloadBuf.putVarInt(v.id) - payloadBuf.putVarInt(v.code) - payloadBuf.putVarInt(v.stream_count) - payloadBuf.putUtf8String(v.reason) + export function serialize(v: PublishDone): Uint8Array { + const mainBuf = new MutableBytesBuffer(new Uint8Array()) + mainBuf.putVarInt(ControlMessageType.PublishDone) + const payloadBuf = new MutableBytesBuffer(new Uint8Array()) + payloadBuf.putVarInt(v.id) + payloadBuf.putVarInt(v.code) + payloadBuf.putVarInt(v.stream_count) + payloadBuf.putBytes(ReasonPhrase.serialize(v.reason)) - mainBuf.putU16(payloadBuf.byteLength) - mainBuf.putBytes(payloadBuf.Uint8Array) - return mainBuf.Uint8Array - } + mainBuf.putU16(payloadBuf.byteLength) + mainBuf.putBytes(payloadBuf.Uint8Array) + return mainBuf.Uint8Array + } - export function deserialize(reader: ImmutableBytesBuffer): PublishDone { - const id = reader.getVarInt() - const code = reader.getVarInt() - const stream_count = reader.getVarInt() - const reason = reader.getUtf8String() - return { - id, - code, - stream_count, - reason - } - } -} \ No newline at end of file + export function deserialize(reader: ImmutableBytesBuffer): PublishDone { + const id = reader.getVarInt() + const code = reader.getVarInt() + const stream_count = reader.getVarInt() + const reason = ReasonPhrase.deserialize(reader) + return { + id, + code, + stream_count, + reason + } + } +} diff --git a/lib/transport/control/publish_error.ts b/lib/transport/control/publish_error.ts deleted file mode 100644 index ba25253..0000000 --- a/lib/transport/control/publish_error.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { ControlMessageType } from "." -import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" - - -export interface PublishError { - id: bigint - code: bigint - reason: string -} - -export namespace PublishError { - export function serialize(v: PublishError): Uint8Array { - const mainBuf = new MutableBytesBuffer(new Uint8Array()) - mainBuf.putVarInt(ControlMessageType.PublishError) - const payloadBuf = new MutableBytesBuffer(new Uint8Array()) - payloadBuf.putVarInt(v.id) - payloadBuf.putVarInt(v.code) - payloadBuf.putUtf8String(v.reason) - - mainBuf.putU16(payloadBuf.byteLength) - mainBuf.putBytes(payloadBuf.Uint8Array) - return mainBuf.Uint8Array - } - - export function deserialize(reader: ImmutableBytesBuffer): PublishError { - const id = reader.getVarInt() - const code = reader.getVarInt() - const reason = reader.getUtf8String() - return { - id, - code, - reason - } - } -} \ No newline at end of file diff --git a/lib/transport/control/publish_namespace.ts b/lib/transport/control/publish_namespace.ts index a610322..ac13926 100644 --- a/lib/transport/control/publish_namespace.ts +++ b/lib/transport/control/publish_namespace.ts @@ -1,37 +1,42 @@ import { ControlMessageType } from "." import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" -import { Parameters, Tuple } from "../base_data" +import { Parameters, Tuple, KeyValuePairs } from "../base_data" export interface PublishNamespace { - id: bigint - namespace: Tuple - params?: Parameters + id: bigint + namespace: Tuple + params?: Parameters } export namespace PublishNamespace { - export function serialize(v: PublishNamespace): Uint8Array { - const mainBuf = new MutableBytesBuffer(new Uint8Array()) - mainBuf.putVarInt(ControlMessageType.PublishNamespace) + export function serialize(v: PublishNamespace): Uint8Array { + const mainBuf = new MutableBytesBuffer(new Uint8Array()) + mainBuf.putVarInt(ControlMessageType.PublishNamespace) - const payloadBuf = new MutableBytesBuffer(new Uint8Array()) - payloadBuf.putVarInt(v.id) - payloadBuf.putBytes(Tuple.serialize(v.namespace)) - payloadBuf.putBytes(Parameters.serialize(v.params ?? new Map())) + const payloadBuf = new MutableBytesBuffer(new Uint8Array()) + payloadBuf.putVarInt(v.id) + payloadBuf.putBytes(Tuple.serialize(v.namespace)) + // Draft-16: Number of Parameters + delta-encoded parameters + const params = v.params ?? new Map() + const paramsBytes = KeyValuePairs.serialize(params) + payloadBuf.putVarInt(params.size) + payloadBuf.putBytes(paramsBytes) - mainBuf.putU16(payloadBuf.byteLength) - mainBuf.putBytes(payloadBuf.Uint8Array) - return mainBuf.Uint8Array - } + mainBuf.putU16(payloadBuf.byteLength) + mainBuf.putBytes(payloadBuf.Uint8Array) + return mainBuf.Uint8Array + } - export function deserialize(reader: ImmutableBytesBuffer): PublishNamespace { - const id = reader.getVarInt() - const namespace = Tuple.deserialize(reader) - const params = Parameters.deserialize(reader) - return { - id, - namespace, - params - } - } -} \ No newline at end of file + export function deserialize(reader: ImmutableBytesBuffer): PublishNamespace { + const id = reader.getVarInt() + const namespace = Tuple.deserialize(reader) + const numParams = reader.getNumberVarInt() + const params = Parameters.deserialize_with_count(reader, numParams) + return { + id, + namespace, + params + } + } +} diff --git a/lib/transport/control/publish_namespace_cancel.ts b/lib/transport/control/publish_namespace_cancel.ts new file mode 100644 index 0000000..7640cd5 --- /dev/null +++ b/lib/transport/control/publish_namespace_cancel.ts @@ -0,0 +1,35 @@ +import { ControlMessageType } from "." +import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" +import { ReasonPhrase } from "../base_data" + + +export interface PublishNamespaceCancel { + id: bigint + error_code: bigint + error_reason: string +} + +export namespace PublishNamespaceCancel { + export function serialize(v: PublishNamespaceCancel): Uint8Array { + const mainBuf = new MutableBytesBuffer(new Uint8Array()) + mainBuf.putVarInt(ControlMessageType.PublishNamespaceCancel) + const payloadBuf = new MutableBytesBuffer(new Uint8Array()) + payloadBuf.putVarInt(v.id) + payloadBuf.putVarInt(v.error_code) + payloadBuf.putBytes(ReasonPhrase.serialize(v.error_reason)) + mainBuf.putU16(payloadBuf.byteLength) + mainBuf.putBytes(payloadBuf.Uint8Array) + return mainBuf.Uint8Array + } + + export function deserialize(reader: ImmutableBytesBuffer): PublishNamespaceCancel { + const id = reader.getVarInt() + const error_code = reader.getVarInt() + const error_reason = ReasonPhrase.deserialize(reader) + return { + id, + error_code, + error_reason + } + } +} diff --git a/lib/transport/control/publish_namespace_done.ts b/lib/transport/control/publish_namespace_done.ts index 9c5b737..127b4d4 100644 --- a/lib/transport/control/publish_namespace_done.ts +++ b/lib/transport/control/publish_namespace_done.ts @@ -1,27 +1,25 @@ import { ControlMessageType } from "." import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" -import { Tuple } from "../base_data" export interface PublishNamespaceDone { - namespace: Tuple + id: bigint } export namespace PublishNamespaceDone { - export function serialize(v: PublishNamespaceDone): Uint8Array { - const mainBuf = new MutableBytesBuffer(new Uint8Array()) - mainBuf.putVarInt(ControlMessageType.PublishNamespaceDone) - const payloadBuf = new MutableBytesBuffer(new Uint8Array()) - payloadBuf.putBytes(Tuple.serialize(v.namespace)) + export function serialize(v: PublishNamespaceDone): Uint8Array { + const mainBuf = new MutableBytesBuffer(new Uint8Array()) + mainBuf.putVarInt(ControlMessageType.PublishNamespaceDone) + const payloadBuf = new MutableBytesBuffer(new Uint8Array()) + payloadBuf.putVarInt(v.id) + mainBuf.putU16(payloadBuf.byteLength) + mainBuf.putBytes(payloadBuf.Uint8Array) + return mainBuf.Uint8Array + } - mainBuf.putU16(payloadBuf.byteLength) - mainBuf.putBytes(payloadBuf.Uint8Array) - return mainBuf.Uint8Array - } - - export function deserialize(reader: ImmutableBytesBuffer): PublishNamespaceDone { - const namespace = Tuple.deserialize(reader) - return { - namespace, - } - } -} \ No newline at end of file + export function deserialize(reader: ImmutableBytesBuffer): PublishNamespaceDone { + const id = reader.getVarInt() + return { + id, + } + } +} diff --git a/lib/transport/control/publish_namespace_error.ts b/lib/transport/control/publish_namespace_error.ts deleted file mode 100644 index 8c388e4..0000000 --- a/lib/transport/control/publish_namespace_error.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { ControlMessageType } from "." -import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" - - - -export interface PublishNamespaceError { - id: bigint - code: bigint - reason: string -} - -export namespace PublishNamespaceError { - export function serialize(v: PublishNamespaceError): Uint8Array { - const mainBuf = new MutableBytesBuffer(new Uint8Array()) - mainBuf.putVarInt(ControlMessageType.PublishNamespaceError) - const payloadBuf = new MutableBytesBuffer(new Uint8Array()) - payloadBuf.putVarInt(v.id) - payloadBuf.putVarInt(v.code) - payloadBuf.putUtf8String(v.reason) - - mainBuf.putU16(payloadBuf.byteLength) - mainBuf.putBytes(payloadBuf.Uint8Array) - return mainBuf.Uint8Array - } - - export function deserialize(reader: ImmutableBytesBuffer): PublishNamespaceError { - const id = reader.getVarInt() - const code = reader.getVarInt() - const reason = reader.getUtf8String() - return { - id, - code, - reason - } - } -} \ No newline at end of file diff --git a/lib/transport/control/publish_namespace_ok.ts b/lib/transport/control/publish_namespace_ok.ts deleted file mode 100644 index ce831a4..0000000 --- a/lib/transport/control/publish_namespace_ok.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { ControlMessageType } from "." -import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" - -export interface PublishNamespaceOk { - id: bigint -} - -export namespace PublishNamespaceOk { - export function serialize(v: PublishNamespaceOk): Uint8Array { - const mainBuf = new MutableBytesBuffer(new Uint8Array()) - mainBuf.putVarInt(ControlMessageType.PublishNamespaceOk) - const payloadBuf = new MutableBytesBuffer(new Uint8Array()) - payloadBuf.putVarInt(v.id) - - mainBuf.putU16(payloadBuf.byteLength) - mainBuf.putBytes(payloadBuf.Uint8Array) - return mainBuf.Uint8Array - } - - export function deserialize(reader: ImmutableBytesBuffer): PublishNamespaceOk { - const id = reader.getVarInt() - return { - id - } - } -} \ No newline at end of file diff --git a/lib/transport/control/publish_ok.ts b/lib/transport/control/publish_ok.ts index c233c19..d94fbf0 100644 --- a/lib/transport/control/publish_ok.ts +++ b/lib/transport/control/publish_ok.ts @@ -1,70 +1,38 @@ -import { ControlMessageType, FilterType, GroupOrder } from "." +import { ControlMessageType } from "." import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" -import { Location, Parameters } from "../base_data" +import { Parameters } from "../base_data" export interface PublishOk { - id: bigint // Request ID - forward: number // 0 or 1 u8 - subscriber_priority: number // u8 - group_order: GroupOrder - filter_type: FilterType - start_location?: Location - end_group?: bigint - params?: Parameters + id: bigint // Request ID + params?: Parameters } - export namespace PublishOk { - export function serialize(v: PublishOk): Uint8Array { - const mainBuf = new MutableBytesBuffer(new Uint8Array()) - mainBuf.putVarInt(ControlMessageType.PublishOk) - const payloadBuf = new MutableBytesBuffer(new Uint8Array()) - payloadBuf.putVarInt(v.id) - payloadBuf.putU8(v.forward) - payloadBuf.putU8(v.subscriber_priority) - payloadBuf.putBytes(GroupOrder.serialize(v.group_order)) - payloadBuf.putBytes(FilterType.serialize(v.filter_type)) - if (v.start_location) { - payloadBuf.putBytes(Location.serialize(v.start_location)) - } - if (v.end_group) { - payloadBuf.putVarInt(v.end_group) - } - payloadBuf.putBytes(Parameters.serialize(v.params ?? new Map())) + export function serialize(v: PublishOk): Uint8Array { + const mainBuf = new MutableBytesBuffer(new Uint8Array()) + mainBuf.putVarInt(ControlMessageType.PublishOk) + const payloadBuf = new MutableBytesBuffer(new Uint8Array()) + payloadBuf.putVarInt(v.id) + const params = v.params ?? new Map() + payloadBuf.putVarInt(BigInt(params.size)) + payloadBuf.putBytes(Parameters.serialize(params)) - mainBuf.putU16(payloadBuf.byteLength) - mainBuf.putBytes(payloadBuf.Uint8Array) - return mainBuf.Uint8Array - } + mainBuf.putU16(payloadBuf.byteLength) + mainBuf.putBytes(payloadBuf.Uint8Array) + return mainBuf.Uint8Array + } - export function deserialize(reader: ImmutableBytesBuffer): PublishOk { - const id = reader.getVarInt() - const forward = reader.getU8() - const subscriber_priority = reader.getU8() - const group_order = GroupOrder.deserialize(reader) - const filter_type = FilterType.deserialize(reader) - let start_location: Location | undefined = undefined - if (filter_type == FilterType.AbsoluteStart || filter_type == FilterType.AbsoluteRange) { - start_location = Location.deserialize(reader) - } - let end_group: bigint | undefined = undefined - if (filter_type == FilterType.AbsoluteRange) { - end_group = reader.getVarInt() - } - let params: Parameters | undefined = undefined - if (reader.remaining > 0) { - params = Parameters.deserialize(reader) - } - return { - id, - forward, - subscriber_priority, - group_order, - filter_type, - start_location, - end_group, - params - } - } -} \ No newline at end of file + export function deserialize(reader: ImmutableBytesBuffer): PublishOk { + const id = reader.getVarInt() + const numParams = reader.getNumberVarInt() + let params: Parameters | undefined + if (numParams > 0) { + params = Parameters.deserialize_with_count(reader, Number(numParams)) + } + return { + id, + params + } + } +} diff --git a/lib/transport/control/request_error.ts b/lib/transport/control/request_error.ts new file mode 100644 index 0000000..75fb6f3 --- /dev/null +++ b/lib/transport/control/request_error.ts @@ -0,0 +1,41 @@ +import { ControlMessageType } from "." +import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" +import { ReasonPhrase } from "../base_data" + +// Draft-16: REQUEST_ERROR (Section 9.8) +// Sent in response to any request (SUBSCRIBE, FETCH, PUBLISH, SUBSCRIBE_NAMESPACE, PUBLISH_NAMESPACE, TRACK_STATUS) +export interface RequestError { + id: bigint // Request ID + code: bigint // Error Code (RequestErrorCode) + retry_interval: bigint // Minimum retry time in ms + 1; 0 = don't retry + reason: ReasonPhrase +} + +export namespace RequestError { + export function serialize(v: RequestError): Uint8Array { + const mainBuf = new MutableBytesBuffer(new Uint8Array()) + mainBuf.putVarInt(ControlMessageType.RequestError) + const payloadBuf = new MutableBytesBuffer(new Uint8Array()) + payloadBuf.putVarInt(v.id) + payloadBuf.putVarInt(v.code) + payloadBuf.putVarInt(v.retry_interval) + payloadBuf.putBytes(ReasonPhrase.serialize(v.reason)) + + mainBuf.putU16(payloadBuf.byteLength) + mainBuf.putBytes(payloadBuf.Uint8Array) + return mainBuf.Uint8Array + } + + export function deserialize(reader: ImmutableBytesBuffer): RequestError { + const id = reader.getVarInt() + const code = reader.getVarInt() + const retry_interval = reader.getVarInt() + const reason = ReasonPhrase.deserialize(reader) + return { + id, + code, + retry_interval, + reason, + } + } +} diff --git a/lib/transport/control/request_ok.ts b/lib/transport/control/request_ok.ts new file mode 100644 index 0000000..0017d28 --- /dev/null +++ b/lib/transport/control/request_ok.ts @@ -0,0 +1,36 @@ +import { ControlMessageType } from "." +import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" +import { Parameters } from "../base_data" + +// Draft-16: REQUEST_OK (Section 9.7) +// Sent in response to REQUEST_UPDATE, TRACK_STATUS, SUBSCRIBE_NAMESPACE, PUBLISH_NAMESPACE +export interface RequestOk { + id: bigint // Request ID + parameters: Parameters +} + +export namespace RequestOk { + export function serialize(v: RequestOk): Uint8Array { + const mainBuf = new MutableBytesBuffer(new Uint8Array()) + mainBuf.putVarInt(ControlMessageType.RequestOk) + const payloadBuf = new MutableBytesBuffer(new Uint8Array()) + payloadBuf.putVarInt(v.id) + const paramsBytes = Parameters.serialize(v.parameters) + payloadBuf.putVarInt(v.parameters.size) // Number of Parameters + payloadBuf.putBytes(paramsBytes) + + mainBuf.putU16(payloadBuf.byteLength) + mainBuf.putBytes(payloadBuf.Uint8Array) + return mainBuf.Uint8Array + } + + export function deserialize(reader: ImmutableBytesBuffer): RequestOk { + const id = reader.getVarInt() + const numParams = reader.getNumberVarInt() + const parameters = Parameters.deserialize_with_count(reader, numParams) + return { + id, + parameters, + } + } +} diff --git a/lib/transport/control/requests_block.ts b/lib/transport/control/requests_block.ts deleted file mode 100644 index 9d9beb7..0000000 --- a/lib/transport/control/requests_block.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { ControlMessageType } from "." -import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" - - -export interface RequestsBlocked { - id: bigint -} - -export namespace RequestsBlocked { - export function serialize(v: RequestsBlocked): Uint8Array { - const mainBuf = new MutableBytesBuffer(new Uint8Array()) - mainBuf.putVarInt(ControlMessageType.RequestsBlocked) - const payloadBuf = new MutableBytesBuffer(new Uint8Array()) - payloadBuf.putVarInt(v.id) - - mainBuf.putU16(payloadBuf.byteLength) - mainBuf.putBytes(payloadBuf.Uint8Array) - return mainBuf.Uint8Array - } - - export function deserialize(reader: ImmutableBytesBuffer): RequestsBlocked { - const id = reader.getVarInt() - return { - id, - } - } -} \ No newline at end of file diff --git a/lib/transport/control/requests_blocked.ts b/lib/transport/control/requests_blocked.ts new file mode 100644 index 0000000..10050b0 --- /dev/null +++ b/lib/transport/control/requests_blocked.ts @@ -0,0 +1,30 @@ +import { ControlMessageType } from "." +import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" + + +// Draft-16: REQUESTS_BLOCKED (Section 9.6, type 0x1A) +// Sent when an endpoint would like to send a new request, but cannot +// because the Request ID would exceed the Maximum Request ID value sent by the peer. +export interface RequestsBlocked { + maximum_request_id: bigint +} + +export namespace RequestsBlocked { + export function serialize(v: RequestsBlocked): Uint8Array { + const mainBuf = new MutableBytesBuffer(new Uint8Array()) + mainBuf.putVarInt(ControlMessageType.RequestsBlocked) + const payloadBuf = new MutableBytesBuffer(new Uint8Array()) + payloadBuf.putVarInt(v.maximum_request_id) + + mainBuf.putU16(payloadBuf.byteLength) + mainBuf.putBytes(payloadBuf.Uint8Array) + return mainBuf.Uint8Array + } + + export function deserialize(reader: ImmutableBytesBuffer): RequestsBlocked { + const maximum_request_id = reader.getVarInt() + return { + maximum_request_id, + } + } +} diff --git a/lib/transport/control/server_setup.ts b/lib/transport/control/server_setup.ts index 2f531a3..550c954 100644 --- a/lib/transport/control/server_setup.ts +++ b/lib/transport/control/server_setup.ts @@ -3,27 +3,28 @@ import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" import { Parameters } from "../base_data" export interface ServerSetup { - version: Version - params: Parameters + params: Parameters } export namespace ServerSetup { - export function serialize(v: ServerSetup): Uint8Array { - const mainBuf = new MutableBytesBuffer(new Uint8Array()) - mainBuf.putVarInt(ControlMessageType.ServerSetup) - const payloadBuf = new MutableBytesBuffer(new Uint8Array()) - payloadBuf.putVarInt(v.version) - payloadBuf.putBytes(Parameters.serialize(v.params)) - mainBuf.putU16(payloadBuf.byteLength) - mainBuf.putBytes(payloadBuf.Uint8Array) - return mainBuf.Uint8Array - } + export function serialize(v: ServerSetup): Uint8Array { + const mainBuf = new MutableBytesBuffer(new Uint8Array()) + mainBuf.putVarInt(ControlMessageType.ServerSetup) + const payloadBuf = new MutableBytesBuffer(new Uint8Array()) + const paramsBytes = Parameters.serialize(v.params) + payloadBuf.putVarInt(v.params.size) + payloadBuf.putBytes(paramsBytes) - export function deserialize(reader: ImmutableBytesBuffer): ServerSetup { - const version = reader.getNumberVarInt() as Version - return { - version, - params: Parameters.deserialize(reader) - } - } -} \ No newline at end of file + mainBuf.putU16(payloadBuf.byteLength) + mainBuf.putBytes(payloadBuf.Uint8Array) + return mainBuf.Uint8Array + } + + export function deserialize(reader: ImmutableBytesBuffer): ServerSetup { + const numParams = reader.getNumberVarInt() + const params = Parameters.deserialize_with_count(reader, numParams) + return { + params + } + } +} diff --git a/lib/transport/control/subscribe.ts b/lib/transport/control/subscribe.ts index 9ea3314..e04ae81 100644 --- a/lib/transport/control/subscribe.ts +++ b/lib/transport/control/subscribe.ts @@ -1,143 +1,104 @@ import { ControlMessageType } from "." import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" -import { Tuple, Parameters, Location, KeyValuePairs } from "../base_data" -import { debug } from "../utils" +import { Tuple, Parameters } from "../base_data" export enum GroupOrder { - Publisher = 0x0, - Ascending = 0x1, - Descending = 0x2, + Publisher = 0x0, + Ascending = 0x1, + Descending = 0x2, } export namespace GroupOrder { - export function serialize(v: GroupOrder): Uint8Array { - const buf = new MutableBytesBuffer(new Uint8Array()) - buf.putU8(v) - return buf.Uint8Array - } + export function serialize(v: GroupOrder): Uint8Array { + const buf = new MutableBytesBuffer(new Uint8Array()) + buf.putU8(v) + return buf.Uint8Array + } - export function deserialize(buffer: ImmutableBytesBuffer): GroupOrder { - const order = buffer.getU8() - switch (order) { - case 0: - return GroupOrder.Publisher - case 1: - return GroupOrder.Ascending - case 2: - return GroupOrder.Descending - default: - throw new Error(`Invalid GroupOrder value: ${order}`) - } - } + export function deserialize(buffer: ImmutableBytesBuffer): GroupOrder { + const order = buffer.getU8() + switch (order) { + case 0: + return GroupOrder.Publisher + case 1: + return GroupOrder.Ascending + case 2: + return GroupOrder.Descending + default: + throw new Error(`Invalid GroupOrder value: ${order}`) + } + } } export enum FilterType { - NextGroupStart = 0x1, - LargestObject = 0x2, - AbsoluteStart = 0x3, - AbsoluteRange = 0x4, + NextGroupStart = 0x1, + LargestObject = 0x2, + AbsoluteStart = 0x3, + AbsoluteRange = 0x4, } export namespace FilterType { - export function serialize(v: FilterType): Uint8Array { - const buf = new MutableBytesBuffer(new Uint8Array()) - buf.putVarInt(v) - return buf.Uint8Array - } + export function serialize(v: FilterType): Uint8Array { + const buf = new MutableBytesBuffer(new Uint8Array()) + buf.putVarInt(v) + return buf.Uint8Array + } - export function deserialize(buffer: ImmutableBytesBuffer): FilterType { - const order = buffer.getVarInt() - switch (order) { - case 1n: - return FilterType.NextGroupStart - case 2n: - return FilterType.LargestObject - case 3n: - return FilterType.AbsoluteStart - case 4n: - return FilterType.AbsoluteRange - default: - throw new Error(`Invalid FilterType value: ${order}`) - } - } + export function deserialize(buffer: ImmutableBytesBuffer): FilterType { + const order = buffer.getVarInt() + switch (order) { + case 1n: + return FilterType.NextGroupStart + case 2n: + return FilterType.LargestObject + case 3n: + return FilterType.AbsoluteStart + case 4n: + return FilterType.AbsoluteRange + default: + throw new Error(`Invalid FilterType value: ${order}`) + } + } } export interface Subscribe { - id: bigint // Request ID in draft-14 - namespace: Tuple, - name: string - subscriber_priority: number // u8 - group_order: GroupOrder - forward: number // u8 -> 0/1 - filter_type: FilterType - start_location?: Location - end_group?: bigint - params: Parameters + id: bigint // Request ID + namespace: Tuple, + name: string + params: Parameters } export namespace Subscribe { - export function serialize(v: Subscribe): Uint8Array { - const mainBuf = new MutableBytesBuffer(new Uint8Array()) - mainBuf.putVarInt(ControlMessageType.Subscribe) - const payloadBuf = new MutableBytesBuffer(new Uint8Array()) - payloadBuf.putVarInt(v.id) - payloadBuf.putBytes(Tuple.serialize(v.namespace)) - payloadBuf.putUtf8String(v.name) - payloadBuf.putU8(v.subscriber_priority) - payloadBuf.putBytes(GroupOrder.serialize(v.group_order)) - payloadBuf.putU8(v.forward) - payloadBuf.putBytes(FilterType.serialize(v.filter_type)) + export function serialize(v: Subscribe): Uint8Array { + const mainBuf = new MutableBytesBuffer(new Uint8Array()) + mainBuf.putVarInt(ControlMessageType.Subscribe) + const payloadBuf = new MutableBytesBuffer(new Uint8Array()) + payloadBuf.putVarInt(v.id) + payloadBuf.putBytes(Tuple.serialize(v.namespace)) + payloadBuf.putUtf8String(v.name) + // Draft-16: inline parameters moved to Parameters KVP + const paramsBytes = Parameters.serialize(v.params) + payloadBuf.putVarInt(v.params.size) // Number of Parameters + payloadBuf.putBytes(paramsBytes) - if (v.filter_type === FilterType.AbsoluteStart || v.filter_type === FilterType.AbsoluteRange) { - if (!v.start_location) { - throw new Error('start location required for absolute start or absolute range') - } - payloadBuf.putBytes(Location.serialize(v.start_location)) - } + mainBuf.putU16(payloadBuf.byteLength) + mainBuf.putBytes(payloadBuf.Uint8Array) + return mainBuf.Uint8Array + } - if (v.filter_type === FilterType.AbsoluteRange) { - if (v.end_group == null) { - throw new Error('end group required for absolute range') - } - payloadBuf.putVarInt(v.end_group) - } - payloadBuf.putBytes(Parameters.serialize(v.params)) - - mainBuf.putU16(payloadBuf.byteLength) - mainBuf.putBytes(payloadBuf.Uint8Array) - return mainBuf.Uint8Array - } - - export function deserialize(reader: ImmutableBytesBuffer): Subscribe { - const id = reader.getVarInt() - const namespace = Tuple.deserialize(reader) - const name = reader.getUtf8String() - const subscriber_priority = reader.getU8() - const group_order = GroupOrder.deserialize(reader) - const forward = reader.getU8() - const filter_type = FilterType.deserialize(reader) - let start_location: Location | undefined - if (filter_type == FilterType.AbsoluteRange || filter_type == FilterType.AbsoluteStart) { - start_location = Location.deserialize(reader) - } - let end_group: bigint | undefined - if (filter_type == FilterType.AbsoluteRange) { - end_group = reader.getVarInt() - } - const params = Parameters.deserialize(reader) - return { - id, - namespace, - name, - subscriber_priority, - group_order, - forward, - filter_type, - start_location, - end_group, - params - } - } -} \ No newline at end of file + export function deserialize(reader: ImmutableBytesBuffer): Subscribe { + const id = reader.getVarInt() + const namespace = Tuple.deserialize(reader) + const name = reader.getUtf8String() + const numParams = reader.getNumberVarInt() + const params = Parameters.deserialize_with_count(reader, numParams) + return { + id, + namespace, + name, + params, + } + } +} diff --git a/lib/transport/control/subscribe_error.ts b/lib/transport/control/subscribe_error.ts deleted file mode 100644 index c2121a3..0000000 --- a/lib/transport/control/subscribe_error.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { ControlMessageType } from "." -import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" - -export interface SubscribeError { - id: bigint // Request ID in draft-14 - code: bigint - reason: string -} - - -export namespace SubscribeError { - export function serialize(v: SubscribeError): Uint8Array { - const mainBuf = new MutableBytesBuffer(new Uint8Array()) - mainBuf.putVarInt(ControlMessageType.SubscribeError) - const payloadBuf = new MutableBytesBuffer(new Uint8Array()) - payloadBuf.putVarInt(v.id) - payloadBuf.putVarInt(v.code) - payloadBuf.putUtf8String(v.reason) - - mainBuf.putU16(payloadBuf.byteLength) - mainBuf.putBytes(payloadBuf.Uint8Array) - return mainBuf.Uint8Array - } - - export function deserialize(reader: ImmutableBytesBuffer): SubscribeError { - const id = reader.getVarInt() - const code = reader.getVarInt() - const reason = reader.getUtf8String() - return { - id, - code, - reason - } - } -} \ No newline at end of file diff --git a/lib/transport/control/subscribe_namespace.ts b/lib/transport/control/subscribe_namespace.ts index 605e716..d930c51 100644 --- a/lib/transport/control/subscribe_namespace.ts +++ b/lib/transport/control/subscribe_namespace.ts @@ -1,39 +1,49 @@ -import { ControlMessageType, FilterType, GroupOrder } from "." +import { ControlMessageType } from "." import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" -import { Tuple, Parameters, Location, KeyValuePairs } from "../base_data" +import { Tuple, Parameters, KeyValuePairs } from "../base_data" +export enum SubscribeOptions { + PUBLISH = 0x00, + NAMESPACE = 0x01, + BOTH = 0x02, +} + export interface SubscribeNamespace { - id: bigint, - namespace: string[] - params?: Parameters + id: bigint, + namespace: string[] + subscribe_options: SubscribeOptions + params?: Parameters } export namespace SubscribeNamespace { - export function serialize(v: SubscribeNamespace): Uint8Array { - const mainBuf = new MutableBytesBuffer(new Uint8Array()) - mainBuf.putVarInt(ControlMessageType.SubscribeNamespace) - const payloadBuf = new MutableBytesBuffer(new Uint8Array()) - payloadBuf.putVarInt(v.id) - payloadBuf.putBytes(Tuple.serialize(v.namespace)) - payloadBuf.putBytes(KeyValuePairs.serialize(v.params ?? new Map())) - mainBuf.putU16(payloadBuf.byteLength) - mainBuf.putBytes(payloadBuf.Uint8Array) - return mainBuf.Uint8Array - } + export function serialize(v: SubscribeNamespace): Uint8Array { + const mainBuf = new MutableBytesBuffer(new Uint8Array()) + mainBuf.putVarInt(ControlMessageType.SubscribeNamespace) + const payloadBuf = new MutableBytesBuffer(new Uint8Array()) + payloadBuf.putVarInt(v.id) + payloadBuf.putBytes(Tuple.serialize(v.namespace)) + payloadBuf.putVarInt(v.subscribe_options) + payloadBuf.putBytes(KeyValuePairs.serialize(v.params ?? new Map())) + mainBuf.putU16(payloadBuf.byteLength) + mainBuf.putBytes(payloadBuf.Uint8Array) + return mainBuf.Uint8Array + } - export function deserialize(reader: ImmutableBytesBuffer): SubscribeNamespace { - const id = reader.getVarInt() - const namespace = Tuple.deserialize(reader) - let params: Parameters | undefined - if (reader.remaining > 0) { - params = KeyValuePairs.deserialize(reader) - } - return { - id, - namespace, - params - } - } -} \ No newline at end of file + export function deserialize(reader: ImmutableBytesBuffer): SubscribeNamespace { + const id = reader.getVarInt() + const namespace = Tuple.deserialize(reader) + const subscribe_options = reader.getNumberVarInt() as SubscribeOptions + let params: Parameters | undefined + if (reader.remaining > 0) { + params = KeyValuePairs.deserialize(reader) + } + return { + id, + namespace, + subscribe_options, + params + } + } +} diff --git a/lib/transport/control/subscribe_namespace_error.ts b/lib/transport/control/subscribe_namespace_error.ts deleted file mode 100644 index f1d24f6..0000000 --- a/lib/transport/control/subscribe_namespace_error.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { ControlMessageType } from "." -import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" - - -export interface SubscribeNamespaceError { - id: bigint, - code: bigint, - reason: string -} - - -export namespace SubscribeNamespaceError { - export function serialize(v: SubscribeNamespaceError): Uint8Array { - const mainBuf = new MutableBytesBuffer(new Uint8Array()) - mainBuf.putVarInt(ControlMessageType.SubscribeNamespaceError) - const payloadBuf = new MutableBytesBuffer(new Uint8Array()) - payloadBuf.putVarInt(v.id) - payloadBuf.putVarInt(v.code) - payloadBuf.putUtf8String(v.reason) - - mainBuf.putU16(payloadBuf.byteLength) - mainBuf.putBytes(payloadBuf.Uint8Array) - return mainBuf.Uint8Array - } - - export function deserialize(reader: ImmutableBytesBuffer): SubscribeNamespaceError { - const id = reader.getVarInt() - const code = reader.getVarInt() - const reason = reader.getUtf8String() - return { - id, - code, - reason - } - } -} \ No newline at end of file diff --git a/lib/transport/control/subscribe_namespace_ok.ts b/lib/transport/control/subscribe_namespace_ok.ts deleted file mode 100644 index 3da6031..0000000 --- a/lib/transport/control/subscribe_namespace_ok.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { ControlMessageType } from "." -import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" - - - -export interface SubscribeNamespaceOk { - id: bigint, -} - - -export namespace SubscribeNamespaceOk { - export function serialize(v: SubscribeNamespaceOk): Uint8Array { - const mainBuf = new MutableBytesBuffer(new Uint8Array()) - mainBuf.putVarInt(ControlMessageType.SubscribeNamespaceOk) - const payloadBuf = new MutableBytesBuffer(new Uint8Array()) - payloadBuf.putVarInt(v.id) - mainBuf.putU16(payloadBuf.byteLength) - mainBuf.putBytes(payloadBuf.Uint8Array) - return mainBuf.Uint8Array - } - - export function deserialize(reader: ImmutableBytesBuffer): SubscribeNamespaceOk { - const id = reader.getVarInt() - return { - id, - } - } -} \ No newline at end of file diff --git a/lib/transport/control/subscribe_ok.ts b/lib/transport/control/subscribe_ok.ts index 91b6c98..50637bd 100644 --- a/lib/transport/control/subscribe_ok.ts +++ b/lib/transport/control/subscribe_ok.ts @@ -1,64 +1,58 @@ import { ControlMessageType } from "." import { GroupOrder } from "./subscribe" import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" -import { Parameters, Location } from "../base_data" +import { Parameters, KeyValuePairs } from "../base_data" export interface SubscribeOk { - id: bigint // Request ID - track_alias: bigint // Publisher-specified in draft-14 - expires: bigint - group_order: GroupOrder - content_exists: number // 0 or 1 - largest_location?: Location - params: Parameters + id: bigint // Request ID + track_alias: bigint + params: Parameters + track_extensions?: KeyValuePairs } export namespace SubscribeOk { - export function serialize(v: SubscribeOk): Uint8Array { - const mainBuf = new MutableBytesBuffer(new Uint8Array()) - mainBuf.putVarInt(ControlMessageType.SubscribeOk) - const payloadBuf = new MutableBytesBuffer(new Uint8Array()) - payloadBuf.putVarInt(v.id) - payloadBuf.putVarInt(v.track_alias) - payloadBuf.putVarInt(v.expires) - payloadBuf.putBytes(GroupOrder.serialize(v.group_order)) - payloadBuf.putU8(v.content_exists) - if (v.content_exists) { - if (!v.largest_location) { - throw new Error('largest_location required for content_exists') - } - payloadBuf.putBytes(Location.serialize(v.largest_location)) - } + export function serialize(v: SubscribeOk): Uint8Array { + const mainBuf = new MutableBytesBuffer(new Uint8Array()) + mainBuf.putVarInt(ControlMessageType.SubscribeOk) + const payloadBuf = new MutableBytesBuffer(new Uint8Array()) + payloadBuf.putVarInt(v.id) + payloadBuf.putVarInt(v.track_alias) + const paramsBytes = Parameters.serialize(v.params) + payloadBuf.putVarInt(v.params.size) // Number of Parameters + payloadBuf.putBytes(paramsBytes) + // Draft-16: Track Extensions (length-prefixed KVP block) + if (v.track_extensions && v.track_extensions.size > 0) { + const extBytes = KeyValuePairs.serialize(v.track_extensions) + payloadBuf.putVarInt(extBytes.length) + payloadBuf.putBytes(extBytes) + } else { + payloadBuf.putVarInt(0) // empty track extensions + } - payloadBuf.putBytes(Parameters.serialize(v.params)) + mainBuf.putU16(payloadBuf.byteLength) + mainBuf.putBytes(payloadBuf.Uint8Array) + return mainBuf.Uint8Array + } - mainBuf.putU16(payloadBuf.byteLength) - mainBuf.putBytes(payloadBuf.Uint8Array) - return mainBuf.Uint8Array - } - - export function deserialize(reader: ImmutableBytesBuffer): SubscribeOk { - const id = reader.getVarInt() - const track_alias = reader.getVarInt() - const expires = reader.getVarInt() - const group_order = GroupOrder.deserialize(reader) - const content_exists = reader.getNumberVarInt() - if (content_exists != 0 && content_exists != 1) { - throw new Error("Invalid content_exists value") - } - let largest_location: Location | undefined - if (content_exists) { - largest_location = Location.deserialize(reader) - } - const params = Parameters.deserialize(reader) - return { - id, - track_alias, - expires, - group_order, - content_exists, - largest_location, - params - } - } -} \ No newline at end of file + export function deserialize(reader: ImmutableBytesBuffer): SubscribeOk { + const id = reader.getVarInt() + const track_alias = reader.getVarInt() + const numParams = reader.getNumberVarInt() + const params = Parameters.deserialize_with_count(reader, numParams) + // Draft-16: Track Extensions + let track_extensions: KeyValuePairs | undefined + if (reader.remaining > 0) { + const extLength = reader.getNumberVarInt() + if (extLength > 0) { + const extData = reader.getBytes(extLength) + track_extensions = KeyValuePairs.deserialize(new ImmutableBytesBuffer(extData)) + } + } + return { + id, + track_alias, + params, + track_extensions, + } + } +} diff --git a/lib/transport/control/subscribe_update.ts b/lib/transport/control/subscribe_update.ts index 19f9405..82ed7d3 100644 --- a/lib/transport/control/subscribe_update.ts +++ b/lib/transport/control/subscribe_update.ts @@ -1,56 +1,41 @@ -import { ControlMessageType, GroupOrder } from "." +import { ControlMessageType } from "." import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" -import { Parameters, KeyValuePairs, Location } from "../base_data" +import { Parameters, Location } from "../base_data" export interface SubscribeUpdate { - id: bigint - subscription_id: bigint - start_location: Location - end_group: bigint - subscriber_priority: number - forward: number - params?: Parameters + id: bigint // Request ID (new) + subscription_id: bigint // Existing Request ID + params?: Parameters } export namespace SubscribeUpdate { - export function serialize(v: SubscribeUpdate): Uint8Array { - const mainBuf = new MutableBytesBuffer(new Uint8Array()) - mainBuf.putVarInt(ControlMessageType.SubscribeUpdate) + export function serialize(v: SubscribeUpdate): Uint8Array { + const mainBuf = new MutableBytesBuffer(new Uint8Array()) + mainBuf.putVarInt(ControlMessageType.SubscribeUpdate) - const payloadBuf = new MutableBytesBuffer(new Uint8Array()) - payloadBuf.putVarInt(v.id) - payloadBuf.putVarInt(v.subscription_id) - payloadBuf.putBytes(Location.serialize(v.start_location)) - payloadBuf.putVarInt(v.end_group) - payloadBuf.putVarInt(v.subscriber_priority) - payloadBuf.putVarInt(v.forward) - payloadBuf.putBytes(KeyValuePairs.serialize(v.params ?? new Map())) + const payloadBuf = new MutableBytesBuffer(new Uint8Array()) + payloadBuf.putVarInt(v.id) + payloadBuf.putVarInt(v.subscription_id) // Existing Request ID + const params = v.params ?? new Map() + const paramsBytes = Parameters.serialize(params) + payloadBuf.putVarInt(params.size) // Number of Parameters + payloadBuf.putBytes(paramsBytes) - mainBuf.putU16(payloadBuf.byteLength) - mainBuf.putBytes(payloadBuf.Uint8Array) - return mainBuf.Uint8Array - } + mainBuf.putU16(payloadBuf.byteLength) + mainBuf.putBytes(payloadBuf.Uint8Array) + return mainBuf.Uint8Array + } - export function deserialize(reader: ImmutableBytesBuffer): SubscribeUpdate { - const id = reader.getVarInt() - const subscription_id = reader.getVarInt() - const start_location = Location.deserialize(reader) - const end_group = reader.getVarInt() - const subscriber_priority = reader.getNumberVarInt() - const forward = reader.getNumberVarInt() - let params: Parameters | undefined - if (reader.remaining > 0) { - params = KeyValuePairs.deserialize(reader) - } - return { - id, - subscription_id, - start_location, - end_group, - subscriber_priority, - forward, - params - } - } + export function deserialize(reader: ImmutableBytesBuffer): SubscribeUpdate { + const id = reader.getVarInt() + const subscription_id = reader.getVarInt() + const numParams = reader.getNumberVarInt() + const params = Parameters.deserialize_with_count(reader, numParams) + return { + id, + subscription_id, + params, + } + } } diff --git a/lib/transport/control/track_status.ts b/lib/transport/control/track_status.ts new file mode 100644 index 0000000..4dab7e3 --- /dev/null +++ b/lib/transport/control/track_status.ts @@ -0,0 +1,42 @@ +import { ControlMessageType } from "." +import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" +import { Tuple, Parameters } from "../base_data" + + +export interface TrackStatus { + id: bigint + namespace: Tuple + name: string + params: Parameters +} + +export namespace TrackStatus { + export function serialize(v: TrackStatus): Uint8Array { + const mainBuf = new MutableBytesBuffer(new Uint8Array()) + mainBuf.putVarInt(ControlMessageType.TrackStatus) + const payloadBuf = new MutableBytesBuffer(new Uint8Array()) + payloadBuf.putVarInt(v.id) + payloadBuf.putBytes(Tuple.serialize(v.namespace)) + payloadBuf.putUtf8String(v.name) + const paramsBytes = Parameters.serialize(v.params) + payloadBuf.putVarInt(v.params.size) // Number of Parameters + payloadBuf.putBytes(paramsBytes) + mainBuf.putU16(payloadBuf.byteLength) + mainBuf.putBytes(payloadBuf.Uint8Array) + return mainBuf.Uint8Array + } + + export function deserialize(reader: ImmutableBytesBuffer): TrackStatus { + const id = reader.getVarInt() + const namespace = Tuple.deserialize(reader) + const name = reader.getUtf8String() + const numParams = reader.getNumberVarInt() + const params = Parameters.deserialize_with_count(reader, numParams) + return { + id, + namespace, + name, + params, + } + } +} diff --git a/lib/transport/objects.ts b/lib/transport/objects.ts index e93846a..c8aa534 100644 --- a/lib/transport/objects.ts +++ b/lib/transport/objects.ts @@ -10,7 +10,6 @@ export enum ObjectForwardingPreference { export enum Status { NORMAL = 0, - OBJECT_NULL = 1, GROUP_END = 3, TRACK_END = 4, } @@ -30,8 +29,6 @@ export namespace Status { switch (v) { case 0: return Status.NORMAL - case 1: - return Status.OBJECT_NULL case 3: return Status.GROUP_END case 4: @@ -56,10 +53,20 @@ export interface Object { } export function isDatagram(obj: ObjectDatagram | SubgroupHeader): boolean { - return obj.type in ObjectDatagramType + // Datagram types have bit 4 NOT set; subgroup types have bit 4 SET + return (obj.type & 0x10) === 0 } +// Draft-16: Object Datagram types use bitmask structure 0b00X0XXXX +// Valid ranges: 0x00..0x0F, 0x20..0x2F +// Bit layout: +// bit 0 (0x01) = EXTENSIONS +// bit 1 (0x02) = END_OF_GROUP +// bit 2 (0x04) = ZERO_OBJECT_ID (when set, Object ID omitted, implied 0) +// bit 3 (0x08) = DEFAULT_PRIORITY (when set, priority field omitted) +// bit 5 (0x20) = STATUS (when set, Object Status present instead of payload) +// Invalid combinations: STATUS + END_OF_GROUP (0x22,0x23,0x26,0x27,0x2A,0x2B,0x2E,0x2F) export enum ObjectDatagramType { Type0x0 = 0x0, Type0x1 = 0x1, @@ -69,11 +76,32 @@ export enum ObjectDatagramType { Type0x5 = 0x5, Type0x6 = 0x6, Type0x7 = 0x7, + Type0x8 = 0x8, + Type0x9 = 0x9, + Type0xA = 0xa, + Type0xB = 0xb, + Type0xC = 0xc, + Type0xD = 0xd, + Type0xE = 0xe, + Type0xF = 0xf, Type0x20 = 0x20, Type0x21 = 0x21, + Type0x24 = 0x24, + Type0x25 = 0x25, + Type0x28 = 0x28, + Type0x29 = 0x29, + Type0x2C = 0x2c, + Type0x2D = 0x2d, } export namespace ObjectDatagramType { + // Bitmask constants + const EXTENSIONS_BIT = 0x01 + const END_OF_GROUP_BIT = 0x02 + const ZERO_OBJECT_ID_BIT = 0x04 + const DEFAULT_PRIORITY_BIT = 0x08 + const STATUS_BIT = 0x20 + export function serialize(type: ObjectDatagramType): Uint8Array { const w = new MutableBytesBuffer(new Uint8Array()) w.putVarInt(type) @@ -85,68 +113,40 @@ export namespace ObjectDatagramType { export function try_from(value: number | bigint): ObjectDatagramType { const v = typeof value === "bigint" ? Number(value) : value - switch (v) { - case ObjectDatagramType.Type0x0: - case ObjectDatagramType.Type0x1: - case ObjectDatagramType.Type0x2: - case ObjectDatagramType.Type0x3: - case ObjectDatagramType.Type0x4: - case ObjectDatagramType.Type0x5: - case ObjectDatagramType.Type0x6: - case ObjectDatagramType.Type0x7: - case ObjectDatagramType.Type0x20: - case ObjectDatagramType.Type0x21: - return v as ObjectDatagramType - default: - throw new Error(`invalid object datagram type: ${v}`) + // Must match form 0b00X0XXXX (bit 4 NOT set) + if ((v & 0x10) !== 0) { + throw new Error(`invalid object datagram type: ${v} (bit 4 set - this is a subgroup type)`) + } + // Must be in ranges 0x00..0x0F or 0x20..0x2F + if (v < 0x00 || (v > 0x0F && v < 0x20) || v > 0x2F) { + throw new Error(`invalid object datagram type: ${v} (out of range)`) } + // STATUS + END_OF_GROUP is invalid + if ((v & STATUS_BIT) !== 0 && (v & END_OF_GROUP_BIT) !== 0) { + throw new Error(`invalid object datagram type: ${v} (STATUS + END_OF_GROUP combination)`) + } + return v as ObjectDatagramType } export function isEndOfGroup(type: ObjectDatagramType) { - switch (type) { - case ObjectDatagramType.Type0x2: - case ObjectDatagramType.Type0x3: - case ObjectDatagramType.Type0x6: - case ObjectDatagramType.Type0x7: - return true - default: - return false - } + return (type & END_OF_GROUP_BIT) !== 0 } export function hasExtensions(type: ObjectDatagramType) { - switch (type) { - case ObjectDatagramType.Type0x1: - case ObjectDatagramType.Type0x3: - case ObjectDatagramType.Type0x5: - case ObjectDatagramType.Type0x7: - case ObjectDatagramType.Type0x21: - return true - default: - return false - } + return (type & EXTENSIONS_BIT) !== 0 } export function hasObjectId(type: ObjectDatagramType) { - switch (type) { - case ObjectDatagramType.Type0x4: - case ObjectDatagramType.Type0x5: - case ObjectDatagramType.Type0x6: - case ObjectDatagramType.Type0x7: - return false - default: - return true - } + // When ZERO_OBJECT_ID bit is set, Object ID is omitted (implied 0) + return (type & ZERO_OBJECT_ID_BIT) === 0 + } + + export function hasDefaultPriority(type: ObjectDatagramType) { + return (type & DEFAULT_PRIORITY_BIT) !== 0 } export function hasStatus(type: ObjectDatagramType) { - switch (type) { - case ObjectDatagramType.Type0x20: - case ObjectDatagramType.Type0x21: - return true - default: - return false - } + return (type & STATUS_BIT) !== 0 } } @@ -155,7 +155,7 @@ export interface ObjectDatagram { track_alias: bigint group_id: number object_id?: number - publisher_priority: number + publisher_priority?: number // undefined when DEFAULT_PRIORITY bit is set extension_headers?: KeyValuePairs status?: Status object_payload?: Uint8Array @@ -165,22 +165,32 @@ export namespace ObjectDatagram { export function serialize(obj: ObjectDatagram): Uint8Array { const buf = new MutableBytesBuffer(new Uint8Array()) buf.putBytes(ObjectDatagramType.serialize(obj.type)) + buf.putVarInt(obj.track_alias) buf.putVarInt(obj.group_id) - if (obj.object_id) { + if (ObjectDatagramType.hasObjectId(obj.type) && obj.object_id !== undefined) { buf.putVarInt(obj.object_id) } - if (obj.object_payload) { - buf.putVarInt(obj.object_payload.byteLength) - buf.putBytes(obj.object_payload) + if (!ObjectDatagramType.hasDefaultPriority(obj.type) && obj.publisher_priority !== undefined) { + buf.putU8(obj.publisher_priority) + } + if (ObjectDatagramType.hasExtensions(obj.type) && obj.extension_headers) { + const extHeadersBytes = KeyValuePairs.serialize(obj.extension_headers) + buf.putVarInt(extHeadersBytes.length) + buf.putBytes(extHeadersBytes) + } + if (ObjectDatagramType.hasStatus(obj.type)) { + if (obj.status !== undefined) { + buf.putVarInt(obj.status) + } } else { - buf.putVarInt(0) - buf.putVarInt(obj.status as number) + if (obj.object_payload) { + buf.putBytes(obj.object_payload) + } } return buf.Uint8Array } export function deserialize(reader: ImmutableBytesBuffer): ObjectDatagram { - const type = reader.getNumberVarInt() const alias = reader.getVarInt() const group = reader.getNumberVarInt() @@ -188,13 +198,15 @@ export namespace ObjectDatagram { if (ObjectDatagramType.hasObjectId(type)) { object_id = reader.getNumberVarInt() } - const publisher_priority = reader.getU8() + let publisher_priority: number | undefined + if (!ObjectDatagramType.hasDefaultPriority(type)) { + publisher_priority = reader.getU8() + } let extHeaders: KeyValuePairs | undefined if (ObjectDatagramType.hasExtensions(type)) { - const extHeadersLength = reader.getNumberVarInt() - const extHeadersData = reader.getBytes(extHeadersLength) - - extHeaders = KeyValuePairs.deserialize_with_size(new ImmutableBytesBuffer(extHeadersData), extHeadersLength) + const extHeadersBytesLength = reader.getNumberVarInt() + const extHeadersData = reader.getBytes(extHeadersBytesLength) + extHeaders = KeyValuePairs.deserialize(new ImmutableBytesBuffer(extHeadersData)) } let status: Status | undefined let payload: Uint8Array | undefined @@ -277,7 +289,10 @@ export class Objects { subgroup_id = undefined } - const publisher_priority = await r.getU8() + let publisher_priority: number | undefined + if (!SubgroupType.hasDefaultPriority(subgroupType)) { + publisher_priority = await r.getU8() + } const h: SubgroupHeader = { type: subgroupType, @@ -343,10 +358,15 @@ export class TrackReader { if (ObjectDatagramType.hasObjectId(type)) { object_id = await this.stream.getNumberVarInt() } - const publisher_priority = await this.stream.getU8() + let publisher_priority: number | undefined + if (!ObjectDatagramType.hasDefaultPriority(type)) { + publisher_priority = await this.stream.getU8() + } let extHeaders: KeyValuePairs | undefined if (ObjectDatagramType.hasExtensions(type)) { - extHeaders = await KeyValuePairs.deserialize_with_reader(this.stream) + const extHeadersBytesLength = await this.stream.getNumberVarInt() + const extHeadersData = await this.stream.read(extHeadersBytesLength) + extHeaders = KeyValuePairs.deserialize(new ImmutableBytesBuffer(extHeadersData)) } let status: Status | undefined let payload: Uint8Array | undefined diff --git a/lib/transport/stream.ts b/lib/transport/stream.ts index 27a2086..19b2fcc 100644 --- a/lib/transport/stream.ts +++ b/lib/transport/stream.ts @@ -1,14 +1,16 @@ import { - ControlMessageType, FetchError, + ControlMessageType, MessageWithType, Publish, - PublishDone, PublishError, PublishNamespace, - PublishNamespaceDone, PublishNamespaceError, - PublishNamespaceOk, PublishOk, Unsubscribe, + PublishDone, PublishNamespace, + PublishNamespaceDone, PublishNamespaceCancel, + PublishOk, Unsubscribe, Fetch, FetchOk, FetchCancel, - Subscribe, SubscribeOk, SubscribeError, + Subscribe, SubscribeOk, SubscribeUpdate, SubscribeNamespace, - SubscribeNamespaceOk, SubscribeNamespaceError, + Namespace, NamespaceDone, TrackStatus, + MaxRequestId, RequestsBlocked, + RequestOk, RequestError, } from "./control" import { debug } from "./utils" import { ImmutableBytesBuffer, ReadableWritableStreamBuffer, Reader, Writer } from "./buffer" @@ -106,12 +108,6 @@ export class Decoder { message: SubscribeOk.deserialize(payload), } break - case ControlMessageType.SubscribeError: - res = { - type: t, - message: SubscribeError.deserialize(payload), - } - break case ControlMessageType.Unsubscribe: res = { type: t, @@ -142,76 +138,88 @@ export class Decoder { message: PublishOk.deserialize(payload), } break - case ControlMessageType.PublishError: + case ControlMessageType.PublishNamespace: res = { type: t, - message: PublishError.deserialize(payload), + message: PublishNamespace.deserialize(payload), } break - case ControlMessageType.PublishNamespace: + case ControlMessageType.PublishNamespaceDone: res = { type: t, - message: PublishNamespace.deserialize(payload), + message: PublishNamespaceDone.deserialize(payload), } break - case ControlMessageType.PublishNamespaceOk: + case ControlMessageType.Fetch: res = { type: t, - message: PublishNamespaceOk.deserialize(payload), + message: Fetch.deserialize(payload), } break - case ControlMessageType.PublishNamespaceDone: + case ControlMessageType.FetchCancel: res = { type: t, - message: PublishNamespaceDone.deserialize(payload), + message: FetchCancel.deserialize(payload), } break - case ControlMessageType.PublishNamespaceError: + case ControlMessageType.FetchOk: res = { type: t, - message: PublishNamespaceError.deserialize(payload), + message: FetchOk.deserialize(payload), } break - case ControlMessageType.Fetch: + case ControlMessageType.SubscribeNamespace: res = { type: t, - message: Fetch.deserialize(payload), + message: SubscribeNamespace.deserialize(payload), } break - case ControlMessageType.FetchCancel: + case ControlMessageType.RequestOk: res = { type: t, - message: FetchCancel.deserialize(payload), + message: RequestOk.deserialize(payload), } break - case ControlMessageType.FetchOk: + case ControlMessageType.RequestError: res = { type: t, - message: FetchOk.deserialize(payload), + message: RequestError.deserialize(payload), } break - case ControlMessageType.FetchError: + case ControlMessageType.PublishNamespaceCancel: res = { type: t, - message: FetchError.deserialize(payload), + message: PublishNamespaceCancel.deserialize(payload), } break - case ControlMessageType.SubscribeNamespace: + case ControlMessageType.Namespace: res = { type: t, - message: SubscribeNamespace.deserialize(payload), + message: Namespace.deserialize(payload), } break - case ControlMessageType.SubscribeNamespaceOk: + case ControlMessageType.NamespaceDone: res = { type: t, - message: SubscribeNamespaceOk.deserialize(payload), + message: NamespaceDone.deserialize(payload), } break - case ControlMessageType.SubscribeNamespaceError: + case ControlMessageType.TrackStatus: res = { type: t, - message: SubscribeNamespaceError.deserialize(payload), + message: TrackStatus.deserialize(payload), + } + break + case ControlMessageType.MaxRequestId: + res = { + type: t, + message: MaxRequestId.deserialize(payload), + } + break + case ControlMessageType.RequestsBlocked: + res = { + type: t, + message: RequestsBlocked.deserialize(payload), } break default: @@ -237,16 +245,10 @@ export class Encoder { return Subscribe.serialize(message as Subscribe) case ControlMessageType.SubscribeOk: return SubscribeOk.serialize(message as SubscribeOk) - case ControlMessageType.SubscribeError: - return SubscribeError.serialize(message as SubscribeError) case ControlMessageType.SubscribeUpdate: return SubscribeUpdate.serialize(message as SubscribeUpdate) case ControlMessageType.SubscribeNamespace: return SubscribeNamespace.serialize(message as SubscribeNamespace) - case ControlMessageType.SubscribeNamespaceOk: - return SubscribeNamespaceOk.serialize(message as SubscribeNamespaceOk) - case ControlMessageType.SubscribeNamespaceError: - return SubscribeNamespaceError.serialize(message as SubscribeNamespaceError) case ControlMessageType.Unsubscribe: return Unsubscribe.serialize(message as Unsubscribe) case ControlMessageType.Publish: @@ -255,14 +257,8 @@ export class Encoder { return PublishDone.serialize(message as PublishDone) case ControlMessageType.PublishOk: return PublishOk.serialize(message as PublishOk) - case ControlMessageType.PublishError: - return PublishError.serialize(message as PublishError) case ControlMessageType.PublishNamespace: return PublishNamespace.serialize(message as PublishNamespace) - case ControlMessageType.PublishNamespaceOk: - return PublishNamespaceOk.serialize(message as PublishNamespaceOk) - case ControlMessageType.PublishNamespaceError: - return PublishNamespaceError.serialize(message as PublishNamespaceError) case ControlMessageType.PublishNamespaceDone: return PublishNamespaceDone.serialize(message as PublishNamespaceDone) case ControlMessageType.Fetch: @@ -271,8 +267,22 @@ export class Encoder { return FetchCancel.serialize(message as FetchCancel) case ControlMessageType.FetchOk: return FetchOk.serialize(message as FetchOk) - case ControlMessageType.FetchError: - return FetchError.serialize(message as FetchError) + case ControlMessageType.RequestOk: + return RequestOk.serialize(message as RequestOk) + case ControlMessageType.RequestError: + return RequestError.serialize(message as RequestError) + case ControlMessageType.PublishNamespaceCancel: + return PublishNamespaceCancel.serialize(message as PublishNamespaceCancel) + case ControlMessageType.Namespace: + return Namespace.serialize(message as Namespace) + case ControlMessageType.NamespaceDone: + return NamespaceDone.serialize(message as NamespaceDone) + case ControlMessageType.TrackStatus: + return TrackStatus.serialize(message as TrackStatus) + case ControlMessageType.MaxRequestId: + return MaxRequestId.serialize(message as MaxRequestId) + case ControlMessageType.RequestsBlocked: + return RequestsBlocked.serialize(message as RequestsBlocked) default: throw new Error(`unknown message kind in encoder`) } @@ -282,4 +292,3 @@ export class Encoder { await this.w.write(payload) } } - diff --git a/lib/transport/subgroup.ts b/lib/transport/subgroup.ts index 4c34026..ae81db7 100644 --- a/lib/transport/subgroup.ts +++ b/lib/transport/subgroup.ts @@ -7,7 +7,7 @@ export interface SubgroupHeader { track_alias: bigint group_id: number subgroup_id?: number - publisher_priority: number + publisher_priority?: number // undefined when DEFAULT_PRIORITY bit is set } export namespace SubgroupHeader { @@ -19,7 +19,9 @@ export namespace SubgroupHeader { if (SubgroupType.hasExplicitSubgroupId(header.type) && header.subgroup_id !== undefined) { buf.putVarInt(header.subgroup_id) } - buf.putU8(header.publisher_priority) + if (!SubgroupType.hasDefaultPriority(header.type) && header.publisher_priority !== undefined) { + buf.putU8(header.publisher_priority) + } return buf.Uint8Array } } @@ -37,7 +39,9 @@ export namespace SubgroupObject { buf.putVarInt(obj.object_id) if (obj.extension_headers) { - buf.putBytes(KeyValuePairs.serialize(obj.extension_headers)) + const extHeadersBytes = KeyValuePairs.serialize(obj.extension_headers) + buf.putVarInt(extHeadersBytes.length) + buf.putBytes(extHeadersBytes) } buf.putVarInt(obj.object_payload?.length ?? 0) if (!obj.object_payload) { @@ -49,7 +53,16 @@ export namespace SubgroupObject { } } +// Draft-16: Subgroup header types use bitmask structure 0b00X1XXXX +// Valid ranges: 0x10..0x15, 0x18..0x1D, 0x30..0x35, 0x38..0x3D +// Bit layout: +// bit 0 (0x01) = EXTENSIONS +// bits 1-2 (0x06) = SUBGROUP_ID_MODE (00=zero, 01=first obj, 10=present, 11=RESERVED) +// bit 3 (0x08) = END_OF_GROUP +// bit 4 (0x10) = always set (stream type marker) +// bit 5 (0x20) = DEFAULT_PRIORITY (when set, priority field omitted) export enum SubgroupType { + // Base types (0x10..0x1D) Type0x10 = 0x10, Type0x11 = 0x11, Type0x12 = 0x12, @@ -62,9 +75,28 @@ export enum SubgroupType { Type0x1B = 0x1B, Type0x1C = 0x1C, Type0x1D = 0x1D, + // DEFAULT_PRIORITY types (0x30..0x3D) + Type0x30 = 0x30, + Type0x31 = 0x31, + Type0x32 = 0x32, + Type0x33 = 0x33, + Type0x34 = 0x34, + Type0x35 = 0x35, + Type0x38 = 0x38, + Type0x39 = 0x39, + Type0x3A = 0x3A, + Type0x3B = 0x3B, + Type0x3C = 0x3C, + Type0x3D = 0x3D, } export namespace SubgroupType { + // Bitmask constants + const EXTENSIONS_BIT = 0x01 + const SUBGROUP_ID_MASK = 0x06 + const END_OF_GROUP_BIT = 0x08 + const DEFAULT_PRIORITY_BIT = 0x20 + export function serialize(type: SubgroupType): Uint8Array { const w = new MutableBytesBuffer(new Uint8Array()) w.putVarInt(type) @@ -74,39 +106,27 @@ export namespace SubgroupType { return try_from(reader.getNumberVarInt()) } - // may throw if invalid value if provided + // may throw if invalid value is provided export function try_from(value: number | bigint): SubgroupType { const v = typeof value === "bigint" ? Number(value) : value - switch (v) { - case SubgroupType.Type0x10: - case SubgroupType.Type0x11: - case SubgroupType.Type0x12: - case SubgroupType.Type0x13: - case SubgroupType.Type0x14: - case SubgroupType.Type0x15: - case SubgroupType.Type0x18: - case SubgroupType.Type0x19: - case SubgroupType.Type0x1A: - case SubgroupType.Type0x1B: - case SubgroupType.Type0x1C: - case SubgroupType.Type0x1D: - return v as SubgroupType - default: - throw new Error(`invalid subgroup type: ${v}`) + // Must match form 0b00X1XXXX (bit 4 set) + if ((v & 0x10) === 0) { + throw new Error(`invalid subgroup type: ${v} (bit 4 not set)`) + } + // Must be in ranges 0x10..0x1F or 0x30..0x3F + if (v < 0x10 || (v > 0x1F && v < 0x30) || v > 0x3F) { + throw new Error(`invalid subgroup type: ${v} (out of range)`) } + // SUBGROUP_ID_MODE = 0b11 is reserved + if (((v & SUBGROUP_ID_MASK) >> 1) === 3) { + throw new Error(`invalid subgroup type: ${v} (reserved SUBGROUP_ID_MODE=0b11)`) + } + return v as SubgroupType } export function isSubgroupIdPresent(type: SubgroupType) { - switch (type) { - case SubgroupType.Type0x14: - case SubgroupType.Type0x15: - case SubgroupType.Type0x1C: - case SubgroupType.Type0x1D: - return true - default: - return false - } + return ((type & SUBGROUP_ID_MASK) >> 1) === 2 } export function hasExplicitSubgroupId(type: SubgroupType) { @@ -114,47 +134,23 @@ export namespace SubgroupType { } export function isSubgroupIdZero(type: SubgroupType) { - switch (type) { - case SubgroupType.Type0x10: - case SubgroupType.Type0x11: - case SubgroupType.Type0x18: - case SubgroupType.Type0x19: - return true - default: - return false - } + return ((type & SUBGROUP_ID_MASK) >> 1) === 0 } export function isSubgroupFirstObjectId(type: SubgroupType) { - return !(hasExplicitSubgroupId(type) || isSubgroupIdZero(type)) + return ((type & SUBGROUP_ID_MASK) >> 1) === 1 } export function isExtensionPresent(type: SubgroupType) { - switch (type) { - case SubgroupType.Type0x11: - case SubgroupType.Type0x13: - case SubgroupType.Type0x15: - case SubgroupType.Type0x19: - case SubgroupType.Type0x1B: - case SubgroupType.Type0x1D: - return true - default: - return false - } + return (type & EXTENSIONS_BIT) !== 0 } export function contains_end_of_group(type: SubgroupType) { - switch (type) { - case SubgroupType.Type0x18: - case SubgroupType.Type0x19: - case SubgroupType.Type0x1A: - case SubgroupType.Type0x1B: - case SubgroupType.Type0x1C: - case SubgroupType.Type0x1D: - return true - default: - return false - } + return (type & END_OF_GROUP_BIT) !== 0 + } + + export function hasDefaultPriority(type: SubgroupType) { + return (type & DEFAULT_PRIORITY_BIT) !== 0 } } @@ -188,7 +184,9 @@ export class SubgroupReader { let extHeaders: KeyValuePairs | undefined if (SubgroupType.isExtensionPresent(this.header.type)) { - extHeaders = await KeyValuePairs.deserialize_with_reader(this.stream) + const extHeadersBytesLength = await this.stream.getNumberVarInt() + const extHeadersData = await this.stream.read(extHeadersBytesLength) + extHeaders = KeyValuePairs.deserialize(new ImmutableBytesBuffer(extHeadersData)) } console.log("subgroup header", object_id, extHeaders, this.stream) From eb6b00748771fb14acf98deadc2d547acf25cce2 Mon Sep 17 00:00:00 2001 From: Manish Date: Wed, 11 Mar 2026 22:21:30 +0530 Subject: [PATCH 02/16] fix: server and client version negotiation --- lib/transport/client.ts | 9 +++---- lib/transport/publisher.ts | 39 ++++++++++++++--------------- lib/transport/subscriber.ts | 49 +++++++++++++++++++++++-------------- 3 files changed, 53 insertions(+), 44 deletions(-) diff --git a/lib/transport/client.ts b/lib/transport/client.ts index b377841..0440cbb 100644 --- a/lib/transport/client.ts +++ b/lib/transport/client.ts @@ -40,7 +40,6 @@ export class Client { const buffer = new ReadableWritableStreamBuffer(stream.readable, stream.writable) const msg: Control.ClientSetup = { - versions: [Control.Version.DRAFT_14], params: new Map(), } const serialized = Control.ClientSetup.serialize(msg) @@ -49,10 +48,10 @@ export class Client { // Receive the setup message. // TODO verify the SETUP response. const server = await this.readServerSetup(buffer) - - if (server.version != Control.Version.DRAFT_14) { - throw new Error(`unsupported server version: ${server.version}`) - } + // + // if (server.version != Control.Version.DRAFT_14) { + // throw new Error(`unsupported server version: ${server.version}`) + // } const control = new Stream.ControlStream(buffer) const objects = new Objects(quic) diff --git a/lib/transport/publisher.ts b/lib/transport/publisher.ts index 32a6c33..509d2cb 100644 --- a/lib/transport/publisher.ts +++ b/lib/transport/publisher.ts @@ -3,6 +3,7 @@ import { ControlStream } from "./stream" import { Queue, Watch } from "../common/async" import { Objects, TrackWriter, ObjectDatagramType } from "./objects" import { SubgroupType, SubgroupWriter } from "./subgroup" +import { Parameters, ParameterType } from "./base_data" export class Publisher { // Used to send control messages @@ -62,21 +63,21 @@ export class Publisher { case Control.ControlMessageType.Unsubscribe: this.recvUnsubscribe(message) break; - case Control.ControlMessageType.PublishNamespaceOk: - this.recvPublishNamespaceOk(message) + case Control.ControlMessageType.RequestOk: + this.recvRequestOk(message) break; - case Control.ControlMessageType.PublishNamespaceError: - this.recvPublishNamespaceError(message) + case Control.ControlMessageType.RequestError: + this.recvRequestError(message) break; default: throw new Error(`unknown control message`) // impossible } } - recvPublishNamespaceOk(msg: Control.PublishNamespaceOk) { + recvRequestOk(msg: Control.RequestOk) { const namespace = this.#waitingPublishNamespaceRequests.get(msg.id) if (!namespace) { - throw new Error(`publish namespace OK for unknown announce: ${msg.id}`) + throw new Error(`request OK for unknown request: ${msg.id}`) } const publishNamespaceSend = this.#publishedNamespaces.get(namespace) if (!publishNamespaceSend) { @@ -87,15 +88,15 @@ export class Publisher { console.log("published namespace:", namespace) } - recvPublishNamespaceError(msg: Control.PublishNamespaceError) { + recvRequestError(msg: Control.RequestError) { const namespace = this.#waitingPublishNamespaceRequests.get(msg.id) if (!namespace) { - throw new Error(`publish namespace error for unknown announce: ${msg.id}`) + throw new Error(`request error for unknown request: ${msg.id}`) } const publishNamespaceSend = this.#publishedNamespaces.get(namespace) if (!publishNamespaceSend) { // TODO debug this - console.warn(`publish namespace error for unknown announce: ${namespace}`) + console.warn(`request error for unknown namespace: ${namespace}`) return } @@ -114,10 +115,11 @@ export class Publisher { await this.#subscribeQueue.push(subscribe) } catch (e: any) { await this.#control.send({ - type: Control.ControlMessageType.SubscribeError, + type: Control.ControlMessageType.RequestError, message: { id: msg.id, code: 0n, + retry_interval: 0n, reason: e.message, } }) @@ -187,7 +189,7 @@ export class PublishNamespaceSend { onError(code: bigint, reason: string) { if (this.closed()) return - const err = new Error(`PUBLISH_NAMESPACE_ERROR (${code})` + reason ? `: ${reason}` : "") + const err = new Error(`REQUEST_ERROR (${code})${reason ? `: ${reason}` : ""}`) this.#state.update(err) } } @@ -197,8 +199,7 @@ export class SubscribeRecv { #objects: Objects #id: bigint #trackAlias: bigint // Publisher-specified in draft-14 - #subscriberPriority: number - groupOrder: Control.GroupOrder + params: Parameters; readonly namespace: string[] readonly track: string @@ -213,8 +214,7 @@ export class SubscribeRecv { this.#trackAlias = trackAlias this.namespace = msg.namespace this.track = msg.name - this.#subscriberPriority = msg.subscriber_priority - this.groupOrder = msg.group_order + this.params = msg.params } // Acknowledge the subscription as valid. @@ -230,11 +230,8 @@ export class SubscribeRecv { type: Control.ControlMessageType.SubscribeOk, message: { id: this.#id, - expires: 0n, - group_order: this.groupOrder, track_alias: this.#trackAlias, - content_exists: 0, - params: new Map(), + params: this.params, } }) } @@ -247,8 +244,8 @@ export class SubscribeRecv { if (!acked) { return this.#control.send({ - type: Control.ControlMessageType.SubscribeError, - message: { id: this.#id, code, reason } + type: Control.ControlMessageType.RequestError, + message: { id: this.#id, code, retry_interval: 0n, reason } }) } if (unsubscribe) { diff --git a/lib/transport/subscriber.ts b/lib/transport/subscriber.ts index ac1d8ab..055d196 100644 --- a/lib/transport/subscriber.ts +++ b/lib/transport/subscriber.ts @@ -5,6 +5,7 @@ import type { TrackReader } from "./objects" import { debug } from "./utils" import { ControlStream } from "./stream" import { SubgroupReader } from "./subgroup" +import { ParameterType } from "./base_data" export interface TrackInfo { track_alias: bigint @@ -50,8 +51,8 @@ export class Subscriber { case Control.ControlMessageType.SubscribeOk: this.recvSubscribeOk(message) break - case Control.ControlMessageType.SubscribeError: - await this.recvSubscribeError(message) + case Control.ControlMessageType.RequestError: + await this.recvRequestError(message) break case Control.ControlMessageType.PublishDone: await this.recvPublishDone(message) @@ -67,8 +68,8 @@ export class Subscriber { } await this.#control.send({ - type: Control.ControlMessageType.PublishNamespaceOk, - message: { id: msg.id } + type: Control.ControlMessageType.RequestOk, + message: { id: msg.id, parameters: new Map() } }) const publishNamespace = new PublishNamespaceRecv(this.#control, msg.namespace, msg.id) @@ -89,12 +90,17 @@ export class Subscriber { message: { id, namespace, + subscribe_options: Control.SubscribeOptions.BOTH, } } await this.#control.send(msg) } - async subscribe(namespace: string[], track: string) { + async subscribe(namespace: string[], track: string, opts?: { + forward?: number, + subscriber_priority?: number, + group_order?: Control.GroupOrder, + }) { const id = this.#control.nextRequestId() const subscribe = new SubscribeSend(this.#control, id, namespace, track) @@ -102,17 +108,24 @@ export class Subscriber { this.#trackToIDMap.set(track, id) + const params = new Map() + if (opts?.forward !== undefined) { + params.set(BigInt(ParameterType.FORWARD), BigInt(opts.forward)) + } + if (opts?.subscriber_priority !== undefined) { + params.set(BigInt(ParameterType.SUBSCRIBER_PRIORITY), BigInt(opts.subscriber_priority)) + } + if (opts?.group_order !== undefined) { + params.set(BigInt(ParameterType.GROUP_ORDER), BigInt(opts.group_order)) + } + const subscription_req: Control.MessageWithType = { type: Control.ControlMessageType.Subscribe, message: { id, namespace, name: track, - subscriber_priority: 127, // default to mid value, see: https://github.com/moq-wg/moq-transport/issues/504 - group_order: Control.GroupOrder.Publisher, - filter_type: Control.FilterType.NextGroupStart, - forward: 1, // always forward - params: new Map(), + params, } } @@ -160,10 +173,10 @@ export class Subscriber { subscribe.onOk(msg.track_alias) } - async recvSubscribeError(msg: Control.SubscribeError) { + async recvRequestError(msg: Control.RequestError) { const subscribe = this.#subscribe.get(msg.id) if (!subscribe) { - throw new Error(`subscribe error for unknown id: ${msg.id}`) + throw new Error(`request error for unknown id: ${msg.id}`) } await subscribe.onError(msg.code, msg.reason) @@ -226,8 +239,8 @@ export class PublishNamespaceRecv { // Send the control message. return this.#control.send({ - type: Control.ControlMessageType.PublishNamespaceOk, - message: { id: this.#id } + type: Control.ControlMessageType.RequestOk, + message: { id: this.#id, parameters: new Map() } }) } @@ -236,8 +249,8 @@ export class PublishNamespaceRecv { this.#state = "closed" return this.#control.send({ - type: Control.ControlMessageType.PublishNamespaceError, - message: { id: this.#id, code, reason } + type: Control.ControlMessageType.RequestError, + message: { id: this.#id, code, retry_interval: 0n, reason } }) } } @@ -274,7 +287,7 @@ export class SubscribeSend { this.#trackAlias = trackAlias } - // FIXME(itzmanish): implement correctly + // FIXME(itzmanish): implement correctly async onDone(code: bigint, streamCount: bigint, reason: string) { throw new Error(`TODO onDone`) } @@ -288,7 +301,7 @@ export class SubscribeSend { reason = `: ${reason}` } - const err = new Error(`SUBSCRIBE_ERROR (${code})${reason}`) + const err = new Error(`REQUEST_ERROR (${code})${reason}`) return await this.#data.abort(err) } From 4620a4aee6f7eedf6cb53e2914737a5790a2cea2 Mon Sep 17 00:00:00 2001 From: Manish Date: Wed, 11 Mar 2026 22:27:28 +0530 Subject: [PATCH 03/16] fix: npm run fmt --- lib/common/tsconfig.json | 6 +- lib/contribute/broadcast.ts | 14 +- lib/contribute/container.ts | 2 +- lib/contribute/track.ts | 2 +- lib/contribute/tsconfig.json | 11 +- lib/media/tsconfig.json | 10 +- lib/moq-publisher/index.ts | 2 +- lib/package.json | 2 +- lib/playback/index.ts | 2 +- lib/playback/worker/index.ts | 2 +- lib/playback/worker/video.ts | 2 +- lib/publish/index.ts | 2 +- lib/transport/base_data.ts | 22 +- lib/transport/buffer.ts | 1101 ++++++++--------- lib/transport/client.ts | 8 +- lib/transport/connection.ts | 4 +- lib/transport/control/client_setup.ts | 2 +- lib/transport/control/fetch.ts | 9 +- lib/transport/control/fetch_cancel.ts | 37 +- lib/transport/control/fetch_ok.ts | 2 +- lib/transport/control/go_away.ts | 36 +- lib/transport/control/index.ts | 112 +- lib/transport/control/max_request_id.ts | 35 +- lib/transport/control/namespace.ts | 1 - lib/transport/control/namespace_done.ts | 1 - lib/transport/control/publish.ts | 2 +- lib/transport/control/publish_done.ts | 3 +- lib/transport/control/publish_namespace.ts | 3 +- .../control/publish_namespace_cancel.ts | 3 +- lib/transport/control/publish_ok.ts | 4 +- lib/transport/control/request_error.ts | 6 +- lib/transport/control/request_ok.ts | 46 +- lib/transport/control/requests_blocked.ts | 1 - lib/transport/control/server_setup.ts | 2 +- lib/transport/control/setup_parameters.ts | 11 +- lib/transport/control/subscribe.ts | 4 +- lib/transport/control/subscribe_namespace.ts | 6 +- lib/transport/control/subscribe_ok.ts | 1 - lib/transport/control/subscribe_update.ts | 5 +- lib/transport/control/track_status.ts | 1 - lib/transport/control/unsubscribe.ts | 35 +- lib/transport/fetch.ts | 28 +- lib/transport/objects.ts | 27 +- lib/transport/publisher.ts | 34 +- lib/transport/stream.ts | 34 +- lib/transport/subgroup.ts | 369 +++--- lib/transport/subscriber.ts | 26 +- lib/transport/tsconfig.json | 6 +- lib/transport/utils.ts | 6 +- lib/tsconfig.json | 12 +- lib/video-moq/index.ts | 38 +- 51 files changed, 1071 insertions(+), 1069 deletions(-) diff --git a/lib/common/tsconfig.json b/lib/common/tsconfig.json index 47e2883..20e0199 100644 --- a/lib/common/tsconfig.json +++ b/lib/common/tsconfig.json @@ -3,7 +3,5 @@ "compilerOptions": { "composite": true }, - "include": [ - "." - ] -} \ No newline at end of file + "include": ["."] +} diff --git a/lib/contribute/broadcast.ts b/lib/contribute/broadcast.ts index 43f140c..887c8af 100644 --- a/lib/contribute/broadcast.ts +++ b/lib/contribute/broadcast.ts @@ -46,7 +46,7 @@ export class Broadcast { const settings = media.getSettings() if (media.kind === "audio") { - const audioContext = new AudioContext(); + const audioContext = new AudioContext() audioContext.createMediaStreamSource(new MediaStream([media])) const sampleRate = audioContext.sampleRate Object.assign(settings, { @@ -121,7 +121,7 @@ export class Broadcast { console.log("[Broadcast] #run loop started") await this.connection.publish_namespace(this.namespace) - for (; ;) { + for (;;) { const subscriber = await this.connection.subscribed() if (!subscriber) break @@ -164,7 +164,7 @@ export class Broadcast { const bytes = Catalog.encode(this.catalog) await subscriber.ack() - await sleep(500); + await sleep(500) const stream = await subscriber.subgroup({ group: 0, subgroup: 0 }) await stream.write({ object_id: 0, object_payload: bytes }) @@ -176,7 +176,7 @@ export class Broadcast { if (!track) throw new Error(`no track with name ${subscriber.track}`) await subscriber.ack() - await sleep(500); + await sleep(500) const init = await track.init() @@ -193,11 +193,11 @@ export class Broadcast { await subscriber.ack() // NOTE(itzmanish): hack to make sure subscribe ok reaches before the segement object - await sleep(500); + await sleep(500) const segments = track.segments().getReader() - for (; ;) { + for (;;) { const { value: segment, done } = await segments.read() if (done) break @@ -221,7 +221,7 @@ export class Broadcast { // Pipe the segment to the stream. const chunks = segment.chunks().getReader() - for (; ;) { + for (;;) { const { value, done } = await chunks.read() if (done) break diff --git a/lib/contribute/container.ts b/lib/contribute/container.ts index b82d4aa..056e59f 100644 --- a/lib/contribute/container.ts +++ b/lib/contribute/container.ts @@ -125,7 +125,7 @@ export class Container { // Moof and mdat atoms are written in pairs. // TODO remove the moof/mdat from the Box to reclaim memory once everything works - for (; ;) { + for (;;) { const moof = this.#mp4.moofs.shift() const mdat = this.#mp4.mdats.shift() diff --git a/lib/contribute/track.ts b/lib/contribute/track.ts index e184ea7..c5b9daf 100644 --- a/lib/contribute/track.ts +++ b/lib/contribute/track.ts @@ -141,7 +141,7 @@ export class Track { return new ReadableStream({ pull: async (controller) => { - for (; ;) { + for (;;) { let index = pos - this.#offset if (index < 0) index = 0 diff --git a/lib/contribute/tsconfig.json b/lib/contribute/tsconfig.json index e198b77..70899a7 100644 --- a/lib/contribute/tsconfig.json +++ b/lib/contribute/tsconfig.json @@ -1,13 +1,8 @@ { "extends": "../tsconfig.json", - "include": [ - "." - ], + "include": ["."], "compilerOptions": { - "types": [ - "dom-mediacapture-transform", - "dom-webcodecs" - ] + "types": ["dom-mediacapture-transform", "dom-webcodecs"] }, "references": [ { @@ -20,4 +15,4 @@ "path": "../media" } ] -} \ No newline at end of file +} diff --git a/lib/media/tsconfig.json b/lib/media/tsconfig.json index 1e2adde..2e4ee28 100644 --- a/lib/media/tsconfig.json +++ b/lib/media/tsconfig.json @@ -1,12 +1,8 @@ { "extends": "../tsconfig.json", - "include": [ - "." - ], + "include": ["."], "compilerOptions": { - "types": [ - "mp4box" - ] + "types": ["mp4box"] }, "references": [ { @@ -16,4 +12,4 @@ "path": "../common" } ] -} \ No newline at end of file +} diff --git a/lib/moq-publisher/index.ts b/lib/moq-publisher/index.ts index f976f1d..9cfa3c4 100644 --- a/lib/moq-publisher/index.ts +++ b/lib/moq-publisher/index.ts @@ -194,4 +194,4 @@ export class PublisherMoq extends HTMLElement { } customElements.define("publisher-moq", PublisherMoq) -export default PublisherMoq \ No newline at end of file +export default PublisherMoq diff --git a/lib/package.json b/lib/package.json index 4a5f81d..2f6b02e 100644 --- a/lib/package.json +++ b/lib/package.json @@ -83,4 +83,4 @@ "type": "git", "url": "https://github.com/englishm/moq-js" } -} \ No newline at end of file +} diff --git a/lib/playback/index.ts b/lib/playback/index.ts index d468b96..bd1cd12 100644 --- a/lib/playback/index.ts +++ b/lib/playback/index.ts @@ -154,7 +154,7 @@ export default class Player extends EventTarget { try { console.log("starting segment data loop") - for (; ;) { + for (;;) { console.log("waiting for segment data") const segment = await Promise.race([sub.data(), this.#running]) if (!segment) continue diff --git a/lib/playback/worker/index.ts b/lib/playback/worker/index.ts index 4a9d556..651ee49 100644 --- a/lib/playback/worker/index.ts +++ b/lib/playback/worker/index.ts @@ -87,7 +87,7 @@ class Worker { segments.releaseLock() // Read each chunk, decoding the MP4 frames and adding them to the queue. - for (; ;) { + for (;;) { const chunk = await reader.read() if (!chunk) { break diff --git a/lib/playback/worker/video.ts b/lib/playback/worker/video.ts index a8691d6..e841a51 100644 --- a/lib/playback/worker/video.ts +++ b/lib/playback/worker/video.ts @@ -57,7 +57,7 @@ export class Renderer { async #run() { const reader = this.#timeline.frames.pipeThrough(this.#queue).getReader() - for (; ;) { + for (;;) { const { value: frame, done } = await reader.read() if (this.#paused) continue if (done) break diff --git a/lib/publish/index.ts b/lib/publish/index.ts index ac6b6a2..08e1e21 100644 --- a/lib/publish/index.ts +++ b/lib/publish/index.ts @@ -51,4 +51,4 @@ export class PublisherApi { await this.connection.closed() } } -} \ No newline at end of file +} diff --git a/lib/transport/base_data.ts b/lib/transport/base_data.ts index 43ff993..c7cd0ed 100644 --- a/lib/transport/base_data.ts +++ b/lib/transport/base_data.ts @@ -8,7 +8,7 @@ export namespace Tuple { export function serialize(tuple: Tuple): Uint8Array { const buf = new MutableBytesBuffer(new Uint8Array()) buf.putVarInt(tuple.length) - tuple.forEach(field => { + tuple.forEach((field) => { const serialized = TupleField.serialize(field) buf.putBytes(serialized) }) @@ -64,7 +64,6 @@ export namespace Location { } } - // Draft-16: Key-Value-Pairs use delta-encoded types (Section 1.4.2) // Delta Type is delta from previous type (or 0 if first) // Type even => value is varint (no length prefix) @@ -72,7 +71,6 @@ export namespace Location { export type KeyValuePairs = Map export type Parameters = KeyValuePairs - export namespace KeyValuePairs { export function valueIsVarInt(key: bigint): boolean { return (key & 1n) === 0n @@ -179,7 +177,6 @@ export namespace KeyValuePairs { } } - export namespace Parameters { export function valueIsVarInt(key: bigint): boolean { return KeyValuePairs.valueIsVarInt(key) @@ -206,7 +203,6 @@ export namespace Parameters { } } - // Draft-16: Reason Phrase structure (Section 1.4.3) // Max length is 1024 bytes export const REASON_PHRASE_MAX_LENGTH = 1024 @@ -244,7 +240,6 @@ export namespace ReasonPhrase { } } - // Draft-16: Parameter Type constants (Section 13.2, Table 8) export enum ParameterType { DELIVERY_TIMEOUT = 0x02, @@ -258,20 +253,18 @@ export enum ParameterType { NEW_GROUP_REQUEST = 0x32, } - // Draft-16: Extension Header Type constants (Section 13.3, Table 9) export enum ExtensionHeaderType { DELIVERY_TIMEOUT = 0x02, MAX_CACHE_DURATION = 0x04, - IMMUTABLE_EXTENSIONS = 0x0B, - DEFAULT_PUBLISHER_PRIORITY = 0x0E, + IMMUTABLE_EXTENSIONS = 0x0b, + DEFAULT_PUBLISHER_PRIORITY = 0x0e, DEFAULT_PUBLISHER_GROUP_ORDER = 0x22, DYNAMIC_GROUPS = 0x30, - PRIOR_GROUP_ID_GAP = 0x3C, - PRIOR_OBJECT_ID_GAP = 0x3E, + PRIOR_GROUP_ID_GAP = 0x3c, + PRIOR_OBJECT_ID_GAP = 0x3e, } - // Draft-16: Session Termination Error Codes (Section 13.4.1, Table 10) export enum SessionTerminationError { NO_ERROR = 0x0, @@ -294,10 +287,9 @@ export enum SessionTerminationError { UNKNOWN_AUTH_TOKEN_ALIAS = 0x17, EXPIRED_AUTH_TOKEN = 0x18, INVALID_AUTHORITY = 0x19, - MALFORMED_AUTHORITY = 0x1A, + MALFORMED_AUTHORITY = 0x1a, } - // Draft-16: REQUEST_ERROR Codes (Section 13.4.2, Table 11) export enum RequestErrorCode { INTERNAL_ERROR = 0x0, @@ -315,7 +307,6 @@ export enum RequestErrorCode { INVALID_JOINING_REQUEST_ID = 0x32, } - // Draft-16: PUBLISH_DONE Codes (Section 13.4.3, Table 12) export enum PublishDoneCode { INTERNAL_ERROR = 0x0, @@ -329,7 +320,6 @@ export enum PublishDoneCode { MALFORMED_TRACK = 0x12, } - // Draft-16: Data Stream Reset Error Codes (Section 13.4.4, Table 13) export enum DataStreamResetCode { INTERNAL_ERROR = 0x0, diff --git a/lib/transport/buffer.ts b/lib/transport/buffer.ts index fa57b72..b2046e0 100644 --- a/lib/transport/buffer.ts +++ b/lib/transport/buffer.ts @@ -5,577 +5,572 @@ const MAX_U53 = Number.MAX_SAFE_INTEGER const MAX_U62: bigint = 2n ** 62n - 1n // 0-4611686018427387903 (62 bits) export interface Reader { - byteLength: number - waitForBytes(len: number): Promise - read(len: number): Promise - done(): Promise - close(): Promise - release(): [Uint8Array, ReadableStream] | [Uint8Array, WritableStream] - - getU8(): Promise - getU16(): Promise - getNumberVarInt(): Promise - getVarInt(): Promise - getVarBytes(): Promise - getUtf8String(): Promise + byteLength: number + waitForBytes(len: number): Promise + read(len: number): Promise + done(): Promise + close(): Promise + release(): [Uint8Array, ReadableStream] | [Uint8Array, WritableStream] + + getU8(): Promise + getU16(): Promise + getNumberVarInt(): Promise + getVarInt(): Promise + getVarBytes(): Promise + getUtf8String(): Promise } export interface Writer { - write(data: Uint8Array): Promise - flush(): Promise - clear(): void - close(): Promise - release(): [Uint8Array, WritableStream] | [Uint8Array, ReadableStream] - - putU8(v: number): void - putU16(v: number): void - putVarInt(v: number | bigint): void - putUtf8String(v: string): void + write(data: Uint8Array): Promise + flush(): Promise + clear(): void + close(): Promise + release(): [Uint8Array, WritableStream] | [Uint8Array, ReadableStream] + + putU8(v: number): void + putU16(v: number): void + putVarInt(v: number | bigint): void + putUtf8String(v: string): void } export class ImmutableBytesBuffer { - protected buffer: Uint8Array - protected offset = 0 - view: DataView - - constructor(buffer: Uint8Array) { - this.buffer = buffer - this.view = new DataView(buffer.buffer, buffer.byteOffset, buffer.length) - } - - get length(): number { - return this.buffer.length - } - - get remaining(): number { - return this.length - this.offset - } - - get Uint8Array(): Uint8Array { - return this.buffer.slice(0, this.offset) - } - - get firstByteValue(): number { - return this.buffer[this.offset] - } - - getRemainingBuffer(): Uint8Array { - return this.buffer.subarray(this.offset) - } - - getU8(): number { - if (this.remaining < 1) throw new Error("not enough bytes") - const val = this.view.getUint8(this.offset) - this.offset += 1 - return val - } - - getU16(): number { - if (this.remaining < 2) throw new Error("not enough bytes") - const val = this.view.getUint16(this.offset) - this.offset += 2 - return val - } - - getU32(): number { - if (this.remaining < 4) throw new Error("not enough bytes") - const val = this.view.getUint32(this.offset) - this.offset += 4 - return val - } - - getU64(): bigint { - if (this.remaining < 8) throw new Error("not enough bytes") - const val = this.view.getBigUint64(this.offset) - this.offset += 8 - return val - } - - getVarInt(): bigint { - if (this.remaining < 1) throw new Error("not enough bytes") - - // Read first byte to determine length - const first = this.view.getUint8(this.offset) - const prefix = (first & 0b11000000) >> 6 // Top 2 bits indicate length - - if (prefix === 0) { - // 1 byte: 00xxxxxx - this.offset += 1 - return BigInt(first & 0x3f) - } else if (prefix === 1) { - // 2 bytes: 01xxxxxx xxxxxxxx - if (this.remaining < 2) throw new Error("not enough bytes") - const val = this.view.getUint16(this.offset) - this.offset += 2 - return BigInt(val & 0x3fff) - } else if (prefix === 2) { - // 4 bytes: 10xxxxxx xxxxxxxx xxxxxxxx xxxxxxxx - if (this.remaining < 4) throw new Error("not enough bytes") - const val = this.view.getUint32(this.offset) - this.offset += 4 - return BigInt(val & 0x3fffffff) - } else { - // 8 bytes: 11xxxxxx xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx - if (this.remaining < 8) throw new Error("not enough bytes") - const val = this.view.getBigUint64(this.offset) - this.offset += 8 - return val & 0x3fffffffffffffffn - } - } - - getNumberVarInt(): number { - const val = this.getVarInt() - if (val > Number.MAX_SAFE_INTEGER) throw new Error("varint too large") - return Number(val) - } - - getBytes(len: number): Uint8Array { - if (this.remaining < len) throw new Error("not enough bytes") - const val = this.buffer.slice(this.offset, this.offset + len) - this.offset += len - return val - } - - getVarBytes(): Uint8Array { - const len = this.getNumberVarInt() - return this.getBytes(len) - } - - getUtf8String(): string { - const len = this.getNumberVarInt() - const val = this.getBytes(len) - return new TextDecoder().decode(val) - } + protected buffer: Uint8Array + protected offset = 0 + view: DataView + + constructor(buffer: Uint8Array) { + this.buffer = buffer + this.view = new DataView(buffer.buffer, buffer.byteOffset, buffer.length) + } + + get length(): number { + return this.buffer.length + } + + get remaining(): number { + return this.length - this.offset + } + + get Uint8Array(): Uint8Array { + return this.buffer.slice(0, this.offset) + } + + get firstByteValue(): number { + return this.buffer[this.offset] + } + + getRemainingBuffer(): Uint8Array { + return this.buffer.subarray(this.offset) + } + + getU8(): number { + if (this.remaining < 1) throw new Error("not enough bytes") + const val = this.view.getUint8(this.offset) + this.offset += 1 + return val + } + + getU16(): number { + if (this.remaining < 2) throw new Error("not enough bytes") + const val = this.view.getUint16(this.offset) + this.offset += 2 + return val + } + + getU32(): number { + if (this.remaining < 4) throw new Error("not enough bytes") + const val = this.view.getUint32(this.offset) + this.offset += 4 + return val + } + + getU64(): bigint { + if (this.remaining < 8) throw new Error("not enough bytes") + const val = this.view.getBigUint64(this.offset) + this.offset += 8 + return val + } + + getVarInt(): bigint { + if (this.remaining < 1) throw new Error("not enough bytes") + + // Read first byte to determine length + const first = this.view.getUint8(this.offset) + const prefix = (first & 0b11000000) >> 6 // Top 2 bits indicate length + + if (prefix === 0) { + // 1 byte: 00xxxxxx + this.offset += 1 + return BigInt(first & 0x3f) + } else if (prefix === 1) { + // 2 bytes: 01xxxxxx xxxxxxxx + if (this.remaining < 2) throw new Error("not enough bytes") + const val = this.view.getUint16(this.offset) + this.offset += 2 + return BigInt(val & 0x3fff) + } else if (prefix === 2) { + // 4 bytes: 10xxxxxx xxxxxxxx xxxxxxxx xxxxxxxx + if (this.remaining < 4) throw new Error("not enough bytes") + const val = this.view.getUint32(this.offset) + this.offset += 4 + return BigInt(val & 0x3fffffff) + } else { + // 8 bytes: 11xxxxxx xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx + if (this.remaining < 8) throw new Error("not enough bytes") + const val = this.view.getBigUint64(this.offset) + this.offset += 8 + return val & 0x3fffffffffffffffn + } + } + + getNumberVarInt(): number { + const val = this.getVarInt() + if (val > Number.MAX_SAFE_INTEGER) throw new Error("varint too large") + return Number(val) + } + + getBytes(len: number): Uint8Array { + if (this.remaining < len) throw new Error("not enough bytes") + const val = this.buffer.slice(this.offset, this.offset + len) + this.offset += len + return val + } + + getVarBytes(): Uint8Array { + const len = this.getNumberVarInt() + return this.getBytes(len) + } + + getUtf8String(): string { + const len = this.getNumberVarInt() + const val = this.getBytes(len) + return new TextDecoder().decode(val) + } } export class MutableBytesBuffer { - offset = 0 - buffer: Uint8Array - view: DataView - constructor(buffer: Uint8Array) { - this.buffer = buffer - this.view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength) - } - - get length(): number { - return this.offset - } - - get byteLength(): number { - // NOTE(itzmanish): since we are working with u8 buffer, byteLength is same as length - return this.length - } - - get Uint8Array(): Uint8Array { - return this.buffer.subarray(0, this.offset) - } - - enoughSpaceAvailable(len: number): void { - const required = this.offset + len - if (required < this.buffer.length) { - return - } - - const newBuffer = new Uint8Array(nextPow2(required)) - newBuffer.set(this.buffer.subarray(0, this.offset)) - this.buffer = newBuffer - this.view = new DataView(this.buffer.buffer, this.buffer.byteOffset, this.buffer.byteLength) - } - - putU8(v: number) { - this.enoughSpaceAvailable(1) - this.view.setUint8(this.offset, v) - this.offset += 1 - } - - putU16(v: number) { - this.enoughSpaceAvailable(2) - this.view.setUint16(this.offset, v) - this.offset += 2 - } - - putU32(v: number) { - this.enoughSpaceAvailable(4) - this.view.setUint32(this.offset, v) - this.offset += 4 - } - - putU64(v: bigint) { - this.enoughSpaceAvailable(8) - this.view.setBigUint64(this.offset, v) - this.offset += 8 - } - - putVarInt(v: number | bigint) { - const value = typeof v === "number" ? BigInt(v) : v - if (value < 0) throw new Error("underflow, value is negative: " + v) - - let length: number - let prefix: number - - if (value < MAX_U6) { - // 1 byte: 00xxxxxx - length = 1 - prefix = 0x00 - } else if (value < MAX_U14) { - // 2 bytes: 01xxxxxx xxxxxxxx - length = 2 - prefix = 0x40 - } else if (value < MAX_U30) { - // 4 bytes: 10xxxxxx xxxxxxxx xxxxxxxx xxxxxxxx - length = 4 - prefix = 0x80 - } else if (value <= MAX_U62) { - // 8 bytes: 11xxxxxx xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx - length = 8 - prefix = 0xc0 - } else { - throw new Error("overflow, value larger than 62-bits: " + v) - } - - this.enoughSpaceAvailable(length) - - // Write first byte with prefix - const shift = BigInt((length - 1) * 8) - this.putU8(Number((value >> shift) | BigInt(prefix))) - - // Write remaining bytes - for (let i = length - 2; i >= 0; i--) { - this.putU8(Number((value >> BigInt(i * 8)) & 0xffn)) - } - } - - putBytes(v: Uint8Array) { - this.enoughSpaceAvailable(v.length) - this.buffer.set(v, this.offset) - this.offset += v.length - } - - putUtf8String(v: string) { - const bytes = new TextEncoder().encode(v) - this.putLengthPrefixedByteArray(bytes.length, bytes) - } - - putLengthPrefixedByteArray(len: number, v: Uint8Array) { - this.putVarInt(len) - this.putBytes(v) - } - + offset = 0 + buffer: Uint8Array + view: DataView + constructor(buffer: Uint8Array) { + this.buffer = buffer + this.view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength) + } + + get length(): number { + return this.offset + } + + get byteLength(): number { + // NOTE(itzmanish): since we are working with u8 buffer, byteLength is same as length + return this.length + } + + get Uint8Array(): Uint8Array { + return this.buffer.subarray(0, this.offset) + } + + enoughSpaceAvailable(len: number): void { + const required = this.offset + len + if (required < this.buffer.length) { + return + } + + const newBuffer = new Uint8Array(nextPow2(required)) + newBuffer.set(this.buffer.subarray(0, this.offset)) + this.buffer = newBuffer + this.view = new DataView(this.buffer.buffer, this.buffer.byteOffset, this.buffer.byteLength) + } + + putU8(v: number) { + this.enoughSpaceAvailable(1) + this.view.setUint8(this.offset, v) + this.offset += 1 + } + + putU16(v: number) { + this.enoughSpaceAvailable(2) + this.view.setUint16(this.offset, v) + this.offset += 2 + } + + putU32(v: number) { + this.enoughSpaceAvailable(4) + this.view.setUint32(this.offset, v) + this.offset += 4 + } + + putU64(v: bigint) { + this.enoughSpaceAvailable(8) + this.view.setBigUint64(this.offset, v) + this.offset += 8 + } + + putVarInt(v: number | bigint) { + const value = typeof v === "number" ? BigInt(v) : v + if (value < 0) throw new Error("underflow, value is negative: " + v) + + let length: number + let prefix: number + + if (value < MAX_U6) { + // 1 byte: 00xxxxxx + length = 1 + prefix = 0x00 + } else if (value < MAX_U14) { + // 2 bytes: 01xxxxxx xxxxxxxx + length = 2 + prefix = 0x40 + } else if (value < MAX_U30) { + // 4 bytes: 10xxxxxx xxxxxxxx xxxxxxxx xxxxxxxx + length = 4 + prefix = 0x80 + } else if (value <= MAX_U62) { + // 8 bytes: 11xxxxxx xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx + length = 8 + prefix = 0xc0 + } else { + throw new Error("overflow, value larger than 62-bits: " + v) + } + + this.enoughSpaceAvailable(length) + + // Write first byte with prefix + const shift = BigInt((length - 1) * 8) + this.putU8(Number((value >> shift) | BigInt(prefix))) + + // Write remaining bytes + for (let i = length - 2; i >= 0; i--) { + this.putU8(Number((value >> BigInt(i * 8)) & 0xffn)) + } + } + + putBytes(v: Uint8Array) { + this.enoughSpaceAvailable(v.length) + this.buffer.set(v, this.offset) + this.offset += v.length + } + + putUtf8String(v: string) { + const bytes = new TextEncoder().encode(v) + this.putLengthPrefixedByteArray(bytes.length, bytes) + } + + putLengthPrefixedByteArray(len: number, v: Uint8Array) { + this.putVarInt(len) + this.putBytes(v) + } } export class ReadableStreamBuffer implements Reader { - protected buffer: Uint8Array - protected reader: ReadableStreamDefaultReader - protected readableStream: ReadableStream - - constructor(reader: ReadableStream, buffer?: Uint8Array) { - this.buffer = buffer ?? new Uint8Array() - this.reader = reader.getReader() - this.readableStream = reader - } - - waitForBytes(len: number): Promise { - if (this.buffer.byteLength >= len) { - return Promise.resolve() - } - return this.#fillTo(len) - } - - get byteLength(): number { - return this.buffer.byteLength - } - - // Adds more data to the buffer, returning true if more data was added. - async #fill(): Promise { - const result = await this.reader.read() - if (result.done) { - return false - } - const buffer = new Uint8Array(result.value) - - if (this.buffer.byteLength == 0) { - this.buffer = buffer - } else { - const temp = new Uint8Array(this.buffer.byteLength + buffer.byteLength) - temp.set(this.buffer) - temp.set(buffer, this.buffer.byteLength) - this.buffer = temp - } - - return true - } - - // Add more data to the buffer until it's at least size bytes. - async #fillTo(size: number) { - while (this.buffer.byteLength < size) { - if (!(await this.#fill())) { - throw new Error("unexpected end of stream") - } - } - } - - // Consumes the first size bytes of the buffer. - #slice(size: number): Uint8Array { - const result = new Uint8Array(this.buffer.buffer, this.buffer.byteOffset, size) - this.buffer = new Uint8Array(this.buffer.buffer, this.buffer.byteOffset + size) - - return result - } - - async read(size: number): Promise { - if (size == 0) return new Uint8Array() - - await this.#fillTo(size) - return this.#slice(size) - } - - async readAll(): Promise { - // eslint-disable-next-line no-empty - while (await this.#fill()) { } - return this.#slice(this.buffer.byteLength) - } - - async getUtf8String(maxLength?: number): Promise { - const length = await this.getNumberVarInt() - if (maxLength !== undefined && length > maxLength) { - throw new Error(`string length ${length} exceeds max length ${maxLength}`) - } - - const buffer = await this.read(length) - return new TextDecoder().decode(buffer) - } - - async getU8(): Promise { - await this.#fillTo(1) - return this.#slice(1)[0] - } - - async getU16(): Promise { - await this.#fillTo(2) - const slice = this.#slice(2) - const view = new DataView(slice.buffer, slice.byteOffset, slice.byteLength) - - return view.getInt16(0) - } - - // Returns a Number using 53-bits, the max Javascript can use for integer math - async getNumberVarInt(): Promise { - const v = await this.getVarInt() - if (v > MAX_U53) { - throw new Error("value larger than 53-bits; use v62 instead") - } - - return Number(v) - } - - async getVarInt(): Promise { - await this.#fillTo(1) - const size = (this.buffer[0] & 0xc0) >> 6 - - if (size == 0) { - const first = this.#slice(1)[0] - return BigInt(first) & 0x3fn - } else if (size == 1) { - await this.#fillTo(2) - const slice = this.#slice(2) - const view = new DataView(slice.buffer, slice.byteOffset, slice.byteLength) - - return BigInt(view.getInt16(0)) & 0x3fffn - } else if (size == 2) { - await this.#fillTo(4) - const slice = this.#slice(4) - const view = new DataView(slice.buffer, slice.byteOffset, slice.byteLength) - - return BigInt(view.getUint32(0)) & 0x3fffffffn - } else if (size == 3) { - await this.#fillTo(8) - const slice = this.#slice(8) - const view = new DataView(slice.buffer, slice.byteOffset, slice.byteLength) - - return view.getBigUint64(0) & 0x3fffffffffffffffn - } else { - throw new Error("impossible") - } - } - - async getVarBytes(): Promise { - const length = await this.getNumberVarInt() - return this.read(length) - } - - - async done(): Promise { - if (this.buffer.byteLength > 0) return false - return !(await this.#fill()) - } - - async close() { - this.reader.releaseLock() - await this.readableStream.cancel() - } - - release(): [Uint8Array, ReadableStream] { - this.reader.releaseLock() - return [this.buffer, this.readableStream] - } - + protected buffer: Uint8Array + protected reader: ReadableStreamDefaultReader + protected readableStream: ReadableStream + + constructor(reader: ReadableStream, buffer?: Uint8Array) { + this.buffer = buffer ?? new Uint8Array() + this.reader = reader.getReader() + this.readableStream = reader + } + + waitForBytes(len: number): Promise { + if (this.buffer.byteLength >= len) { + return Promise.resolve() + } + return this.#fillTo(len) + } + + get byteLength(): number { + return this.buffer.byteLength + } + + // Adds more data to the buffer, returning true if more data was added. + async #fill(): Promise { + const result = await this.reader.read() + if (result.done) { + return false + } + const buffer = new Uint8Array(result.value) + + if (this.buffer.byteLength == 0) { + this.buffer = buffer + } else { + const temp = new Uint8Array(this.buffer.byteLength + buffer.byteLength) + temp.set(this.buffer) + temp.set(buffer, this.buffer.byteLength) + this.buffer = temp + } + + return true + } + + // Add more data to the buffer until it's at least size bytes. + async #fillTo(size: number) { + while (this.buffer.byteLength < size) { + if (!(await this.#fill())) { + throw new Error("unexpected end of stream") + } + } + } + + // Consumes the first size bytes of the buffer. + #slice(size: number): Uint8Array { + const result = new Uint8Array(this.buffer.buffer, this.buffer.byteOffset, size) + this.buffer = new Uint8Array(this.buffer.buffer, this.buffer.byteOffset + size) + + return result + } + + async read(size: number): Promise { + if (size == 0) return new Uint8Array() + + await this.#fillTo(size) + return this.#slice(size) + } + + async readAll(): Promise { + // eslint-disable-next-line no-empty + while (await this.#fill()) {} + return this.#slice(this.buffer.byteLength) + } + + async getUtf8String(maxLength?: number): Promise { + const length = await this.getNumberVarInt() + if (maxLength !== undefined && length > maxLength) { + throw new Error(`string length ${length} exceeds max length ${maxLength}`) + } + + const buffer = await this.read(length) + return new TextDecoder().decode(buffer) + } + + async getU8(): Promise { + await this.#fillTo(1) + return this.#slice(1)[0] + } + + async getU16(): Promise { + await this.#fillTo(2) + const slice = this.#slice(2) + const view = new DataView(slice.buffer, slice.byteOffset, slice.byteLength) + + return view.getInt16(0) + } + + // Returns a Number using 53-bits, the max Javascript can use for integer math + async getNumberVarInt(): Promise { + const v = await this.getVarInt() + if (v > MAX_U53) { + throw new Error("value larger than 53-bits; use v62 instead") + } + + return Number(v) + } + + async getVarInt(): Promise { + await this.#fillTo(1) + const size = (this.buffer[0] & 0xc0) >> 6 + + if (size == 0) { + const first = this.#slice(1)[0] + return BigInt(first) & 0x3fn + } else if (size == 1) { + await this.#fillTo(2) + const slice = this.#slice(2) + const view = new DataView(slice.buffer, slice.byteOffset, slice.byteLength) + + return BigInt(view.getInt16(0)) & 0x3fffn + } else if (size == 2) { + await this.#fillTo(4) + const slice = this.#slice(4) + const view = new DataView(slice.buffer, slice.byteOffset, slice.byteLength) + + return BigInt(view.getUint32(0)) & 0x3fffffffn + } else if (size == 3) { + await this.#fillTo(8) + const slice = this.#slice(8) + const view = new DataView(slice.buffer, slice.byteOffset, slice.byteLength) + + return view.getBigUint64(0) & 0x3fffffffffffffffn + } else { + throw new Error("impossible") + } + } + + async getVarBytes(): Promise { + const length = await this.getNumberVarInt() + return this.read(length) + } + + async done(): Promise { + if (this.buffer.byteLength > 0) return false + return !(await this.#fill()) + } + + async close() { + this.reader.releaseLock() + await this.readableStream.cancel() + } + + release(): [Uint8Array, ReadableStream] { + this.reader.releaseLock() + return [this.buffer, this.readableStream] + } } export class WritableStreamBuffer implements Writer { - protected writer: WritableStreamDefaultWriter - protected writableStream: WritableStream - protected buffer: MutableBytesBuffer - constructor(writer: WritableStream) { - this.writer = writer.getWriter() - this.writableStream = writer - this.buffer = new MutableBytesBuffer(new Uint8Array()) - } - - async write(data: Uint8Array) { - return this.writer.write(data) - } - - async close() { - this.writer.releaseLock() - return this.writableStream.close() - } - - async flush() { - await this.write(this.buffer.Uint8Array) - this.clear() - } - - clear() { - this.buffer = new MutableBytesBuffer(new Uint8Array()) - } - - putU8(v: number) { - this.buffer.putU8(v) - } - - putU16(v: number) { - this.buffer.putU16(v) - } - - putVarInt(v: number | bigint) { - this.buffer.putVarInt(v) - } - - putUtf8String(v: string) { - this.buffer.putUtf8String(v) - } - - release(): [Uint8Array, WritableStream] { - this.writer.releaseLock() - return [this.buffer.Uint8Array, this.writableStream] - } + protected writer: WritableStreamDefaultWriter + protected writableStream: WritableStream + protected buffer: MutableBytesBuffer + constructor(writer: WritableStream) { + this.writer = writer.getWriter() + this.writableStream = writer + this.buffer = new MutableBytesBuffer(new Uint8Array()) + } + + async write(data: Uint8Array) { + return this.writer.write(data) + } + + async close() { + this.writer.releaseLock() + return this.writableStream.close() + } + + async flush() { + await this.write(this.buffer.Uint8Array) + this.clear() + } + + clear() { + this.buffer = new MutableBytesBuffer(new Uint8Array()) + } + + putU8(v: number) { + this.buffer.putU8(v) + } + + putU16(v: number) { + this.buffer.putU16(v) + } + + putVarInt(v: number | bigint) { + this.buffer.putVarInt(v) + } + + putUtf8String(v: string) { + this.buffer.putUtf8String(v) + } + + release(): [Uint8Array, WritableStream] { + this.writer.releaseLock() + return [this.buffer.Uint8Array, this.writableStream] + } } export class ReadableWritableStreamBuffer implements Reader, Writer { - private readStreamBuffer: ReadableStreamBuffer - private writeStreamBuffer: WritableStreamBuffer - - constructor(reader: ReadableStream, writer: WritableStream) { - this.readStreamBuffer = new ReadableStreamBuffer(reader) - this.writeStreamBuffer = new WritableStreamBuffer(writer) - } - - waitForBytes(len: number): Promise { - return this.readStreamBuffer.waitForBytes(len) - } - - get byteLength(): number { - return this.readStreamBuffer.byteLength - } - - async read(len: number): Promise { - return this.readStreamBuffer.read(len) - } - - - async getU8(): Promise { - return this.readStreamBuffer.getU8() - } - - async getU16(): Promise { - return this.readStreamBuffer.getU16() - } - - async getNumberVarInt(): Promise { - return this.readStreamBuffer.getNumberVarInt() - } - - async getVarInt(): Promise { - return this.readStreamBuffer.getVarInt() - } - - async getUtf8String(): Promise { - return this.readStreamBuffer.getUtf8String() - } - - async getVarBytes(): Promise { - return this.readStreamBuffer.getVarBytes() - } - - async done(): Promise { - return this.readStreamBuffer.done() - } - - putU16(v: number): void { - this.writeStreamBuffer.putU16(v) - } - putVarInt(v: number | bigint): void { - this.writeStreamBuffer.putVarInt(v) - } - putUtf8String(v: string): void { - this.writeStreamBuffer.putUtf8String(v) - } - putU8(v: number) { - this.writeStreamBuffer.putU8(v) - } - - async write(data: Uint8Array) { - return this.writeStreamBuffer.write(data) - } - - async close() { - this.readStreamBuffer.close() - this.writeStreamBuffer.close() - } - async flush(): Promise { - return this.writeStreamBuffer.flush() - } - - clear(): void { - this.writeStreamBuffer.clear() - } - - - release(): [Uint8Array, ReadableStream] | [Uint8Array, WritableStream] { - throw new Error("use release all instead of release") - } - - releaseAll(): [Uint8Array, Uint8Array, ReadableStream, WritableStream] { - const [readBuffer, readStream] = this.readStreamBuffer.release() - const [writeBuffer, writeStream] = this.writeStreamBuffer.release() - return [readBuffer, writeBuffer, readStream, writeStream] - } + private readStreamBuffer: ReadableStreamBuffer + private writeStreamBuffer: WritableStreamBuffer + + constructor(reader: ReadableStream, writer: WritableStream) { + this.readStreamBuffer = new ReadableStreamBuffer(reader) + this.writeStreamBuffer = new WritableStreamBuffer(writer) + } + + waitForBytes(len: number): Promise { + return this.readStreamBuffer.waitForBytes(len) + } + + get byteLength(): number { + return this.readStreamBuffer.byteLength + } + + async read(len: number): Promise { + return this.readStreamBuffer.read(len) + } + + async getU8(): Promise { + return this.readStreamBuffer.getU8() + } + + async getU16(): Promise { + return this.readStreamBuffer.getU16() + } + + async getNumberVarInt(): Promise { + return this.readStreamBuffer.getNumberVarInt() + } + + async getVarInt(): Promise { + return this.readStreamBuffer.getVarInt() + } + + async getUtf8String(): Promise { + return this.readStreamBuffer.getUtf8String() + } + + async getVarBytes(): Promise { + return this.readStreamBuffer.getVarBytes() + } + + async done(): Promise { + return this.readStreamBuffer.done() + } + + putU16(v: number): void { + this.writeStreamBuffer.putU16(v) + } + putVarInt(v: number | bigint): void { + this.writeStreamBuffer.putVarInt(v) + } + putUtf8String(v: string): void { + this.writeStreamBuffer.putUtf8String(v) + } + putU8(v: number) { + this.writeStreamBuffer.putU8(v) + } + + async write(data: Uint8Array) { + return this.writeStreamBuffer.write(data) + } + + async close() { + this.readStreamBuffer.close() + this.writeStreamBuffer.close() + } + async flush(): Promise { + return this.writeStreamBuffer.flush() + } + + clear(): void { + this.writeStreamBuffer.clear() + } + + release(): [Uint8Array, ReadableStream] | [Uint8Array, WritableStream] { + throw new Error("use release all instead of release") + } + + releaseAll(): [Uint8Array, Uint8Array, ReadableStream, WritableStream] { + const [readBuffer, readStream] = this.readStreamBuffer.release() + const [writeBuffer, writeStream] = this.writeStreamBuffer.release() + return [readBuffer, writeBuffer, readStream, writeStream] + } } function nextPow2(x: number): number { - // Handle edge cases - if (x <= 1) return 1; - - // Decrement to handle exact powers of 2 - x--; - - // Fill all bits below the highest set bit - x = x | (x >> 1); - x = x | (x >> 2); - x = x | (x >> 4); - x = x | (x >> 8); - x = x | (x >> 16); - - // Increment to get next power of 2 - return x + 1; -} \ No newline at end of file + // Handle edge cases + if (x <= 1) return 1 + + // Decrement to handle exact powers of 2 + x-- + + // Fill all bits below the highest set bit + x = x | (x >> 1) + x = x | (x >> 2) + x = x | (x >> 4) + x = x | (x >> 8) + x = x | (x >> 16) + + // Increment to get next power of 2 + return x + 1 +} diff --git a/lib/transport/client.ts b/lib/transport/client.ts index 0440cbb..115edd0 100644 --- a/lib/transport/client.ts +++ b/lib/transport/client.ts @@ -1,5 +1,5 @@ import * as Control from "./control" -import * as Stream from './stream' +import * as Stream from "./stream" import { Objects } from "./objects" import { Connection } from "./connection" import { ClientSetup, ControlMessageType, ServerSetup } from "./control" @@ -79,7 +79,8 @@ export class Client { async readServerSetup(buffer: ReadableWritableStreamBuffer): Promise { const type: ControlMessageType = await buffer.getNumberVarInt() - if (type !== ControlMessageType.ServerSetup) throw new Error(`server SETUP type must be ${ControlMessageType.ServerSetup}, got ${type}`) + if (type !== ControlMessageType.ServerSetup) + throw new Error(`server SETUP type must be ${ControlMessageType.ServerSetup}, got ${type}`) const advertisedLength = await buffer.getU16() const bufferLen = buffer.byteLength @@ -96,7 +97,8 @@ export class Client { async readClientSetup(buffer: ReadableWritableStreamBuffer): Promise { const type: ControlMessageType = await buffer.getNumberVarInt() - if (type !== ControlMessageType.ClientSetup) throw new Error(`client SETUP type must be ${ControlMessageType.ClientSetup}, got ${type}`) + if (type !== ControlMessageType.ClientSetup) + throw new Error(`client SETUP type must be ${ControlMessageType.ClientSetup}, got ${type}`) const advertisedLength = await buffer.getU16() const bufferLen = buffer.byteLength diff --git a/lib/transport/connection.ts b/lib/transport/connection.ts index 418f376..edd27f7 100644 --- a/lib/transport/connection.ts +++ b/lib/transport/connection.ts @@ -68,7 +68,7 @@ export class Connection { // Receive messages until the connection is closed. try { console.log("starting control loop") - for (; ;) { + for (;;) { const msg = await this.#controlStream.recv() await this.#recv(msg) } @@ -81,7 +81,7 @@ export class Connection { async #runObjects() { try { console.log("starting object loop") - for (; ;) { + for (;;) { const obj = await this.#objects.recv() console.log("object loop got obj", obj) if (!obj) break diff --git a/lib/transport/control/client_setup.ts b/lib/transport/control/client_setup.ts index a1ac3f8..4067079 100644 --- a/lib/transport/control/client_setup.ts +++ b/lib/transport/control/client_setup.ts @@ -25,7 +25,7 @@ export namespace ClientSetup { const numParams = reader.getNumberVarInt() const params = Parameters.deserialize_with_count(reader, numParams) return { - params + params, } } } diff --git a/lib/transport/control/fetch.ts b/lib/transport/control/fetch.ts index 088bd63..e149339 100644 --- a/lib/transport/control/fetch.ts +++ b/lib/transport/control/fetch.ts @@ -37,7 +37,6 @@ export interface StandaloneFetch { end_location: Location } - export namespace StandaloneFetch { export function serialize(v: StandaloneFetch): Uint8Array { const mainBuf = new MutableBytesBuffer(new Uint8Array()) @@ -62,12 +61,11 @@ export namespace StandaloneFetch { namespace, name, start_location, - end_location + end_location, } } } - export interface JoiningFetch { id: bigint start: bigint @@ -91,12 +89,11 @@ export namespace JoiningFetch { const start = r.getVarInt() return { id, - start + start, } } } - export interface Fetch { id: bigint fetch_type: FetchType @@ -147,7 +144,7 @@ export namespace Fetch { fetch_type, standalone, joining, - params + params, } } } diff --git a/lib/transport/control/fetch_cancel.ts b/lib/transport/control/fetch_cancel.ts index a6f4073..cc62ad1 100644 --- a/lib/transport/control/fetch_cancel.ts +++ b/lib/transport/control/fetch_cancel.ts @@ -1,29 +1,26 @@ import { ControlMessageType } from "." import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" - export interface FetchCancel { - id: bigint + id: bigint } - export namespace FetchCancel { - export function serialize(v: FetchCancel): Uint8Array { - const mainBuf = new MutableBytesBuffer(new Uint8Array()) - mainBuf.putVarInt(ControlMessageType.FetchCancel) - const payloadBuf = new MutableBytesBuffer(new Uint8Array()) - payloadBuf.putVarInt(v.id) + export function serialize(v: FetchCancel): Uint8Array { + const mainBuf = new MutableBytesBuffer(new Uint8Array()) + mainBuf.putVarInt(ControlMessageType.FetchCancel) + const payloadBuf = new MutableBytesBuffer(new Uint8Array()) + payloadBuf.putVarInt(v.id) + mainBuf.putU16(payloadBuf.byteLength) + mainBuf.putBytes(payloadBuf.Uint8Array) + return mainBuf.Uint8Array + } - mainBuf.putU16(payloadBuf.byteLength) - mainBuf.putBytes(payloadBuf.Uint8Array) - return mainBuf.Uint8Array - } - - export function deserialize(reader: ImmutableBytesBuffer): FetchCancel { - const id = reader.getVarInt() - return { - id - } - } -} \ No newline at end of file + export function deserialize(reader: ImmutableBytesBuffer): FetchCancel { + const id = reader.getVarInt() + return { + id, + } + } +} diff --git a/lib/transport/control/fetch_ok.ts b/lib/transport/control/fetch_ok.ts index 984fad2..ea6f67a 100644 --- a/lib/transport/control/fetch_ok.ts +++ b/lib/transport/control/fetch_ok.ts @@ -51,7 +51,7 @@ export namespace FetchOk { end_of_track, end_location, track_extensions, - params + params, } } } diff --git a/lib/transport/control/go_away.ts b/lib/transport/control/go_away.ts index eb64f6b..5e87d0a 100644 --- a/lib/transport/control/go_away.ts +++ b/lib/transport/control/go_away.ts @@ -1,28 +1,26 @@ import { ControlMessageType } from "." import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" - export interface GoAway { - session_uri: string + session_uri: string } - export namespace GoAway { - export function serialize(v: GoAway): Uint8Array { - const mainBuf = new MutableBytesBuffer(new Uint8Array()) - mainBuf.putVarInt(ControlMessageType.GoAway) - const payloadBuf = new MutableBytesBuffer(new Uint8Array()) - payloadBuf.putUtf8String(v.session_uri) + export function serialize(v: GoAway): Uint8Array { + const mainBuf = new MutableBytesBuffer(new Uint8Array()) + mainBuf.putVarInt(ControlMessageType.GoAway) + const payloadBuf = new MutableBytesBuffer(new Uint8Array()) + payloadBuf.putUtf8String(v.session_uri) - mainBuf.putU16(payloadBuf.byteLength) - mainBuf.putBytes(payloadBuf.Uint8Array) - return mainBuf.Uint8Array - } + mainBuf.putU16(payloadBuf.byteLength) + mainBuf.putBytes(payloadBuf.Uint8Array) + return mainBuf.Uint8Array + } - export function deserialize(reader: ImmutableBytesBuffer): GoAway { - const session_uri = reader.getUtf8String() - return { - session_uri - } - } -} \ No newline at end of file + export function deserialize(reader: ImmutableBytesBuffer): GoAway { + const session_uri = reader.getUtf8String() + return { + session_uri, + } + } +} diff --git a/lib/transport/control/index.ts b/lib/transport/control/index.ts index e78b666..895c279 100644 --- a/lib/transport/control/index.ts +++ b/lib/transport/control/index.ts @@ -23,7 +23,6 @@ import { RequestError } from "./request_error" import { MaxRequestId } from "./max_request_id" import { RequestsBlocked } from "./requests_blocked" - enum Version { DRAFT_00 = 0xff000000, DRAFT_01 = 0xff000001, @@ -66,13 +65,35 @@ type MessageWithType = type Message = Subscriber | Publisher // Sent by subscriber -type Subscriber = Subscribe | SubscribeUpdate | SubscribeNamespace | - Unsubscribe | PublishOk | Fetch | FetchCancel | PublishNamespaceCancel | - TrackStatus | MaxRequestId | RequestsBlocked | RequestOk | RequestError +type Subscriber = + | Subscribe + | SubscribeUpdate + | SubscribeNamespace + | Unsubscribe + | PublishOk + | Fetch + | FetchCancel + | PublishNamespaceCancel + | TrackStatus + | MaxRequestId + | RequestsBlocked + | RequestOk + | RequestError // Sent by publisher -type Publisher = SubscribeOk | PublishDone | Publish | PublishNamespace | PublishNamespaceDone - | Namespace | NamespaceDone | FetchOk | MaxRequestId | RequestsBlocked | RequestOk | RequestError +type Publisher = + | SubscribeOk + | PublishDone + | Publish + | PublishNamespace + | PublishNamespaceDone + | Namespace + | NamespaceDone + | FetchOk + | MaxRequestId + | RequestsBlocked + | RequestOk + | RequestError function isSubscriber(m: ControlMessageType): boolean { return ( @@ -139,42 +160,66 @@ export enum ControlMessageType { ServerSetup = 0x21, // Legacy aliases for backward compat during transition - SubscribeUpdate = 0x2, // Same as RequestUpdate in draft-16 + SubscribeUpdate = 0x2, // Same as RequestUpdate in draft-16 } export namespace ControlMessageType { export function toString(t: ControlMessageType): string { switch (t) { - case ControlMessageType.ReservedSetupV00: return "ReservedSetupV00" - case ControlMessageType.GoAway: return "GoAway" - case ControlMessageType.MaxRequestId: return "MaxRequestId" - case ControlMessageType.RequestsBlocked: return "RequestsBlocked" - case ControlMessageType.RequestUpdate: return "RequestUpdate" - case ControlMessageType.Subscribe: return "Subscribe" - case ControlMessageType.SubscribeOk: return "SubscribeOk" - case ControlMessageType.RequestError: return "RequestError" - case ControlMessageType.Unsubscribe: return "Unsubscribe" - case ControlMessageType.PublishDone: return "PublishDone" - case ControlMessageType.PublishNamespaceCancel: return "PublishNamespaceCancel" - case ControlMessageType.TrackStatus: return "TrackStatus" - case ControlMessageType.NamespaceDone: return "NamespaceDone" - case ControlMessageType.Publish: return "Publish" - case ControlMessageType.PublishOk: return "PublishOk" - case ControlMessageType.PublishNamespace: return "PublishNamespace" - case ControlMessageType.RequestOk: return "RequestOk" - case ControlMessageType.Namespace: return "Namespace" - case ControlMessageType.PublishNamespaceDone: return "PublishNamespaceDone" - case ControlMessageType.SubscribeNamespace: return "SubscribeNamespace" - case ControlMessageType.Fetch: return "Fetch" - case ControlMessageType.FetchCancel: return "FetchCancel" - case ControlMessageType.FetchOk: return "FetchOk" - case ControlMessageType.ClientSetup: return "ClientSetup" - case ControlMessageType.ServerSetup: return "ServerSetup" + case ControlMessageType.ReservedSetupV00: + return "ReservedSetupV00" + case ControlMessageType.GoAway: + return "GoAway" + case ControlMessageType.MaxRequestId: + return "MaxRequestId" + case ControlMessageType.RequestsBlocked: + return "RequestsBlocked" + case ControlMessageType.RequestUpdate: + return "RequestUpdate" + case ControlMessageType.Subscribe: + return "Subscribe" + case ControlMessageType.SubscribeOk: + return "SubscribeOk" + case ControlMessageType.RequestError: + return "RequestError" + case ControlMessageType.Unsubscribe: + return "Unsubscribe" + case ControlMessageType.PublishDone: + return "PublishDone" + case ControlMessageType.PublishNamespaceCancel: + return "PublishNamespaceCancel" + case ControlMessageType.TrackStatus: + return "TrackStatus" + case ControlMessageType.NamespaceDone: + return "NamespaceDone" + case ControlMessageType.Publish: + return "Publish" + case ControlMessageType.PublishOk: + return "PublishOk" + case ControlMessageType.PublishNamespace: + return "PublishNamespace" + case ControlMessageType.RequestOk: + return "RequestOk" + case ControlMessageType.Namespace: + return "Namespace" + case ControlMessageType.PublishNamespaceDone: + return "PublishNamespaceDone" + case ControlMessageType.SubscribeNamespace: + return "SubscribeNamespace" + case ControlMessageType.Fetch: + return "Fetch" + case ControlMessageType.FetchCancel: + return "FetchCancel" + case ControlMessageType.FetchOk: + return "FetchOk" + case ControlMessageType.ClientSetup: + return "ClientSetup" + case ControlMessageType.ServerSetup: + return "ServerSetup" } } } - export { Subscribe, SubscribeOk, @@ -201,7 +246,6 @@ export { RequestsBlocked, RequestOk, RequestError, - Version, isSubscriber, isPublisher, diff --git a/lib/transport/control/max_request_id.ts b/lib/transport/control/max_request_id.ts index 2c58538..dee7c5c 100644 --- a/lib/transport/control/max_request_id.ts +++ b/lib/transport/control/max_request_id.ts @@ -1,29 +1,28 @@ import { ControlMessageType } from "." import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" - // Draft-16: MAX_REQUEST_ID (Section 9.5, type 0x15) // Sent to increase the number of requests the peer can send within a session. // The Maximum Request ID MUST only increase within a session. export interface MaxRequestId { - max_request_id: bigint + max_request_id: bigint } export namespace MaxRequestId { - export function serialize(v: MaxRequestId): Uint8Array { - const mainBuf = new MutableBytesBuffer(new Uint8Array()) - mainBuf.putVarInt(ControlMessageType.MaxRequestId) - const payloadBuf = new MutableBytesBuffer(new Uint8Array()) - payloadBuf.putVarInt(v.max_request_id) - mainBuf.putU16(payloadBuf.byteLength) - mainBuf.putBytes(payloadBuf.Uint8Array) - return mainBuf.Uint8Array - } + export function serialize(v: MaxRequestId): Uint8Array { + const mainBuf = new MutableBytesBuffer(new Uint8Array()) + mainBuf.putVarInt(ControlMessageType.MaxRequestId) + const payloadBuf = new MutableBytesBuffer(new Uint8Array()) + payloadBuf.putVarInt(v.max_request_id) + mainBuf.putU16(payloadBuf.byteLength) + mainBuf.putBytes(payloadBuf.Uint8Array) + return mainBuf.Uint8Array + } - export function deserialize(reader: ImmutableBytesBuffer): MaxRequestId { - const max_request_id = reader.getVarInt() - return { - max_request_id, - } - } -} \ No newline at end of file + export function deserialize(reader: ImmutableBytesBuffer): MaxRequestId { + const max_request_id = reader.getVarInt() + return { + max_request_id, + } + } +} diff --git a/lib/transport/control/namespace.ts b/lib/transport/control/namespace.ts index a381645..e739037 100644 --- a/lib/transport/control/namespace.ts +++ b/lib/transport/control/namespace.ts @@ -2,7 +2,6 @@ import { ControlMessageType } from "." import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" import { Tuple } from "../base_data" - export interface Namespace { namespace_suffix: string[] } diff --git a/lib/transport/control/namespace_done.ts b/lib/transport/control/namespace_done.ts index 66da359..5ba7386 100644 --- a/lib/transport/control/namespace_done.ts +++ b/lib/transport/control/namespace_done.ts @@ -2,7 +2,6 @@ import { ControlMessageType } from "." import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" import { Tuple } from "../base_data" - export interface NamespaceDone { namespace_suffix: string[] } diff --git a/lib/transport/control/publish.ts b/lib/transport/control/publish.ts index 383ece1..889d8d6 100644 --- a/lib/transport/control/publish.ts +++ b/lib/transport/control/publish.ts @@ -55,7 +55,7 @@ export namespace Publish { namespace, name, track_extensions, - params + params, } } } diff --git a/lib/transport/control/publish_done.ts b/lib/transport/control/publish_done.ts index ccb6aa8..c1878c6 100644 --- a/lib/transport/control/publish_done.ts +++ b/lib/transport/control/publish_done.ts @@ -2,7 +2,6 @@ import { ControlMessageType } from "." import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" import { ReasonPhrase } from "../base_data" - export interface PublishDone { id: bigint code: bigint @@ -34,7 +33,7 @@ export namespace PublishDone { id, code, stream_count, - reason + reason, } } } diff --git a/lib/transport/control/publish_namespace.ts b/lib/transport/control/publish_namespace.ts index ac13926..06a4f29 100644 --- a/lib/transport/control/publish_namespace.ts +++ b/lib/transport/control/publish_namespace.ts @@ -2,7 +2,6 @@ import { ControlMessageType } from "." import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" import { Parameters, Tuple, KeyValuePairs } from "../base_data" - export interface PublishNamespace { id: bigint namespace: Tuple @@ -36,7 +35,7 @@ export namespace PublishNamespace { return { id, namespace, - params + params, } } } diff --git a/lib/transport/control/publish_namespace_cancel.ts b/lib/transport/control/publish_namespace_cancel.ts index 7640cd5..eaafb01 100644 --- a/lib/transport/control/publish_namespace_cancel.ts +++ b/lib/transport/control/publish_namespace_cancel.ts @@ -2,7 +2,6 @@ import { ControlMessageType } from "." import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" import { ReasonPhrase } from "../base_data" - export interface PublishNamespaceCancel { id: bigint error_code: bigint @@ -29,7 +28,7 @@ export namespace PublishNamespaceCancel { return { id, error_code, - error_reason + error_reason, } } } diff --git a/lib/transport/control/publish_ok.ts b/lib/transport/control/publish_ok.ts index d94fbf0..8494f38 100644 --- a/lib/transport/control/publish_ok.ts +++ b/lib/transport/control/publish_ok.ts @@ -2,8 +2,6 @@ import { ControlMessageType } from "." import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" import { Parameters } from "../base_data" - - export interface PublishOk { id: bigint // Request ID params?: Parameters @@ -32,7 +30,7 @@ export namespace PublishOk { } return { id, - params + params, } } } diff --git a/lib/transport/control/request_error.ts b/lib/transport/control/request_error.ts index 75fb6f3..8574d42 100644 --- a/lib/transport/control/request_error.ts +++ b/lib/transport/control/request_error.ts @@ -5,9 +5,9 @@ import { ReasonPhrase } from "../base_data" // Draft-16: REQUEST_ERROR (Section 9.8) // Sent in response to any request (SUBSCRIBE, FETCH, PUBLISH, SUBSCRIBE_NAMESPACE, PUBLISH_NAMESPACE, TRACK_STATUS) export interface RequestError { - id: bigint // Request ID - code: bigint // Error Code (RequestErrorCode) - retry_interval: bigint // Minimum retry time in ms + 1; 0 = don't retry + id: bigint // Request ID + code: bigint // Error Code (RequestErrorCode) + retry_interval: bigint // Minimum retry time in ms + 1; 0 = don't retry reason: ReasonPhrase } diff --git a/lib/transport/control/request_ok.ts b/lib/transport/control/request_ok.ts index 0017d28..54eead0 100644 --- a/lib/transport/control/request_ok.ts +++ b/lib/transport/control/request_ok.ts @@ -5,32 +5,32 @@ import { Parameters } from "../base_data" // Draft-16: REQUEST_OK (Section 9.7) // Sent in response to REQUEST_UPDATE, TRACK_STATUS, SUBSCRIBE_NAMESPACE, PUBLISH_NAMESPACE export interface RequestOk { - id: bigint // Request ID - parameters: Parameters + id: bigint // Request ID + parameters: Parameters } export namespace RequestOk { - export function serialize(v: RequestOk): Uint8Array { - const mainBuf = new MutableBytesBuffer(new Uint8Array()) - mainBuf.putVarInt(ControlMessageType.RequestOk) - const payloadBuf = new MutableBytesBuffer(new Uint8Array()) - payloadBuf.putVarInt(v.id) - const paramsBytes = Parameters.serialize(v.parameters) - payloadBuf.putVarInt(v.parameters.size) // Number of Parameters - payloadBuf.putBytes(paramsBytes) + export function serialize(v: RequestOk): Uint8Array { + const mainBuf = new MutableBytesBuffer(new Uint8Array()) + mainBuf.putVarInt(ControlMessageType.RequestOk) + const payloadBuf = new MutableBytesBuffer(new Uint8Array()) + payloadBuf.putVarInt(v.id) + const paramsBytes = Parameters.serialize(v.parameters) + payloadBuf.putVarInt(v.parameters.size) // Number of Parameters + payloadBuf.putBytes(paramsBytes) - mainBuf.putU16(payloadBuf.byteLength) - mainBuf.putBytes(payloadBuf.Uint8Array) - return mainBuf.Uint8Array - } + mainBuf.putU16(payloadBuf.byteLength) + mainBuf.putBytes(payloadBuf.Uint8Array) + return mainBuf.Uint8Array + } - export function deserialize(reader: ImmutableBytesBuffer): RequestOk { - const id = reader.getVarInt() - const numParams = reader.getNumberVarInt() - const parameters = Parameters.deserialize_with_count(reader, numParams) - return { - id, - parameters, - } - } + export function deserialize(reader: ImmutableBytesBuffer): RequestOk { + const id = reader.getVarInt() + const numParams = reader.getNumberVarInt() + const parameters = Parameters.deserialize_with_count(reader, numParams) + return { + id, + parameters, + } + } } diff --git a/lib/transport/control/requests_blocked.ts b/lib/transport/control/requests_blocked.ts index 10050b0..eda0548 100644 --- a/lib/transport/control/requests_blocked.ts +++ b/lib/transport/control/requests_blocked.ts @@ -1,7 +1,6 @@ import { ControlMessageType } from "." import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" - // Draft-16: REQUESTS_BLOCKED (Section 9.6, type 0x1A) // Sent when an endpoint would like to send a new request, but cannot // because the Request ID would exceed the Maximum Request ID value sent by the peer. diff --git a/lib/transport/control/server_setup.ts b/lib/transport/control/server_setup.ts index 550c954..90b5967 100644 --- a/lib/transport/control/server_setup.ts +++ b/lib/transport/control/server_setup.ts @@ -24,7 +24,7 @@ export namespace ServerSetup { const numParams = reader.getNumberVarInt() const params = Parameters.deserialize_with_count(reader, numParams) return { - params + params, } } } diff --git a/lib/transport/control/setup_parameters.ts b/lib/transport/control/setup_parameters.ts index 4bd8178..67d233a 100644 --- a/lib/transport/control/setup_parameters.ts +++ b/lib/transport/control/setup_parameters.ts @@ -1,8 +1,7 @@ export enum SetupParameters { - Authority = 0x05, - Path = 0x01, - MaxRequestId = 0x02, - MaxAuthTokenCacheSize = 0x04, - // TODO(itzmanish): implement more params from the draft + Authority = 0x05, + Path = 0x01, + MaxRequestId = 0x02, + MaxAuthTokenCacheSize = 0x04, + // TODO(itzmanish): implement more params from the draft } - diff --git a/lib/transport/control/subscribe.ts b/lib/transport/control/subscribe.ts index e04ae81..30c80f3 100644 --- a/lib/transport/control/subscribe.ts +++ b/lib/transport/control/subscribe.ts @@ -61,15 +61,13 @@ export namespace FilterType { } } - export interface Subscribe { id: bigint // Request ID - namespace: Tuple, + namespace: Tuple name: string params: Parameters } - export namespace Subscribe { export function serialize(v: Subscribe): Uint8Array { const mainBuf = new MutableBytesBuffer(new Uint8Array()) diff --git a/lib/transport/control/subscribe_namespace.ts b/lib/transport/control/subscribe_namespace.ts index d930c51..ce02ff6 100644 --- a/lib/transport/control/subscribe_namespace.ts +++ b/lib/transport/control/subscribe_namespace.ts @@ -2,7 +2,6 @@ import { ControlMessageType } from "." import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" import { Tuple, Parameters, KeyValuePairs } from "../base_data" - export enum SubscribeOptions { PUBLISH = 0x00, NAMESPACE = 0x01, @@ -10,13 +9,12 @@ export enum SubscribeOptions { } export interface SubscribeNamespace { - id: bigint, + id: bigint namespace: string[] subscribe_options: SubscribeOptions params?: Parameters } - export namespace SubscribeNamespace { export function serialize(v: SubscribeNamespace): Uint8Array { const mainBuf = new MutableBytesBuffer(new Uint8Array()) @@ -43,7 +41,7 @@ export namespace SubscribeNamespace { id, namespace, subscribe_options, - params + params, } } } diff --git a/lib/transport/control/subscribe_ok.ts b/lib/transport/control/subscribe_ok.ts index 50637bd..38cf2c5 100644 --- a/lib/transport/control/subscribe_ok.ts +++ b/lib/transport/control/subscribe_ok.ts @@ -3,7 +3,6 @@ import { GroupOrder } from "./subscribe" import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" import { Parameters, KeyValuePairs } from "../base_data" - export interface SubscribeOk { id: bigint // Request ID track_alias: bigint diff --git a/lib/transport/control/subscribe_update.ts b/lib/transport/control/subscribe_update.ts index 82ed7d3..4153038 100644 --- a/lib/transport/control/subscribe_update.ts +++ b/lib/transport/control/subscribe_update.ts @@ -3,12 +3,11 @@ import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" import { Parameters, Location } from "../base_data" export interface SubscribeUpdate { - id: bigint // Request ID (new) - subscription_id: bigint // Existing Request ID + id: bigint // Request ID (new) + subscription_id: bigint // Existing Request ID params?: Parameters } - export namespace SubscribeUpdate { export function serialize(v: SubscribeUpdate): Uint8Array { const mainBuf = new MutableBytesBuffer(new Uint8Array()) diff --git a/lib/transport/control/track_status.ts b/lib/transport/control/track_status.ts index 4dab7e3..6e7aa94 100644 --- a/lib/transport/control/track_status.ts +++ b/lib/transport/control/track_status.ts @@ -2,7 +2,6 @@ import { ControlMessageType } from "." import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" import { Tuple, Parameters } from "../base_data" - export interface TrackStatus { id: bigint namespace: Tuple diff --git a/lib/transport/control/unsubscribe.ts b/lib/transport/control/unsubscribe.ts index a092f82..581d9c0 100644 --- a/lib/transport/control/unsubscribe.ts +++ b/lib/transport/control/unsubscribe.ts @@ -1,27 +1,26 @@ import { ControlMessageType } from "." import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" - export interface Unsubscribe { - id: bigint + id: bigint } export namespace Unsubscribe { - export function serialize(v: Unsubscribe): Uint8Array { - const mainBuf = new MutableBytesBuffer(new Uint8Array()) - mainBuf.putVarInt(ControlMessageType.Unsubscribe) - const payloadBuf = new MutableBytesBuffer(new Uint8Array()) - payloadBuf.putVarInt(v.id) + export function serialize(v: Unsubscribe): Uint8Array { + const mainBuf = new MutableBytesBuffer(new Uint8Array()) + mainBuf.putVarInt(ControlMessageType.Unsubscribe) + const payloadBuf = new MutableBytesBuffer(new Uint8Array()) + payloadBuf.putVarInt(v.id) - mainBuf.putU16(payloadBuf.byteLength) - mainBuf.putBytes(payloadBuf.Uint8Array) - return mainBuf.Uint8Array - } + mainBuf.putU16(payloadBuf.byteLength) + mainBuf.putBytes(payloadBuf.Uint8Array) + return mainBuf.Uint8Array + } - export function deserialize(reader: ImmutableBytesBuffer): Unsubscribe { - const id = reader.getVarInt() - return { - id - } - } -} \ No newline at end of file + export function deserialize(reader: ImmutableBytesBuffer): Unsubscribe { + const id = reader.getVarInt() + return { + id, + } + } +} diff --git a/lib/transport/fetch.ts b/lib/transport/fetch.ts index ca9b492..6ad1504 100644 --- a/lib/transport/fetch.ts +++ b/lib/transport/fetch.ts @@ -1,25 +1,21 @@ - export interface FetchHeader { - type: FetchType - id: bigint + type: FetchType + id: bigint } - export enum FetchType { - Type0x5 = 0x5, + Type0x5 = 0x5, } export namespace FetchType { - export function try_from(value: number | bigint): FetchType { - const v = typeof value === "bigint" ? Number(value) : value + export function try_from(value: number | bigint): FetchType { + const v = typeof value === "bigint" ? Number(value) : value - switch (v) { - case FetchType.Type0x5: - return v as FetchType - default: - throw new Error(`invalid fetch type: ${v}`) - } - } + switch (v) { + case FetchType.Type0x5: + return v as FetchType + default: + throw new Error(`invalid fetch type: ${v}`) + } + } } - - diff --git a/lib/transport/objects.ts b/lib/transport/objects.ts index c8aa534..3fc65ed 100644 --- a/lib/transport/objects.ts +++ b/lib/transport/objects.ts @@ -1,7 +1,14 @@ import { SubgroupHeader, SubgroupObject, SubgroupReader, SubgroupType, SubgroupWriter } from "./subgroup" import { KeyValuePairs } from "./base_data" import { debug } from "./utils" -import { ImmutableBytesBuffer, MutableBytesBuffer, ReadableStreamBuffer, Reader, WritableStreamBuffer, Writer } from "./buffer" +import { + ImmutableBytesBuffer, + MutableBytesBuffer, + ReadableStreamBuffer, + Reader, + WritableStreamBuffer, + Writer, +} from "./buffer" export enum ObjectForwardingPreference { Datagram = "Datagram", @@ -57,7 +64,6 @@ export function isDatagram(obj: ObjectDatagram | SubgroupHeader): boolean { return (obj.type & 0x10) === 0 } - // Draft-16: Object Datagram types use bitmask structure 0b00X0XXXX // Valid ranges: 0x00..0x0F, 0x20..0x2F // Bit layout: @@ -118,7 +124,7 @@ export namespace ObjectDatagramType { throw new Error(`invalid object datagram type: ${v} (bit 4 set - this is a subgroup type)`) } // Must be in ranges 0x00..0x0F or 0x20..0x2F - if (v < 0x00 || (v > 0x0F && v < 0x20) || v > 0x2F) { + if (v < 0x00 || (v > 0x0f && v < 0x20) || v > 0x2f) { throw new Error(`invalid object datagram type: ${v} (out of range)`) } // STATUS + END_OF_GROUP is invalid @@ -155,7 +161,7 @@ export interface ObjectDatagram { track_alias: bigint group_id: number object_id?: number - publisher_priority?: number // undefined when DEFAULT_PRIORITY bit is set + publisher_priority?: number // undefined when DEFAULT_PRIORITY bit is set extension_headers?: KeyValuePairs status?: Status object_payload?: Uint8Array @@ -237,7 +243,7 @@ export class Objects { } async send(h: ObjectDatagram | SubgroupHeader): Promise { - const is_datagram = isDatagram(h); + const is_datagram = isDatagram(h) if (is_datagram) { // Datagram mode @@ -318,9 +324,7 @@ export class TrackWriter { // For compatibility with reader interface public header = { track_alias: 0n } - constructor( - public stream: Writer, - ) { } + constructor(public stream: Writer) {} async write(c: ObjectDatagram) { return this.stream.write(ObjectDatagram.serialize(c)) @@ -331,15 +335,11 @@ export class TrackWriter { } } - export class TrackReader { // Header with track_alias for routing public header: { track_alias: bigint } - constructor( - stream: Reader, - track_alias: bigint = 0n, - ) { + constructor(stream: Reader, track_alias: bigint = 0n) { this.stream = stream this.header = { track_alias } } @@ -392,4 +392,3 @@ export class TrackReader { await this.stream.close() } } - diff --git a/lib/transport/publisher.ts b/lib/transport/publisher.ts index 509d2cb..cec4d2d 100644 --- a/lib/transport/publisher.ts +++ b/lib/transport/publisher.ts @@ -55,20 +55,20 @@ export class Publisher { } async recv(msg: Control.MessageWithType) { - const { type, message } = msg; + const { type, message } = msg switch (type) { case Control.ControlMessageType.Subscribe: await this.recvSubscribe(message) - break; + break case Control.ControlMessageType.Unsubscribe: this.recvUnsubscribe(message) - break; + break case Control.ControlMessageType.RequestOk: this.recvRequestOk(message) - break; + break case Control.ControlMessageType.RequestError: this.recvRequestError(message) - break; + break default: throw new Error(`unknown control message`) // impossible } @@ -121,7 +121,7 @@ export class Publisher { code: 0n, retry_interval: 0n, reason: e.message, - } + }, }) throw e } @@ -151,7 +151,7 @@ export class PublishNamespaceSend { } async ok() { - for (; ;) { + for (;;) { const [state, next] = this.#state.value() if (state === "ack") return if (state instanceof Error) throw state @@ -162,7 +162,7 @@ export class PublishNamespaceSend { } async active() { - for (; ;) { + for (;;) { const [state, next] = this.#state.value() if (state instanceof Error) throw state if (!next) return @@ -199,7 +199,7 @@ export class SubscribeRecv { #objects: Objects #id: bigint #trackAlias: bigint // Publisher-specified in draft-14 - params: Parameters; + params: Parameters readonly namespace: string[] readonly track: string @@ -232,12 +232,20 @@ export class SubscribeRecv { id: this.#id, track_alias: this.#trackAlias, params: this.params, - } + }, }) } // Close the subscription with an error. - async close({ code = 0n, reason = "", unsubscribe = true }: { code?: bigint; reason?: string; unsubscribe?: boolean }) { + async close({ + code = 0n, + reason = "", + unsubscribe = true, + }: { + code?: bigint + reason?: string + unsubscribe?: boolean + }) { if (this.#state === "closed") return const acked = this.#state === "ack" this.#state = "closed" @@ -245,13 +253,13 @@ export class SubscribeRecv { if (!acked) { return this.#control.send({ type: Control.ControlMessageType.RequestError, - message: { id: this.#id, code, retry_interval: 0n, reason } + message: { id: this.#id, code, retry_interval: 0n, reason }, }) } if (unsubscribe) { return this.#control.send({ type: Control.ControlMessageType.Unsubscribe, - message: { id: this.#id } + message: { id: this.#id }, }) } } diff --git a/lib/transport/stream.ts b/lib/transport/stream.ts index 19b2fcc..39418d0 100644 --- a/lib/transport/stream.ts +++ b/lib/transport/stream.ts @@ -1,16 +1,27 @@ - import { ControlMessageType, - MessageWithType, Publish, - PublishDone, PublishNamespace, - PublishNamespaceDone, PublishNamespaceCancel, - PublishOk, Unsubscribe, - Fetch, FetchOk, FetchCancel, - Subscribe, SubscribeOk, - SubscribeUpdate, SubscribeNamespace, - Namespace, NamespaceDone, TrackStatus, - MaxRequestId, RequestsBlocked, - RequestOk, RequestError, + MessageWithType, + Publish, + PublishDone, + PublishNamespace, + PublishNamespaceDone, + PublishNamespaceCancel, + PublishOk, + Unsubscribe, + Fetch, + FetchOk, + FetchCancel, + Subscribe, + SubscribeOk, + SubscribeUpdate, + SubscribeNamespace, + Namespace, + NamespaceDone, + TrackStatus, + MaxRequestId, + RequestsBlocked, + RequestOk, + RequestError, } from "./control" import { debug } from "./utils" import { ImmutableBytesBuffer, ReadableWritableStreamBuffer, Reader, Writer } from "./buffer" @@ -227,7 +238,6 @@ export class Decoder { } return res - } } diff --git a/lib/transport/subgroup.ts b/lib/transport/subgroup.ts index ae81db7..8bf89ec 100644 --- a/lib/transport/subgroup.ts +++ b/lib/transport/subgroup.ts @@ -3,54 +3,54 @@ import { KeyValuePairs } from "./base_data" import { Status } from "./objects" export interface SubgroupHeader { - type: SubgroupType - track_alias: bigint - group_id: number - subgroup_id?: number - publisher_priority?: number // undefined when DEFAULT_PRIORITY bit is set + type: SubgroupType + track_alias: bigint + group_id: number + subgroup_id?: number + publisher_priority?: number // undefined when DEFAULT_PRIORITY bit is set } export namespace SubgroupHeader { - export function serialize(header: SubgroupHeader): Uint8Array { - const buf = new MutableBytesBuffer(new Uint8Array()) - buf.putBytes(SubgroupType.serialize(header.type)) - buf.putVarInt(header.track_alias) - buf.putVarInt(header.group_id) - if (SubgroupType.hasExplicitSubgroupId(header.type) && header.subgroup_id !== undefined) { - buf.putVarInt(header.subgroup_id) - } - if (!SubgroupType.hasDefaultPriority(header.type) && header.publisher_priority !== undefined) { - buf.putU8(header.publisher_priority) - } - return buf.Uint8Array - } + export function serialize(header: SubgroupHeader): Uint8Array { + const buf = new MutableBytesBuffer(new Uint8Array()) + buf.putBytes(SubgroupType.serialize(header.type)) + buf.putVarInt(header.track_alias) + buf.putVarInt(header.group_id) + if (SubgroupType.hasExplicitSubgroupId(header.type) && header.subgroup_id !== undefined) { + buf.putVarInt(header.subgroup_id) + } + if (!SubgroupType.hasDefaultPriority(header.type) && header.publisher_priority !== undefined) { + buf.putU8(header.publisher_priority) + } + return buf.Uint8Array + } } export interface SubgroupObject { - object_id: number - extension_headers?: KeyValuePairs - status?: Status // only if payload is null - object_payload?: Uint8Array + object_id: number + extension_headers?: KeyValuePairs + status?: Status // only if payload is null + object_payload?: Uint8Array } export namespace SubgroupObject { - export function serialize(obj: SubgroupObject): Uint8Array { - const buf = new MutableBytesBuffer(new Uint8Array()) - buf.putVarInt(obj.object_id) - - if (obj.extension_headers) { - const extHeadersBytes = KeyValuePairs.serialize(obj.extension_headers) - buf.putVarInt(extHeadersBytes.length) - buf.putBytes(extHeadersBytes) - } - buf.putVarInt(obj.object_payload?.length ?? 0) - if (!obj.object_payload) { - buf.putVarInt(obj.status!) - } else { - buf.putBytes(obj.object_payload) - } - return buf.Uint8Array - } + export function serialize(obj: SubgroupObject): Uint8Array { + const buf = new MutableBytesBuffer(new Uint8Array()) + buf.putVarInt(obj.object_id) + + if (obj.extension_headers) { + const extHeadersBytes = KeyValuePairs.serialize(obj.extension_headers) + buf.putVarInt(extHeadersBytes.length) + buf.putBytes(extHeadersBytes) + } + buf.putVarInt(obj.object_payload?.length ?? 0) + if (!obj.object_payload) { + buf.putVarInt(obj.status!) + } else { + buf.putBytes(obj.object_payload) + } + return buf.Uint8Array + } } // Draft-16: Subgroup header types use bitmask structure 0b00X1XXXX @@ -62,158 +62,157 @@ export namespace SubgroupObject { // bit 4 (0x10) = always set (stream type marker) // bit 5 (0x20) = DEFAULT_PRIORITY (when set, priority field omitted) export enum SubgroupType { - // Base types (0x10..0x1D) - Type0x10 = 0x10, - Type0x11 = 0x11, - Type0x12 = 0x12, - Type0x13 = 0x13, - Type0x14 = 0x14, - Type0x15 = 0x15, - Type0x18 = 0x18, - Type0x19 = 0x19, - Type0x1A = 0x1A, - Type0x1B = 0x1B, - Type0x1C = 0x1C, - Type0x1D = 0x1D, - // DEFAULT_PRIORITY types (0x30..0x3D) - Type0x30 = 0x30, - Type0x31 = 0x31, - Type0x32 = 0x32, - Type0x33 = 0x33, - Type0x34 = 0x34, - Type0x35 = 0x35, - Type0x38 = 0x38, - Type0x39 = 0x39, - Type0x3A = 0x3A, - Type0x3B = 0x3B, - Type0x3C = 0x3C, - Type0x3D = 0x3D, + // Base types (0x10..0x1D) + Type0x10 = 0x10, + Type0x11 = 0x11, + Type0x12 = 0x12, + Type0x13 = 0x13, + Type0x14 = 0x14, + Type0x15 = 0x15, + Type0x18 = 0x18, + Type0x19 = 0x19, + Type0x1A = 0x1a, + Type0x1B = 0x1b, + Type0x1C = 0x1c, + Type0x1D = 0x1d, + // DEFAULT_PRIORITY types (0x30..0x3D) + Type0x30 = 0x30, + Type0x31 = 0x31, + Type0x32 = 0x32, + Type0x33 = 0x33, + Type0x34 = 0x34, + Type0x35 = 0x35, + Type0x38 = 0x38, + Type0x39 = 0x39, + Type0x3A = 0x3a, + Type0x3B = 0x3b, + Type0x3C = 0x3c, + Type0x3D = 0x3d, } export namespace SubgroupType { - // Bitmask constants - const EXTENSIONS_BIT = 0x01 - const SUBGROUP_ID_MASK = 0x06 - const END_OF_GROUP_BIT = 0x08 - const DEFAULT_PRIORITY_BIT = 0x20 - - export function serialize(type: SubgroupType): Uint8Array { - const w = new MutableBytesBuffer(new Uint8Array()) - w.putVarInt(type) - return w.Uint8Array - } - export function deserialize(reader: ImmutableBytesBuffer): SubgroupType { - return try_from(reader.getNumberVarInt()) - } - - // may throw if invalid value is provided - export function try_from(value: number | bigint): SubgroupType { - const v = typeof value === "bigint" ? Number(value) : value - - // Must match form 0b00X1XXXX (bit 4 set) - if ((v & 0x10) === 0) { - throw new Error(`invalid subgroup type: ${v} (bit 4 not set)`) - } - // Must be in ranges 0x10..0x1F or 0x30..0x3F - if (v < 0x10 || (v > 0x1F && v < 0x30) || v > 0x3F) { - throw new Error(`invalid subgroup type: ${v} (out of range)`) - } - // SUBGROUP_ID_MODE = 0b11 is reserved - if (((v & SUBGROUP_ID_MASK) >> 1) === 3) { - throw new Error(`invalid subgroup type: ${v} (reserved SUBGROUP_ID_MODE=0b11)`) - } - return v as SubgroupType - } - - export function isSubgroupIdPresent(type: SubgroupType) { - return ((type & SUBGROUP_ID_MASK) >> 1) === 2 - } - - export function hasExplicitSubgroupId(type: SubgroupType) { - return isSubgroupIdPresent(type) - } - - export function isSubgroupIdZero(type: SubgroupType) { - return ((type & SUBGROUP_ID_MASK) >> 1) === 0 - } - - export function isSubgroupFirstObjectId(type: SubgroupType) { - return ((type & SUBGROUP_ID_MASK) >> 1) === 1 - } - - export function isExtensionPresent(type: SubgroupType) { - return (type & EXTENSIONS_BIT) !== 0 - } - - export function contains_end_of_group(type: SubgroupType) { - return (type & END_OF_GROUP_BIT) !== 0 - } - - export function hasDefaultPriority(type: SubgroupType) { - return (type & DEFAULT_PRIORITY_BIT) !== 0 - } + // Bitmask constants + const EXTENSIONS_BIT = 0x01 + const SUBGROUP_ID_MASK = 0x06 + const END_OF_GROUP_BIT = 0x08 + const DEFAULT_PRIORITY_BIT = 0x20 + + export function serialize(type: SubgroupType): Uint8Array { + const w = new MutableBytesBuffer(new Uint8Array()) + w.putVarInt(type) + return w.Uint8Array + } + export function deserialize(reader: ImmutableBytesBuffer): SubgroupType { + return try_from(reader.getNumberVarInt()) + } + + // may throw if invalid value is provided + export function try_from(value: number | bigint): SubgroupType { + const v = typeof value === "bigint" ? Number(value) : value + + // Must match form 0b00X1XXXX (bit 4 set) + if ((v & 0x10) === 0) { + throw new Error(`invalid subgroup type: ${v} (bit 4 not set)`) + } + // Must be in ranges 0x10..0x1F or 0x30..0x3F + if (v < 0x10 || (v > 0x1f && v < 0x30) || v > 0x3f) { + throw new Error(`invalid subgroup type: ${v} (out of range)`) + } + // SUBGROUP_ID_MODE = 0b11 is reserved + if ((v & SUBGROUP_ID_MASK) >> 1 === 3) { + throw new Error(`invalid subgroup type: ${v} (reserved SUBGROUP_ID_MODE=0b11)`) + } + return v as SubgroupType + } + + export function isSubgroupIdPresent(type: SubgroupType) { + return (type & SUBGROUP_ID_MASK) >> 1 === 2 + } + + export function hasExplicitSubgroupId(type: SubgroupType) { + return isSubgroupIdPresent(type) + } + + export function isSubgroupIdZero(type: SubgroupType) { + return (type & SUBGROUP_ID_MASK) >> 1 === 0 + } + + export function isSubgroupFirstObjectId(type: SubgroupType) { + return (type & SUBGROUP_ID_MASK) >> 1 === 1 + } + + export function isExtensionPresent(type: SubgroupType) { + return (type & EXTENSIONS_BIT) !== 0 + } + + export function contains_end_of_group(type: SubgroupType) { + return (type & END_OF_GROUP_BIT) !== 0 + } + + export function hasDefaultPriority(type: SubgroupType) { + return (type & DEFAULT_PRIORITY_BIT) !== 0 + } } - export class SubgroupWriter { - constructor( - public header: SubgroupHeader, - public stream: Writer, - ) { } - - async write(c: SubgroupObject) { - return this.stream.write(SubgroupObject.serialize(c)) - } - - async close() { - return this.stream.close() - } + constructor( + public header: SubgroupHeader, + public stream: Writer, + ) {} + + async write(c: SubgroupObject) { + return this.stream.write(SubgroupObject.serialize(c)) + } + + async close() { + return this.stream.close() + } } export class SubgroupReader { - constructor( - public header: SubgroupHeader, - public stream: Reader, - ) { } - - async read(): Promise { - if (await this.stream.done()) { - return - } - - const object_id = await this.stream.getNumberVarInt() - - let extHeaders: KeyValuePairs | undefined - if (SubgroupType.isExtensionPresent(this.header.type)) { - const extHeadersBytesLength = await this.stream.getNumberVarInt() - const extHeadersData = await this.stream.read(extHeadersBytesLength) - extHeaders = KeyValuePairs.deserialize(new ImmutableBytesBuffer(extHeadersData)) - } - - console.log("subgroup header", object_id, extHeaders, this.stream) - - let obj_payload_len = await this.stream.getNumberVarInt() - - let object_payload: Uint8Array | undefined - let status: Status | undefined - - console.log("subgroup read", object_id, obj_payload_len) - - if (obj_payload_len == 0) { - status = Status.try_from(await this.stream.getNumberVarInt()) - } else { - object_payload = await this.stream.read(obj_payload_len) - } - - console.log("read success??", object_id, status, extHeaders, object_payload) - return { - object_id, - status, - extension_headers: extHeaders, - object_payload, - } - } - - async close() { - await this.stream.close() - } + constructor( + public header: SubgroupHeader, + public stream: Reader, + ) {} + + async read(): Promise { + if (await this.stream.done()) { + return + } + + const object_id = await this.stream.getNumberVarInt() + + let extHeaders: KeyValuePairs | undefined + if (SubgroupType.isExtensionPresent(this.header.type)) { + const extHeadersBytesLength = await this.stream.getNumberVarInt() + const extHeadersData = await this.stream.read(extHeadersBytesLength) + extHeaders = KeyValuePairs.deserialize(new ImmutableBytesBuffer(extHeadersData)) + } + + console.log("subgroup header", object_id, extHeaders, this.stream) + + let obj_payload_len = await this.stream.getNumberVarInt() + + let object_payload: Uint8Array | undefined + let status: Status | undefined + + console.log("subgroup read", object_id, obj_payload_len) + + if (obj_payload_len == 0) { + status = Status.try_from(await this.stream.getNumberVarInt()) + } else { + object_payload = await this.stream.read(obj_payload_len) + } + + console.log("read success??", object_id, status, extHeaders, object_payload) + return { + object_id, + status, + extension_headers: extHeaders, + object_payload, + } + } + + async close() { + await this.stream.close() + } } diff --git a/lib/transport/subscriber.ts b/lib/transport/subscriber.ts index 055d196..e2f9a6f 100644 --- a/lib/transport/subscriber.ts +++ b/lib/transport/subscriber.ts @@ -40,7 +40,7 @@ export class Subscriber { } async recv(msg: Control.MessageWithType) { - const { type, message } = msg; + const { type, message } = msg switch (type) { case Control.ControlMessageType.PublishNamespace: await this.recvPublishNamespace(message) @@ -69,7 +69,7 @@ export class Subscriber { await this.#control.send({ type: Control.ControlMessageType.RequestOk, - message: { id: msg.id, parameters: new Map() } + message: { id: msg.id, parameters: new Map() }, }) const publishNamespace = new PublishNamespaceRecv(this.#control, msg.namespace, msg.id) @@ -91,16 +91,20 @@ export class Subscriber { id, namespace, subscribe_options: Control.SubscribeOptions.BOTH, - } + }, } await this.#control.send(msg) } - async subscribe(namespace: string[], track: string, opts?: { - forward?: number, - subscriber_priority?: number, - group_order?: Control.GroupOrder, - }) { + async subscribe( + namespace: string[], + track: string, + opts?: { + forward?: number + subscriber_priority?: number + group_order?: Control.GroupOrder + }, + ) { const id = this.#control.nextRequestId() const subscribe = new SubscribeSend(this.#control, id, namespace, track) @@ -126,7 +130,7 @@ export class Subscriber { namespace, name: track, params, - } + }, } await this.#control.send(subscription_req) @@ -240,7 +244,7 @@ export class PublishNamespaceRecv { // Send the control message. return this.#control.send({ type: Control.ControlMessageType.RequestOk, - message: { id: this.#id, parameters: new Map() } + message: { id: this.#id, parameters: new Map() }, }) } @@ -250,7 +254,7 @@ export class PublishNamespaceRecv { return this.#control.send({ type: Control.ControlMessageType.RequestError, - message: { id: this.#id, code, retry_interval: 0n, reason } + message: { id: this.#id, code, retry_interval: 0n, reason }, }) } } diff --git a/lib/transport/tsconfig.json b/lib/transport/tsconfig.json index 38c67ea..88da906 100644 --- a/lib/transport/tsconfig.json +++ b/lib/transport/tsconfig.json @@ -1,11 +1,9 @@ { "extends": "../tsconfig.json", - "include": [ - "." - ], + "include": ["."], "references": [ { "path": "../common" } ] -} \ No newline at end of file +} diff --git a/lib/transport/utils.ts b/lib/transport/utils.ts index f658e06..f46577e 100644 --- a/lib/transport/utils.ts +++ b/lib/transport/utils.ts @@ -1,7 +1,7 @@ export function debug(...msg: any[]) { - console.log("itzmanish:", ...msg) + console.log("itzmanish:", ...msg) } export async function sleep(ms: number) { - return new Promise((resolve) => setTimeout(resolve, ms)) -} \ No newline at end of file + return new Promise((resolve) => setTimeout(resolve, ms)) +} diff --git a/lib/tsconfig.json b/lib/tsconfig.json index 5299650..08dc503 100644 --- a/lib/tsconfig.json +++ b/lib/tsconfig.json @@ -4,11 +4,7 @@ "moduleResolution": "node", "module": "es2022", "target": "es2022", - "lib": [ - "es2022", - "dom", - "dom.iterable" - ], + "lib": ["es2022", "dom", "dom.iterable"], "outDir": "./dist", "strict": true, "allowJs": false, @@ -18,7 +14,5 @@ "isolatedModules": false, "noEmit": false }, - "exclude": [ - "node_modules" - ] -} \ No newline at end of file + "exclude": ["node_modules"] +} diff --git a/lib/video-moq/index.ts b/lib/video-moq/index.ts index e16df1d..8a90c1a 100644 --- a/lib/video-moq/index.ts +++ b/lib/video-moq/index.ts @@ -378,20 +378,20 @@ export class VideoMoq extends HTMLElement { public play(): Promise { return this.player ? this.player.play().then(() => { - if (!this.#playButton) return - this.#playButton.innerHTML = PAUSE_SVG - this.#playButton.ariaLabel = "Pause" - }) + if (!this.#playButton) return + this.#playButton.innerHTML = PAUSE_SVG + this.#playButton.ariaLabel = "Pause" + }) : Promise.resolve() } public pause(): Promise { return this.player ? this.player.pause().then(() => { - if (!this.#playButton) return - this.#playButton.innerHTML = PLAY_SVG - this.#playButton.ariaLabel = "Play" - }) + if (!this.#playButton) return + this.#playButton.innerHTML = PLAY_SVG + this.#playButton.ariaLabel = "Play" + }) : Promise.resolve() } @@ -420,23 +420,23 @@ export class VideoMoq extends HTMLElement { public unmute(): Promise { return this.player ? this.player.mute(false).then(() => { - if (!this.#volumeButton) return - this.#volumeButton.ariaLabel = "Mute" - this.#volumeButton.innerText = "🔊" - this.#volumeRange!.value = this.previousVolume.toString() - }) + if (!this.#volumeButton) return + this.#volumeButton.ariaLabel = "Mute" + this.#volumeButton.innerText = "🔊" + this.#volumeRange!.value = this.previousVolume.toString() + }) : Promise.resolve() } public mute(): Promise { return this.player ? this.player.mute(true).then(() => { - if (!this.#volumeButton) return - this.#volumeButton.ariaLabel = "Unmute" - this.#volumeButton.innerText = "🔇" - this.previousVolume = parseFloat(this.#volumeRange!.value) - this.#volumeRange!.value = "0" - }) + if (!this.#volumeButton) return + this.#volumeButton.ariaLabel = "Unmute" + this.#volumeButton.innerText = "🔇" + this.previousVolume = parseFloat(this.#volumeRange!.value) + this.#volumeRange!.value = "0" + }) : Promise.resolve() } From 41d9b790a2e4b7355736aa9fb0696c1db7f16b8e Mon Sep 17 00:00:00 2001 From: Manish Date: Fri, 22 May 2026 18:41:19 +0530 Subject: [PATCH 04/16] Implement draft-16 transport updates and playback fixes - Add draft-16 WebTransport protocol negotiation and request ID flow control - Align control/data wire helpers with draft-16 parameter and extension encoding - Add subscription filter serialization and expose filter options on subscribe - Fix playback startup so media subscriptions begin after catalog/init readiness - Incorporate browser audio decoder and player lifecycle fixes - Add focused transport wire-format tests --- demo/index.html | 14 +- demo/player.html | 6 +- demo/server.cjs | 41 +- lib/media/catalog/index.ts | 2 +- lib/package.json | 1 + lib/playback/backend.ts | 2 +- lib/playback/index.ts | 32 +- lib/playback/worker/audio.ts | 146 ++++- lib/playback/worker/index.ts | 48 ++ lib/playback/worker/timeline.ts | 8 + lib/playback/worker/video.ts | 10 +- lib/playback/worklet/index.ts | 10 +- lib/test/transport.cjs | 557 ++++++++++++++++++ lib/transport/base_data.ts | 38 +- lib/transport/client.ts | 40 +- lib/transport/connection.ts | 48 +- lib/transport/control/client_setup.ts | 6 +- lib/transport/control/fetch.ts | 33 +- lib/transport/control/fetch_cancel.ts | 2 +- lib/transport/control/fetch_ok.ts | 13 +- lib/transport/control/go_away.ts | 2 +- lib/transport/control/index.ts | 117 +--- lib/transport/control/max_request_id.ts | 2 +- lib/transport/control/message_type.ts | 108 ++++ lib/transport/control/namespace.ts | 2 +- lib/transport/control/namespace_done.ts | 2 +- lib/transport/control/publish.ts | 15 +- lib/transport/control/publish_done.ts | 2 +- lib/transport/control/publish_namespace.ts | 8 +- .../control/publish_namespace_cancel.ts | 2 +- .../control/publish_namespace_done.ts | 2 +- lib/transport/control/publish_ok.ts | 3 +- lib/transport/control/request_error.ts | 2 +- lib/transport/control/request_ok.ts | 6 +- lib/transport/control/requests_blocked.ts | 2 +- lib/transport/control/server_setup.ts | 6 +- lib/transport/control/subscribe.ts | 47 +- lib/transport/control/subscribe_namespace.ts | 12 +- lib/transport/control/subscribe_ok.ts | 24 +- lib/transport/control/subscribe_update.ts | 6 +- lib/transport/control/track_status.ts | 6 +- lib/transport/control/unsubscribe.ts | 2 +- lib/transport/index.ts | 2 + lib/transport/object_status.ts | 34 ++ lib/transport/objects.ts | 54 +- lib/transport/publisher.ts | 16 +- lib/transport/request_id.ts | 78 +++ lib/transport/stream.ts | 45 +- lib/transport/subgroup.ts | 69 ++- lib/transport/subscriber.ts | 66 ++- lib/video-moq/index.ts | 22 +- package.json | 5 +- scripts/sync-demo-lib.mjs | 24 + 53 files changed, 1479 insertions(+), 371 deletions(-) create mode 100644 lib/test/transport.cjs create mode 100644 lib/transport/control/message_type.ts create mode 100644 lib/transport/object_status.ts create mode 100644 lib/transport/request_id.ts create mode 100644 scripts/sync-demo-lib.mjs diff --git a/demo/index.html b/demo/index.html index f707373..d567afa 100644 --- a/demo/index.html +++ b/demo/index.html @@ -77,6 +77,8 @@

How it works

+ + + +

MoQ Player — getStats() Demo

+
+
+ +
+ + + + + + +
+
Idle — press Play to start.
+
+
+
Stats will appear here once playback starts.
+
+
+
+ + + + diff --git a/samples/index.html b/samples/index.html index 295e580..c8ac782 100644 --- a/samples/index.html +++ b/samples/index.html @@ -82,6 +82,13 @@

MoQ Player - Samples

interactions.

+
  • + getStats() - Example +

    + Live stats overlay using Player.getStats() — shows transport, decoder, render, audio, and + timeline metrics updated every second. +

    +
  • From 5092add9686cab39f53731f40389563b35d35609 Mon Sep 17 00:00:00 2001 From: Manish Date: Tue, 23 Jun 2026 20:23:40 +0530 Subject: [PATCH 16/16] fix(publisher): support repeated dynamic tracks --- draft-14-moq.txt | 5712 +++++++++++++++++++++++++++ draft-16.txt | 6104 +++++++++++++++++++++++++++++ publisher/contribute/broadcast.ts | 39 +- publisher/contribute/track.ts | 9 +- publisher/publish/index.ts | 7 +- 5 files changed, 11861 insertions(+), 10 deletions(-) create mode 100644 draft-14-moq.txt create mode 100644 draft-16.txt diff --git a/draft-14-moq.txt b/draft-14-moq.txt new file mode 100644 index 0000000..478c66d --- /dev/null +++ b/draft-14-moq.txt @@ -0,0 +1,5712 @@ + + + + +Media Over QUIC S. Nandakumar +Internet-Draft Cisco +Intended status: Standards Track V. Vasiliev +Expires: 6 March 2026 I. Swett, Ed. + Google + A. Frindell, Ed. + Meta + 2 September 2025 + + + Media over QUIC Transport + draft-ietf-moq-transport-14 + +Abstract + + This document defines the core behavior for Media over QUIC Transport + (MOQT), a media transport protocol designed to operate over QUIC and + WebTransport, which have similar functionality. MOQT allows a + producer of media to publish data and have it consumed via + subscription by a multiplicity of endpoints. It supports + intermediate content distribution networks and is designed for high + scale and low latency distribution. + +About This Document + + This note is to be removed before publishing as an RFC. + + The latest revision of this draft can be found at https://moq- + wg.github.io/moq-transport/draft-ietf-moq-transport.html. Status + information for this document may be found at + https://datatracker.ietf.org/doc/draft-ietf-moq-transport/. + + Discussion of this document takes place on the Media Over QUIC + Working Group mailing list (mailto:moq@ietf.org), which is archived + at https://mailarchive.ietf.org/arch/browse/moq/. Subscribe at + https://www.ietf.org/mailman/listinfo/moq/. + + Source for this draft and an issue tracker can be found at + https://github.com/moq-wg/moq-transport. + +Status of This Memo + + This Internet-Draft is submitted in full conformance with the + provisions of BCP 78 and BCP 79. + + + + + + + +Nandakumar, et al. Expires 6 March 2026 [Page 1] + +Internet-Draft moq-transport September 2025 + + + Internet-Drafts are working documents of the Internet Engineering + Task Force (IETF). Note that other groups may also distribute + working documents as Internet-Drafts. The list of current Internet- + Drafts is at https://datatracker.ietf.org/drafts/current/. + + Internet-Drafts are draft documents valid for a maximum of six months + and may be updated, replaced, or obsoleted by other documents at any + time. It is inappropriate to use Internet-Drafts as reference + material or to cite them other than as "work in progress." + + This Internet-Draft will expire on 6 March 2026. + +Copyright Notice + + Copyright (c) 2025 IETF Trust and the persons identified as the + document authors. All rights reserved. + + This document is subject to BCP 78 and the IETF Trust's Legal + Provisions Relating to IETF Documents (https://trustee.ietf.org/ + license-info) in effect on the date of publication of this document. + Please review these documents carefully, as they describe your rights + and restrictions with respect to this document. Code Components + extracted from this document must include Revised BSD License text as + described in Section 4.e of the Trust Legal Provisions and are + provided without warranty as described in the Revised BSD License. + +Table of Contents + + 1. Introduction . . . . . . . . . . . . . . . . . . . . . . . . 5 + 1.1. Motivation . . . . . . . . . . . . . . . . . . . . . . . 6 + 1.1.1. Latency . . . . . . . . . . . . . . . . . . . . . . . 6 + 1.1.2. Leveraging QUIC . . . . . . . . . . . . . . . . . . . 6 + 1.1.3. Convergence . . . . . . . . . . . . . . . . . . . . . 6 + 1.1.4. Relays . . . . . . . . . . . . . . . . . . . . . . . 7 + 1.2. Terms and Definitions . . . . . . . . . . . . . . . . . . 7 + 1.3. Stream Management Terms . . . . . . . . . . . . . . . . . 8 + 1.4. Notational Conventions . . . . . . . . . . . . . . . . . 8 + 1.4.1. Location Structure . . . . . . . . . . . . . . . . . 9 + 1.4.2. Key-Value-Pair Structure . . . . . . . . . . . . . . 9 + 1.4.3. Reason Phrase Structure . . . . . . . . . . . . . . . 10 + 2. Object Data Model . . . . . . . . . . . . . . . . . . . . . . 10 + 2.1. Objects . . . . . . . . . . . . . . . . . . . . . . . . . 11 + 2.2. Subgroups . . . . . . . . . . . . . . . . . . . . . . . . 11 + 2.3. Groups . . . . . . . . . . . . . . . . . . . . . . . . . 12 + 2.3.1. Group IDs . . . . . . . . . . . . . . . . . . . . . . 13 + 2.4. Track . . . . . . . . . . . . . . . . . . . . . . . . . . 13 + 2.4.1. Track Naming . . . . . . . . . . . . . . . . . . . . 13 + 2.5. Malformed Tracks . . . . . . . . . . . . . . . . . . . . 14 + + + +Nandakumar, et al. Expires 6 March 2026 [Page 2] + +Internet-Draft moq-transport September 2025 + + + 2.5.1. Scope . . . . . . . . . . . . . . . . . . . . . . . . 15 + 3. Sessions . . . . . . . . . . . . . . . . . . . . . . . . . . 16 + 3.1. Session establishment . . . . . . . . . . . . . . . . . . 16 + 3.1.1. WebTransport . . . . . . . . . . . . . . . . . . . . 16 + 3.1.2. QUIC . . . . . . . . . . . . . . . . . . . . . . . . 16 + 3.1.3. Connection URL . . . . . . . . . . . . . . . . . . . 17 + 3.2. Version and Extension Negotiation . . . . . . . . . . . . 17 + 3.3. Session initialization . . . . . . . . . . . . . . . . . 18 + 3.4. Termination . . . . . . . . . . . . . . . . . . . . . . . 18 + 3.5. Migration . . . . . . . . . . . . . . . . . . . . . . . . 20 + 3.6. Congestion Control . . . . . . . . . . . . . . . . . . . 20 + 3.6.1. Bufferbloat . . . . . . . . . . . . . . . . . . . . . 21 + 3.6.2. Application-Limited . . . . . . . . . . . . . . . . . 21 + 3.6.3. Consistent Throughput . . . . . . . . . . . . . . . . 21 + 4. Modularity . . . . . . . . . . . . . . . . . . . . . . . . . 21 + 5. Publishing and Retrieving Tracks . . . . . . . . . . . . . . 22 + 5.1. Subscriptions . . . . . . . . . . . . . . . . . . . . . . 22 + 6. Namespace Discovery . . . . . . . . . . . . . . . . . . . . . 23 + 6.1. Subscribing to Namespaces . . . . . . . . . . . . . . . . 24 + 6.2. Publishing Namespaces . . . . . . . . . . . . . . . . . . 24 + 7. Priorities . . . . . . . . . . . . . . . . . . . . . . . . . 25 + 7.1. Definitions . . . . . . . . . . . . . . . . . . . . . . . 25 + 7.2. Scheduling Algorithm . . . . . . . . . . . . . . . . . . 26 + 7.3. Considerations for Setting Priorities . . . . . . . . . . 27 + 8. Relays . . . . . . . . . . . . . . . . . . . . . . . . . . . 28 + 8.1. Caching Relays . . . . . . . . . . . . . . . . . . . . . 28 + 8.2. Multiple Publishers . . . . . . . . . . . . . . . . . . . 29 + 8.3. Subscriber Interactions . . . . . . . . . . . . . . . . . 29 + 8.3.1. Graceful Subscriber Relay Switchover . . . . . . . . 30 + 8.4. Publisher Interactions . . . . . . . . . . . . . . . . . 30 + 8.4.1. Graceful Publisher Network Switchover . . . . . . . . 32 + 8.4.2. Graceful Publisher Relay Switchover . . . . . . . . . 33 + 8.5. Relay Object Handling . . . . . . . . . . . . . . . . . . 33 + 9. Control Messages . . . . . . . . . . . . . . . . . . . . . . 33 + 9.1. Request ID . . . . . . . . . . . . . . . . . . . . . . . 35 + 9.2. Parameters . . . . . . . . . . . . . . . . . . . . . . . 36 + 9.2.1. Version Specific Parameters . . . . . . . . . . . . . 36 + 9.3. CLIENT_SETUP and SERVER_SETUP . . . . . . . . . . . . . . 41 + 9.3.1. Versions . . . . . . . . . . . . . . . . . . . . . . 42 + 9.3.2. Setup Parameters . . . . . . . . . . . . . . . . . . 42 + 9.4. GOAWAY . . . . . . . . . . . . . . . . . . . . . . . . . 44 + 9.5. MAX_REQUEST_ID . . . . . . . . . . . . . . . . . . . . . 45 + 9.6. REQUESTS_BLOCKED . . . . . . . . . . . . . . . . . . . . 46 + 9.7. SUBSCRIBE . . . . . . . . . . . . . . . . . . . . . . . . 46 + 9.8. SUBSCRIBE_OK . . . . . . . . . . . . . . . . . . . . . . 49 + 9.9. SUBSCRIBE_ERROR . . . . . . . . . . . . . . . . . . . . . 51 + 9.10. SUBSCRIBE_UPDATE . . . . . . . . . . . . . . . . . . . . 52 + 9.11. UNSUBSCRIBE . . . . . . . . . . . . . . . . . . . . . . . 53 + + + +Nandakumar, et al. Expires 6 March 2026 [Page 3] + +Internet-Draft moq-transport September 2025 + + + 9.12. PUBLISH_DONE . . . . . . . . . . . . . . . . . . . . . . 54 + 9.13. PUBLISH . . . . . . . . . . . . . . . . . . . . . . . . . 56 + 9.14. PUBLISH_OK . . . . . . . . . . . . . . . . . . . . . . . 57 + 9.15. PUBLISH_ERROR . . . . . . . . . . . . . . . . . . . . . . 58 + 9.16. FETCH . . . . . . . . . . . . . . . . . . . . . . . . . . 59 + 9.16.1. Standalone Fetch . . . . . . . . . . . . . . . . . . 60 + 9.16.2. Joining Fetches . . . . . . . . . . . . . . . . . . 60 + 9.16.3. Fetch Handling . . . . . . . . . . . . . . . . . . . 61 + 9.17. FETCH_OK . . . . . . . . . . . . . . . . . . . . . . . . 63 + 9.18. FETCH_ERROR . . . . . . . . . . . . . . . . . . . . . . . 65 + 9.19. FETCH_CANCEL . . . . . . . . . . . . . . . . . . . . . . 66 + 9.20. TRACK_STATUS . . . . . . . . . . . . . . . . . . . . . . 66 + 9.21. TRACK_STATUS_OK . . . . . . . . . . . . . . . . . . . . . 67 + 9.22. TRACK_STATUS_ERROR . . . . . . . . . . . . . . . . . . . 67 + 9.23. PUBLISH_NAMESPACE . . . . . . . . . . . . . . . . . . . . 67 + 9.24. PUBLISH_NAMESPACE_OK . . . . . . . . . . . . . . . . . . 68 + 9.25. PUBLISH_NAMESPACE_ERROR . . . . . . . . . . . . . . . . . 68 + 9.26. PUBLISH_NAMESPACE_DONE . . . . . . . . . . . . . . . . . 69 + 9.27. PUBLISH_NAMESPACE_CANCEL . . . . . . . . . . . . . . . . 70 + 9.28. SUBSCRIBE_NAMESPACE . . . . . . . . . . . . . . . . . . . 70 + 9.29. SUBSCRIBE_NAMESPACE_OK . . . . . . . . . . . . . . . . . 71 + 9.30. SUBSCRIBE_NAMESPACE_ERROR . . . . . . . . . . . . . . . . 72 + 9.31. UNSUBSCRIBE_NAMESPACE . . . . . . . . . . . . . . . . . . 73 + 10. Data Streams and Datagrams . . . . . . . . . . . . . . . . . 73 + 10.1. Track Alias . . . . . . . . . . . . . . . . . . . . . . 74 + 10.2. Objects . . . . . . . . . . . . . . . . . . . . . . . . 74 + 10.2.1. Canonical Object Properties . . . . . . . . . . . . 74 + 10.3. Datagrams . . . . . . . . . . . . . . . . . . . . . . . 77 + 10.3.1. Object Datagram . . . . . . . . . . . . . . . . . . 77 + 10.4. Streams . . . . . . . . . . . . . . . . . . . . . . . . 79 + 10.4.1. Stream Cancellation . . . . . . . . . . . . . . . . 79 + 10.4.2. Subgroup Header . . . . . . . . . . . . . . . . . . 79 + 10.4.3. Closing Subgroup Streams . . . . . . . . . . . . . . 82 + 10.4.4. Fetch Header . . . . . . . . . . . . . . . . . . . . 85 + 10.5. Examples . . . . . . . . . . . . . . . . . . . . . . . . 86 + 11. Extension Headers . . . . . . . . . . . . . . . . . . . . . . 87 + 11.1. Prior Group ID Gap . . . . . . . . . . . . . . . . . . . 87 + 11.2. Immutable Extensions . . . . . . . . . . . . . . . . . . 88 + 11.3. Prior Object ID Gap . . . . . . . . . . . . . . . . . . 89 + 12. Security Considerations . . . . . . . . . . . . . . . . . . . 89 + 12.1. Resource Exhaustion . . . . . . . . . . . . . . . . . . 90 + 12.2. Timeouts . . . . . . . . . . . . . . . . . . . . . . . . 90 + 12.3. Relay security considerations . . . . . . . . . . . . . 90 + 12.3.1. State maintenance . . . . . . . . . . . . . . . . . 90 + 12.3.2. SUBSCRIBE_NAMESPACE with short prefixes . . . . . . 90 + 13. IANA Considerations . . . . . . . . . . . . . . . . . . . . . 91 + 13.1. Error Codes . . . . . . . . . . . . . . . . . . . . . . 91 + 13.1.1. Session Termination Error Codes . . . . . . . . . . 91 + + + +Nandakumar, et al. Expires 6 March 2026 [Page 4] + +Internet-Draft moq-transport September 2025 + + + 13.1.2. SUBSCRIBE_ERROR Codes . . . . . . . . . . . . . . . 92 + 13.1.3. PUBLISH_DONE Codes . . . . . . . . . . . . . . . . . 93 + 13.1.4. PUBLISH_ERROR Codes . . . . . . . . . . . . . . . . 93 + 13.1.5. FETCH_ERROR Codes . . . . . . . . . . . . . . . . . 94 + 13.1.6. ANNOUNCE_ERROR Codes . . . . . . . . . . . . . . . . 94 + 13.1.7. SUBSCRIBE_NAMESPACE_ERROR Codes . . . . . . . . . . 95 + 13.1.8. Data Stream Reset Error Codes . . . . . . . . . . . 95 + Contributors . . . . . . . . . . . . . . . . . . . . . . . . . . 96 + References . . . . . . . . . . . . . . . . . . . . . . . . . . . 96 + Normative References . . . . . . . . . . . . . . . . . . . . . 96 + Informative References . . . . . . . . . . . . . . . . . . . . 97 + Appendix A. Change Log . . . . . . . . . . . . . . . . . . . . . 98 + A.1. Since draft-ietf-moq-transport-13 . . . . . . . . . . . . 98 + A.2. Since draft-ietf-moq-transport-12 . . . . . . . . . . . . 100 + A.3. Since draft-ietf-moq-transport-11 . . . . . . . . . . . . 100 + A.4. Since draft-ietf-moq-transport-10 . . . . . . . . . . . . 101 + Authors' Addresses . . . . . . . . . . . . . . . . . . . . . . . 102 + +1. Introduction + + Media Over QUIC Transport (MOQT) is a protocol that is optimized for + the QUIC protocol [QUIC], either directly or via WebTransport + [WebTransport], for the dissemination of media. MOQT utilizes a + publish/subscribe workflow in which producers of media publish data + in response to subscription requests from a multiplicity of + endpoints. MOQT supports wide range of use-cases with different + resiliency and latency (live, interactive) needs without compromising + the scalability and cost effectiveness associated with content + delivery networks. + + MOQT is a generic protocol designed to work in concert with multiple + MoQ Streaming Formats. These MoQ Streaming Formats define how + content is encoded, packaged, and mapped to MOQT objects, along with + policies for discovery and subscription. + + * Section 2 describes the data model employed by MOQT. + + * Section 3 covers aspects of setting up an MOQT session. + + * Section 7 covers mechanisms for prioritizing subscriptions. + + * Section 8 covers behavior at the relay entities. + + * Section 9 covers how control messages are encoded on the wire. + + * Section 10 covers how data messages are encoded on the wire. + + + + + +Nandakumar, et al. Expires 6 March 2026 [Page 5] + +Internet-Draft moq-transport September 2025 + + +1.1. Motivation + + The development of MOQT is driven by goals in a number of areas - + specifically latency, the robust feature set of QUIC and relay + support. + +1.1.1. Latency + + Latency is necessary to correct for variable network throughput. + Ideally live content is consumed at the same bitrate it is produced. + End-to-end latency would be fixed and only subject to encoding and + transmission delays. Unfortunately, networks have variable + throughput, primarily due to congestion. Attempting to deliver + content encoded at a higher bitrate than the network can cause + queuing along the path from producer to consumer. The speed at which + a protocol can detect and respond to congestion determines the + overall latency. TCP-based protocols are simple but are slow to + detect congestion and suffer from head-of-line blocking. Protocols + utilizing UDP directly can avoid queuing, but the application is then + responsible for the complexity of fragmentation, congestion control, + retransmissions, receiver feedback, reassembly, and more. One goal + of MOQT is to achieve the best of both these worlds: leverage the + features of QUIC to create a simple yet flexible low latency protocol + that can rapidly detect and respond to congestion. + +1.1.2. Leveraging QUIC + + The parallel nature of QUIC streams can provide improvements in the + face of loss. A goal of MOQT is to design a streaming protocol to + leverage the transmission benefits afforded by parallel QUIC streams + as well exercising options for flexible loss recovery. + +1.1.3. Convergence + + Some live media architectures today have separate protocols for + ingest and distribution, for example RTMP and HTTP based HLS or DASH. + Switching protocols necessitates intermediary origins which re- + package the media content. While specialization can have its + benefits, there are efficiency gains to be had in not having to re- + package content. A goal of MOQT is to develop a single protocol + which can be used for transmission from contribution to distribution. + A related goal is the ability to support existing encoding and + packaging schemas, both for backwards compatibility and for + interoperability with the established content preparation ecosystem. + + + + + + + +Nandakumar, et al. Expires 6 March 2026 [Page 6] + +Internet-Draft moq-transport September 2025 + + +1.1.4. Relays + + An integral feature of a protocol being successful is its ability to + deliver media at scale. Greatest scale is achieved when third-party + networks, independent of both the publisher and subscriber, can be + leveraged to relay the content. These relays must cache content for + distribution efficiency while simultaneously routing content and + deterministically responding to congestion in a multi-tenant network. + A goal of MOQT is to treat relays as first-class citizens of the + protocol and ensure that objects are structured such that information + necessary for distribution is available to relays while the media + content itself remains opaque and private. + +1.2. Terms and Definitions + + The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", + "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and + "OPTIONAL" in this document are to be interpreted as described in + BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all + capitals, as shown here. + + The following terms are used with the first letter capitalized. + + Application: The entity using MOQT to transmit and receive data. + + Client: The party initiating a Transport Session. + + Server: The party accepting an incoming Transport Session. + + Endpoint: A Client or Server. + + Peer: The other endpoint than the one being described + + Publisher: An endpoint that handles subscriptions by sending + requested Objects from the requested track. + + Subscriber: An endpoint that subscribes to and receives tracks. + + Original Publisher: The initial publisher of a given track. + + End Subscriber: A subscriber that initiates a subscription and does + not send the data on to other subscribers. + + Relay: An entity that is both a Publisher and a Subscriber, is not + the Original Publisher or End Subscriber, and conforms to all + requirements in Section 8. + + Upstream: In the direction of the Original Publisher + + + +Nandakumar, et al. Expires 6 March 2026 [Page 7] + +Internet-Draft moq-transport September 2025 + + + Downstream: In the direction of the End Subscriber(s) + + Transport Session: A raw QUIC connection or a WebTransport session. + + Stream: A bidirectional or unidirectional bytestream provided by the + QUIC transport or WebTransport. + + Congestion: Packet loss and queuing caused by degraded or overloaded + networks. + + Group: A temporal sequence of objects. A group represents a join + point in a track. See (Section 2.3). + + Object: An object is an addressable unit whose payload is a sequence + of bytes. Objects form the base element in the MOQT data model. + See (Section 2.1). + + Track: A track is a collection of groups. See (Section 2.4). + +1.3. Stream Management Terms + + This document uses stream management terms described in [RFC9000], + Section 1.3 including STOP_SENDING, RESET_STREAM and FIN. + +1.4. Notational Conventions + + This document uses the conventions detailed in ([RFC9000], + Section 1.3) when describing the binary encoding. + + As a quick reference, the following list provides a non normative + summary of the parts of RFC9000 field syntax that are used in this + specification. + + x (L): Indicates that x is L bits long + + x (i): Indicates that x holds an integer value using the variable- + length encoding as described in ([RFC9000], Section 16) + + x (..): Indicates that x can be any length including zero bits long. + Values in this format always end on a byte boundary. + + [x (L)]: Indicates that x is optional and has a length of L + + x (L) ...: Indicates that x is repeated zero or more times and that + each instance has a length of L + + This document extends the RFC9000 syntax and with the additional + field types: + + + +Nandakumar, et al. Expires 6 March 2026 [Page 8] + +Internet-Draft moq-transport September 2025 + + + x (b): Indicates that x consists of a variable length integer + encoding as described in ([RFC9000], Section 16), followed by that + many bytes of binary data + + x (tuple): Indicates that x is a tuple, consisting of a variable + length integer encoded as described in ([RFC9000], Section 16), + followed by that many variable length tuple fields, each of which + are encoded as (b) above. + + To reduce unnecessary use of bandwidth, variable length integers + SHOULD be encoded using the least number of bytes possible to + represent the required value. + +1.4.1. Location Structure + + Location identifies a particular Object in a Group within a Track. + + Location { + Group (i), + Object (i) + } + + Figure 1: Location structure + + In this document, the constituent parts of any Location A can be + referred to using A.Group or A.Object. + + Location A < Location B if: + + A.Group < B.Group || (A.Group == B.Group && A.Object < B.Object) + +1.4.2. Key-Value-Pair Structure + + Key-Value-Pair is a flexible structure designed to carry key/value + pairs in which the key is a variable length integer and the value is + either a variable length integer or a byte field of arbitrary length. + + Key-Value-Pair is used in both the data plane and control plane, but + is optimized for use in the data plane. + + Key-Value-Pair { + Type (i), + [Length (i),] + Value (..) + } + + Figure 2: MOQT Key-Value-Pair + + + + +Nandakumar, et al. Expires 6 March 2026 [Page 9] + +Internet-Draft moq-transport September 2025 + + + * Type: an unsigned integer, encoded as a varint, identifying the + type of the value and also the subsequent serialization. + + * Length: Only present when Type is odd. Specifies the length of + the Value field. The maximum length of a value is 2^16-1 bytes. + If an endpoint receives a length larger than the maximum, it MUST + close the session with a Protocol Violation. + + * Value: A single varint encoded value when Type is even, otherwise + a sequence of Length bytes. + + If a receiver understands a Type, and the following Value or Length/ + Value does not match the serialization defined by that Type, the + receiver MUST terminate the session with error code + KEY_VALUE_FORMATTING_ERROR. + +1.4.3. Reason Phrase Structure + + Reason Phrase provides a way for the sender to encode additional + diagnostic information about the error condition, where appropriate. + + Reason Phrase { + Reason Phrase Length (i), + Reason Phrase Value (..) + } + + * Reason Phrase Length: A variable-length integer specifying the + length of the reason phrase in bytes. The reason phrase length + has a maximum length of 1024 bytes. If an endpoint receives a + length exceeding the maximum, it MUST close the session with a + PROTOCOL_VIOLATION + + * Reason Phrase Value: Additional diagnostic information about the + error condition. The reason phrase value is encoded as UTF-8 + string and does not carry information, such as language tags, that + would aid comprehension by any entity other than the one that + created the text. + +2. Object Data Model + + MOQT has a hierarchical data model, comprised of tracks which contain + groups, and groups that contain objects. Inside of a group, the + objects can be organized into subgroups. + + To give an example of how an application might use this data model, + consider an application sending high and low resolution video using a + codec with temporal scalability. Each resolution is sent as a + separate track to allow the subscriber to pick the appropriate + + + +Nandakumar, et al. Expires 6 March 2026 [Page 10] + +Internet-Draft moq-transport September 2025 + + + resolution given the display environment and available bandwidth. + Each independently coded sequence of pictures in a resolution is sent + as a group as the first picture in the sequence can be used as a + random access point. This allows the client to join at the logical + points where decoding of the media can start without needing + information before the join points. The temporal layers are sent as + separate subgroups to allow the priority mechanism to favor lower + temporal layers when there is not enough bandwidth to send all + temporal layers. Each frame of video is sent as a single object. + +2.1. Objects + + The basic data element of MOQT is an object. An object is an + addressable unit whose payload is a sequence of bytes. All objects + belong to a group, indicating ordering and potential dependencies + (see Section 2.3). An object is uniquely identified by its track + namespace, track name, group ID, and object ID, and must be an + identical sequence of bytes regardless of how or where it is + retrieved. An Object can become unavailable, but its contents MUST + NOT change over time. + + Objects are comprised of two parts: metadata and a payload. The + metadata is never encrypted and is always visible to relays (see + Section 8). The payload portion may be encrypted, in which case it + is only visible to the Original Publisher and End Subscribers. The + Original Publisher is solely responsible for the content of the + object payload. This includes the underlying encoding, compression, + any end-to-end encryption, or authentication. A relay MUST NOT + combine, split, or otherwise modify object payloads. + + Objects within a Group are ordered numerically by their Object ID. + +2.2. Subgroups + + A subgroup is a sequence of one or more objects from the same group + (Section 2.3) in ascending order by Object ID. Objects in a subgroup + have a dependency and priority relationship consistent with sharing a + stream and are sent on a single stream whenever possible. A Group is + delivered using at least as many streams as there are Subgroups, + typically with a one-to-one mapping between Subgroups and streams. + + When a Track's forwarding preference (see Section 10.2.1) is + "Datagram", Objects are not sent in Subgroups and the description in + the remainder of this section does not apply. + + + + + + + +Nandakumar, et al. Expires 6 March 2026 [Page 11] + +Internet-Draft moq-transport September 2025 + + + Streams offer in-order reliable delivery and the ability to cancel + sending and retransmission of data. Furthermore, many + implementations offer the ability to control the relative priority of + streams, which allows control over the scheduling of sending data on + active streams. + + Every object within a Group belongs to exactly one Subgroup. + + Objects from two subgroups cannot be sent on the same stream. + Objects from the same Subgroup MUST NOT be sent on different streams, + unless one of the streams was reset prematurely, or upstream + conditions have forced objects from a Subgroup to be sent out of + Object ID order. + + Original publishers assign each Subgroup a Subgroup ID, and do so as + they see fit. The scope of a Subgroup ID is a Group, so Subgroups + from different Groups MAY share a Subgroup ID without implying any + relationship between them. In general, publishers assign objects to + subgroups in order to leverage the features of streams as described + above. + + In general, if Object B is dependent on Object A, then delivery of B + can follow A, i.e. A and B can be usefully delivered over a single + stream. If an Object is dependent on all previous Objects in a + Subgroup, it likely fits best in that Subgroup. If an Object is not + dependent on any of the Objects in a Subgroup, it likely belongs in a + different Subgroup. + + When assigning Objects to different Subgroups, the Original Publisher + makes a reasonable tradeoff between having an optimal mapping of + Object relationships in a Group and minimizing the number of streams + used. + +2.3. Groups + + A group is a collection of Objects and is a sub-unit of a Track + (Section 2.4). Groups SHOULD be independently useful, so Objects + within a Group SHOULD NOT depend on Objects in other Groups. A Group + provides a join point for subscriptions, so a subscriber that does + not want to receive the entire Track can opt to receive only Groups + starting from a given Group ID. Groups can contain any number of + Objects. + + + + + + + + + +Nandakumar, et al. Expires 6 March 2026 [Page 12] + +Internet-Draft moq-transport September 2025 + + +2.3.1. Group IDs + + Within a track, the original publisher SHOULD publish Group IDs which + increase with time (where "time" is defined according to the internal + clock of the media being sent). In some cases, Groups will be + produced in increasing order, but sent to subscribers in a different + order, for example when the subscription's Group Order is Descending. + Due to network reordering and the partial reliability features of + MOQT, Groups can always be received out of order. + + As a result, subscribers cannot infer the existence of a Group until + an object in the Group is received. This can create gaps in a cache + that can be filled by doing a Fetch upstream, if necessary. + + Applications that cannot produce Group IDs that increase with time + are limited to the subset of MOQT that does not compare group IDs. + Subscribers to these Tracks SHOULD NOT use range filters which span + multiple Groups in FETCH or SUBSCRIBE. SUBSCRIBE and FETCH delivery + use Group Order, so a FETCH cannot deliver Groups out of order and a + subscription could have unexpected delivery order if Group IDs do not + increase with time. + + Note that the increase in time between two groups is not defined by + the protocol. + +2.4. Track + + A track is a sequence of groups (Section 2.3). It is the entity + against which a subscriber issues a subscription request. A + subscriber can request to receive individual tracks starting at a + group boundary, including any new objects pushed by the publisher + while the track is active. + +2.4.1. Track Naming + + In MOQT, every track is identified by a Full Track Name, consisting + of a Track Namespace and a Track Name. + + Track Namespace is an ordered N-tuple of bytes where N can be between + 1 and 32. The structured nature of Track Namespace allows relays and + applications to manipulate prefixes of a namespace. If an endpoint + receives a Track Namespace tuple with an N of 0 or more than 32, it + MUST close the session with a Protocol Violation. + + Track Name is a sequence of bytes that identifies an individual track + within the namespace. + + + + + +Nandakumar, et al. Expires 6 March 2026 [Page 13] + +Internet-Draft moq-transport September 2025 + + + The maximum total length of a Full Track Name is 4,096 bytes, + computed as the sum of the lengths of each Track Namespace tuple + field and the Track Name length field. If an endpoint receives a + Full Track Name exceeding this length, it MUST close the session with + a PROTOCOL_VIOLATION. + + In this specification, both the Track Namespace tuple fields and the + Track Name are not constrained to a specific encoding. They carry a + sequence of bytes and comparison between two Track Namespace tuple + fields or Track Names is done by exact comparison of the bytes. + Specifications that use MOQT may constrain the information in these + fields, for example by restricting them to UTF-8. Any such + specification needs to specify the canonicalization into the bytes in + the Track Namespace or Track Name such that exact comparison works. + +2.5. Malformed Tracks + + There are multiple ways a publisher can transmit a Track that does + not conform to MOQT constraints. Such a Track is considered + malformed. Some example conditions that constitute a malformed track + when detected by a receiver include: + + 1. An Object is received in a FETCH response with the same Group as + the previous Object, but whose Object ID is not strictly larger + than the previous object. + + 2. An Object is received in an Ascending FETCH response whose Group + ID is smaller than the previous Object in the response. + + 3. An Object is received in a Descending FETCH response whose Group + ID is larger than the previous Object in the resopnse. + + 4. An Object is received whose Object ID is larger than the final + Object in the Subgroup. The final Object in a Subgroup is the + last Object received on a Subgroup stream before a FIN. + + 5. A Subgroup is received with two or more different final Objects. + + 6. An Object is received in a Group whose Object ID is larger than + the final Object in the Group. The final Object in a Group is + the Object with Status END_OF_GROUP or the last Object sent in a + FETCH that requested the entire Group. + + 7. An Object is received on a Track whose Group and Object ID are + larger than the final Object in the Track. The final Object in a + Track is the Object with Status END_OF_TRACK or the last Object + sent in a FETCH whose response indicated End of Track. + + + + +Nandakumar, et al. Expires 6 March 2026 [Page 14] + +Internet-Draft moq-transport September 2025 + + + 8. The same Object is received more than once with different Payload + or other immutable properties. + + 9. An Object is received with a different Forwarding Preference than + previously observed from the same Track. + + The above list of conditions is not considered exhaustive. + + When a subscriber detects a Malformed Track, it MUST UNSUBSCRIBE any + subscription and FETCH_CANCEL any fetch for that Track from that + publisher, and SHOULD deliver an error to the application. If a + relay detects a Malformed Track, it MUST immediately terminate + downstream subscriptions with PUBLISH_DONE and reset any fetch + streams with Status Code MALFORMED_TRACK. + +2.5.1. Scope + + An MOQT scope is a set of servers (as identified by their connection + URIs) for which the tuple of Track Namespace and Track Name are + guaranteed to be unique and identify a specific track. It is up to + the application using MOQT to define how broad or narrow the scope + is. An application that deals with connections between devices on a + local network may limit the scope to a single connection; by + contrast, an application that uses multiple CDNs to serve media may + require the scope to include all of those CDNs. + + Because the tuple of Track Namespace and Track Name are unique within + an MOQT scope, they can be used as a cache key for the track. If, at + a given moment in time, two tracks within the same scope contain + different data, they MUST have different names and/or namespaces. + MOQT provides subscribers with the ability to alter the specific + manner in which tracks are delivered via Parameters, but the actual + content of the tracks does not depend on those parameters; this is in + contrast to protocols like HTTP, where request headers can alter the + server response. + + A publisher that loses state (e.g. crashes) and intends to resume + publishing on the same Track risks colliding with previously + published Objects and violating the above requirements. A publisher + can handle this in application specific ways, for example: + + 1. Select a unique Track Name or Track Namespace whenever it resumes + publishing. For example, it can base one of the Namespace tuple + fields on the current time, or select a sufficiently large random + value. + + + + + + +Nandakumar, et al. Expires 6 March 2026 [Page 15] + +Internet-Draft moq-transport September 2025 + + + 2. Resume publishing under a previous Track Name and Namespace and + set the initial Group ID to a unique value guaranteed to be + larger than all previously used groups. This can be done by + choosing a Group ID based on the current time. + + 3. Use TRACK_STATUS or similar mechanism to query the previous state + to determine the largest published Group ID. + +3. Sessions + +3.1. Session establishment + + This document defines a protocol that can be used interchangeably + both over a QUIC connection directly [QUIC], and over WebTransport + [WebTransport]. Both provide streams and datagrams with similar + semantics (see [I-D.ietf-webtrans-overview], Section 4); thus, the + main difference lies in how the servers are identified and how the + connection is established. The [QUIC-DATAGRAM] extension MUST be + supported and negotiated in the QUIC connection used for MOQT, which + is already a requirement for WebTransport over HTTP/3. The + RESET_STREAM_AT [I-D.draft-ietf-quic-reliable-stream-reset] extension + to QUIC can be used by MOQT, but the protocol is also designed to + work correctly when the extension is not supported. + + There is no definition of the protocol over other transports, such as + TCP, and applications using MoQ might need to fallback to another + protocol when QUIC or WebTransport aren't available. + +3.1.1. WebTransport + + An MOQT server that is accessible via WebTransport can be identified + using an HTTPS URI ([RFC9110], Section 4.2.2). An MOQT session can + be established by sending an extended CONNECT request to the host and + the path indicated by the URI, as described in ([WebTransport], + Section 3). + +3.1.2. QUIC + + An MOQT server that is accessible via native QUIC can be identified + by a URI with a "moqt" scheme. The "moqt" URI scheme is defined as + follows, using definitions from [RFC3986]: + + moqt-URI = "moqt" "://" authority path-abempty [ "?" query ] + + The authority portion MUST NOT contain an empty host portion. The + moqt URI scheme supports the /.well-known/ path prefix defined in + [RFC8615]. + + + + +Nandakumar, et al. Expires 6 March 2026 [Page 16] + +Internet-Draft moq-transport September 2025 + + + This protocol does not specify any semantics on the path-abempty and + query portions of the URI. The contents of those are left up to the + application. + + The client can establish a connection to a MoQ server identified by a + given URI by setting up a QUIC connection to the host and port + identified by the authority section of the URI. The authority, path- + abempty and query portions of the URI are also transmitted in SETUP + parameters (see Section 9.3.2). + + The ALPN value [RFC7301] used by the protocol is moq-00. + +3.1.3. Connection URL + + Each track MAY have one or more associated connection URLs specifying + network hosts through which a track may be accessed. The syntax of + the Connection URL and the associated connection setup procedures are + specific to the underlying transport protocol usage (see Section 3). + +3.2. Version and Extension Negotiation + + Endpoints use the exchange of Setup messages to negotiate the MOQT + version and any extensions to use. + + The client indicates the MOQT versions it supports in the + CLIENT_SETUP message (see Section 9.3). It also includes the union + of all Setup Parameters (see Section 9.3.2) required for a handshake + by any of those versions. + + Within any MOQT version, clients request the use of extensions by + adding Setup parameters corresponding to that extension. No + extensions are defined in this document. + + The server replies with a SERVER_SETUP message that indicates the + chosen version, includes all parameters required for a handshake in + that version, and parameters for every extension requested by the + client that it supports. + + New versions of MOQT MUST specify which existing extensions can be + used with that version. New extensions MUST specify the existing + versions with which they can be used. + + If a given parameter carries the same information in multiple + versions, but might have different optimal values in those versions, + there SHOULD be separate Setup parameters for that information in + each version. + + + + + +Nandakumar, et al. Expires 6 March 2026 [Page 17] + +Internet-Draft moq-transport September 2025 + + +3.3. Session initialization + + The first stream opened is a client-initiated bidirectional control + stream where the endpoints exchange Setup messages (Section 9.3), + followed by other messages defined in Section 9. + + This draft only specifies a single use of bidirectional streams. + Objects are sent on unidirectional streams. Because there are no + other uses of bidirectional streams, a peer MAY close the session as + a PROTOCOL_VIOLATION if it receives a second bidirectional stream. + + The control stream MUST NOT be closed at the underlying transport + layer while the session is active. Doing so results in the session + being closed as a PROTOCOL_VIOLATION. + +3.4. Termination + + The Transport Session can be terminated at any point. When native + QUIC is used, the session is closed using the CONNECTION_CLOSE frame + ([QUIC], Section 19.19). When WebTransport is used, the session is + closed using the CLOSE_WEBTRANSPORT_SESSION capsule ([WebTransport], + Section 5). + + When terminating the Session, the application MAY use any error + message and SHOULD use a relevant code, as defined below: + + NO_ERROR (0x0): The session is being terminated without an error. + + INTERNAL_ERROR (0x1): An implementation specific error occurred. + + UNAUTHORIZED (0x2): The client is not authorized to establish a + session. + + PROTOCOL_VIOLATION (0x3): The remote endpoint performed an action + that was disallowed by the specification. + + INVALID_REQUEST_ID (0x4): The session was closed because the + endpoint used a Request ID that was smaller than or equal to a + previously received request ID, or the least- significant bit of + the request ID was incorrect for the endpoint. + + DUPLICATE_TRACK_ALIAS (0x5): The endpoint attempted to use a Track + Alias that was already in use. + + KEY_VALUE_FORMATTING_ERROR (0x6): The key-value pair has a + formatting error. + + TOO_MANY_REQUESTS (0x7): The session was closed because the endpoint + + + +Nandakumar, et al. Expires 6 March 2026 [Page 18] + +Internet-Draft moq-transport September 2025 + + + used a Request ID equal to or larger than the current Maximum + Request ID. + + INVALID_PATH (0x8): The PATH parameter was used by a server, on a + WebTransport session, or the server does not support the path. + + MALFORMED_PATH (0x9): The PATH parameter does not conform to the + rules in Section 9.3.2.2. + + GOAWAY_TIMEOUT (0x10): The session was closed because the peer took + too long to close the session in response to a GOAWAY + (Section 9.4) message. See session migration (Section 3.5). + + CONTROL_MESSAGE_TIMEOUT (0x11): The session was closed because the + peer took too long to respond to a control message. + + DATA_STREAM_TIMEOUT (0x12): The session was closed because the peer + took too long to send data expected on an open Data Stream (see + Section 10). This includes fields of a stream header or an object + header within a data stream. If an endpoint times out waiting for + a new object header on an open subgroup stream, it MAY send a + STOP_SENDING on that stream or terminate the subscription. + + AUTH_TOKEN_CACHE_OVERFLOW (0x13): The Session limit Section 9.3.2.4 + of the size of all registered Authorization tokens has been + exceeded. + + DUPLICATE_AUTH_TOKEN_ALIAS (0x14): Authorization Token attempted to + register an Alias that was in use (see Section 9.2.1.1). + + VERSION_NEGOTIATION_FAILED (0x15): The client didn't offer a version + supported by the server. + + MALFORMED_AUTH_TOKEN (0x16): Invalid Auth Token serialization during + registration (see Section 9.2.1.1). + + UNKNOWN_AUTH_TOKEN_ALIAS (0x17): No registered token found for the + provided Alias (see Section 9.2.1.1). + + EXPIRED_AUTH_TOKEN (0x18): Authorization token has expired + (Section 9.2.1.1). + + INVALID_AUTHORITY (0x19): The specified AUTHORITY does not + correspond to this server or cannot be used in this context. + + MALFORMED_AUTHORITY (0x1A): The AUTHORITY value is syntactically + invalid. + + + + +Nandakumar, et al. Expires 6 March 2026 [Page 19] + +Internet-Draft moq-transport September 2025 + + + An endpoint MAY choose to treat a subscription or request specific + error as a session error under certain circumstances, closing the + entire session in response to a condition with a single subscription + or message. Implementations need to consider the impact on other + outstanding subscriptions before making this choice. + +3.5. Migration + + MOQT requires a long-lived and stateful session. However, a service + provider needs the ability to shutdown/restart a server without + waiting for all sessions to drain naturally, as that can take days + for long-form media. MOQT enables proactively draining sessions via + the GOAWAY message (Section 9.4). + + The server sends a GOAWAY message, signaling the client to establish + a new session and migrate any active subscriptions. The GOAWAY + message optionally contains a new URI for the new session, otherwise + the current URI is reused. The server SHOULD terminate the session + with GOAWAY_TIMEOUT after a sufficient timeout if there are still + open subscriptions or fetches on a connection. + + When the server is a subscriber, it SHOULD send a GOAWAY message to + downstream subscribers prior to any UNSUBSCRIBE messages to upstream + publishers. + + After the client receives a GOAWAY, it's RECOMMENDED that the client + waits until there are no more active subscriptions before closing the + session with NO_ERROR. Ideally this is transparent to the + application using MOQT, which involves establishing a new session in + the background and migrating active subscriptions and published + namespaces. The client can choose to delay closing the session if it + expects more OBJECTs to be delivered. The server closes the session + with a GOAWAY_TIMEOUT if the client doesn't close the session quickly + enough. + +3.6. Congestion Control + + MOQT does not specify a congestion controller, but there are + important attributes to consider when selecting a congestion + controller for use with an application built on top of MOQT. + + + + + + + + + + + +Nandakumar, et al. Expires 6 March 2026 [Page 20] + +Internet-Draft moq-transport September 2025 + + +3.6.1. Bufferbloat + + Traditional AIMD congestion controllers (ex. CUBIC [RFC9438] and + Reno [RFC6582]) are prone to Bufferbloat. Bufferbloat occurs when + elements along the path build up a substantial queue of packets, + commonly more than doubling the round trip time. These queued + packets cause head-of-line blocking and latency, even when there is + no packet loss. + +3.6.2. Application-Limited + + The average bitrate for latency sensitive content needs to be less + than the available bandwidth, otherwise data will be queued and/or + dropped. As such, many MOQT applications will typically be limited + by the available data to send, and not the congestion controller. + Many congestion control algorithms only increase the congestion + window or bandwidth estimate if fully utilized. This combination can + lead to underestimating the available network bandwidth. As a + result, applications might need to periodically ensure the congestion + controller is not app-limited for at least a full round trip to + ensure the available bandwidth can be measured. + +3.6.3. Consistent Throughput + + Congestion control algorithms are commonly optimized for throughput, + not consistency. For example, BBR's PROBE_RTT state halves the + sending rate for more than a round trip in order to obtain an + accurate minimum RTT. Similarly, Reno halves it's congestion window + upon detecting loss. In both cases, the large reduction in sending + rate might cause issues with latency sensitive applications. + +4. Modularity + + MOQT defines all messages necessary to implement both simple + publishing or subscribing endpoints as well as highly functional + Relays. Non-Relay endpoints MAY implement only the subset of + functionality required to perform necessary tasks. For example, a + limited media player could operate using only SUBSCRIBE related + messages. Limited endpoints SHOULD respond to any unsupported + messages with the appropriate NOT_SUPPORTED error code, rather than + ignoring them. + + Relays MUST implement all MOQT messages defined in this document, as + well as processing rules described in Section 8. + + + + + + + +Nandakumar, et al. Expires 6 March 2026 [Page 21] + +Internet-Draft moq-transport September 2025 + + +5. Publishing and Retrieving Tracks + +5.1. Subscriptions + + A subscription can be initiated by either a publisher or a + subscriber. A publisher initiates a subscription to a track by + sending the PUBLISH message. The subscriber either accepts or + rejects the subscription using PUBLISH_OK or PUBLISH_ERROR. A + subscriber initiates a subscription to a track by sending the + SUBSCRIBE message. The publisher either accepts or rejects the + subscription using SUBSCRIBE_OK or SUBSCRIBE_ERROR. Once either of + these sequences is successful, the subscription can be updated by the + subscriber using SUBSCRIBE_UPDATE, terminated by the subscriber using + UNSUBSCRIBE, or terminated by the publisher using PUBLISH_DONE. + + All subscriptions have a Forward State which is either 0 or 1. If + the Forward State is 0, the publisher does not send objects for the + subscription. If the Forward State is 1, the publisher sends + objects. The initiator of the subscription sets the initial Forward + State in either PUBLISH or SUBSCRIBE. The sender of PUBLISH_OK can + update the Forward State based on its preference. Once the + subscription is established, the subscriber can update the Forward + State by sending SUBSCRIBE_UPDATE. + + Either endpoint can initiate a subscription to a track without + exchanging any prior messages other than SETUP. Relays MUST NOT send + any PUBLISH messages without knowing the client is interested in and + authorized to receive the content. The communication of intent and + authorization can be accomplished by the client sending + SUBSCRIBE_NAMESPACE, or conveyed in other mechanisms out of band. + + An endpoint MAY SUBSCRIBE to a Track it is publishing, though only + Relays are required to handle such a SUBSCRIBE. Such self- + subscriptions are identical to subscriptions initiated by other + endpoints, and all published Objects will be forwarded back to the + endpoint, subject to priority and congestion response rules. + + A publisher MUST send exactly one SUBSCRIBE_OK or SUBSCRIBE_ERROR in + response to a SUBSCRIBE. It MUST send exactly one FETCH_OK or + FETCH_ERROR in response to a FETCH. A subscriber MUST send exactly + one PUBLISH_OK or PUBLISH_ERROR in response to a PUBLISH. The peer + SHOULD close the session with a protocol error if it receives more + than one. + + Publishers MAY start sending Objects on PUBLISH-initiated + subscriptions before receiving a PUBLISH_OK response to reduce + latency. Doing so can consume unnecessary resources in cases where + the Subscriber rejects the subscription with PUBLISH_ERROR or sets + + + +Nandakumar, et al. Expires 6 March 2026 [Page 22] + +Internet-Draft moq-transport September 2025 + + + Forward State=0 in PUBLISH_OK. It can also result in the Subscriber + dropping Objects if its buffering limits are exceeded (see + Section 10.3 and Section 10.4.2). + + A subscriber keeps subscription state until it sends UNSUBSCRIBE, or + after receipt of a PUBLISH_DONE or SUBSCRIBE_ERROR. Note that + PUBLISH_DONE does not usually indicate that state can immediately be + destroyed, see Section 9.12. + + A subscriber keeps FETCH state until it sends FETCH_CANCEL, receives + FETCH_ERROR, or receives a FIN or RESET_STREAM for the FETCH data + stream. If the data stream is already open, it MAY send STOP_SENDING + for the data stream along with FETCH_CANCEL, but MUST send + FETCH_CANCEL. + + The Publisher can destroy subscription or fetch state as soon as it + has received UNSUBSCRIBE or FETCH_CANCEL, respectively. It MUST + reset any open streams associated with the SUBSCRIBE or FETCH. It + can also destroy state after closing the FETCH data stream. + + The publisher can immediately delete subscription state after sending + PUBLISH_DONE, but MUST NOT send it until it has closed all related + streams. It can destroy all FETCH state after closing the data + stream. + + A SUBSCRIBE_ERROR indicates no objects will be delivered, and both + endpoints can immediately destroy relevant state. Objects MUST NOT + be sent for requests that end with an error. + + A FETCH_ERROR indicates that both endpoints can immediately destroy + state. Since a relay can start delivering FETCH Objects from cache + before determining the result of the request, some Objects could be + received even if the FETCH results in error. + + The Parameters in SUBSCRIBE, PUBLISH_OK and FETCH MUST NOT cause the + publisher to alter the payload of the objects it sends, as that would + violate the track uniqueness guarantee described in Section 2.5.1. + +6. Namespace Discovery + + Discovery of MOQT servers is always done out-of-band. Namespace + discovery can be done in the context of an established MOQT session. + + Given sufficient out of band information, it is valid for a + subscriber to send a SUBSCRIBE or FETCH message to a publisher + (including a relay) without any previous MOQT messages besides SETUP. + However, SUBSCRIBE_NAMESPACE, PUBLISH and PUBLISH_NAMESPACE messages + provide an in-band means of discovery of publishers for a namespace. + + + +Nandakumar, et al. Expires 6 March 2026 [Page 23] + +Internet-Draft moq-transport September 2025 + + + The syntax of these messages is described in Section 9. + +6.1. Subscribing to Namespaces + + If the subscriber is aware of a namespace of interest, it can send + SUBSCRIBE_NAMESPACE to publishers/relays it has established a session + with. The recipient of this message will send any relevant + PUBLISH_NAMESPACE, PUBLISH_NAMESPACE_DONE or PUBLISH messages for + that namespace, or more specific part of that namespace. This + includes echoing back PUBLISH or PUBLISH_NAMESPACE messages to the + endpoint that sent them. If an endpoint accepts its own PUBLISH, + this behaves as self-subscription described in Section 5.1. + + A publisher MUST send exactly one SUBSCRIBE_NAMESPACE_OK or + SUBSCRIBE_NAMESPACE_ERROR in response to a SUBSCRIBE_NAMESPACE. The + subscriber SHOULD close the session with a protocol error if it + detects receiving more than one. + + The receiver of a SUBSCRIBE_NAMESPACE_OK or SUBSCRIBE_NAMESPACE_ERROR + ought to forward the result to the application, so the application + can decide which other publishers to contact, if any. + + An UNSUBSCRIBE_NAMESPACE withdraws a previous SUBSCRIBE_NAMESPACE. + It does not prohibit original publishers from sending further + PUBLISH_NAMESPACE or PUBLISH messages, but relays MUST NOT send any + further PUBLISH messages to a client without knowing the client is + interested in and authorized to receive the content. + +6.2. Publishing Namespaces + + A publisher MAY send PUBLISH_NAMESPACE messages to any subscriber. A + PUBLISH_NAMESPACE indicates to the subscriber that the publisher has + tracks available in that namespace. A subscriber MAY send SUBSCRIBE + or FETCH for tracks in a namespace without having received a + PUBLISH_NAMESPACE for it. + + If a publisher is authoritative for a given namespace, or is a relay + that has received an authorized PUBLISH_NAMESPACE for that namespace + from an upstream publisher, it MUST send a PUBLISH_NAMESPACE to any + subscriber that has subscribed via SUBSCRIBE_NAMESPACE for that + namespace, or a prefix of that namespace. A publisher MAY send the + PUBLISH_NAMESPACE to any other subscriber. + + An endpoint SHOULD report the reception of a PUBLISH_NAMESPACE_OK or + PUBLISH_NAMESPACE_ERROR to the application to inform the search for + additional subscribers for a namespace, or to abandon the attempt to + publish under this namespace. This might be especially useful in + upload or chat applications. A subscriber MUST send exactly one + + + +Nandakumar, et al. Expires 6 March 2026 [Page 24] + +Internet-Draft moq-transport September 2025 + + + PUBLISH_NAMESPACE_OK or PUBLISH_NAMESPACE_ERROR in response to a + PUBLISH_NAMESPACE. The publisher SHOULD close the session with a + protocol error if it receives more than one. + + A PUBLISH_NAMESPACE_DONE message withdraws a previous + PUBLISH_NAMESPACE, although it is not a protocol error for the + subscriber to send a SUBSCRIBE or FETCH message for a track in a + namespace after receiving an PUBLISH_NAMESPACE_DONE. + + A subscriber can send PUBLISH_NAMESPACE_CANCEL to revoke acceptance + of an PUBLISH_NAMESPACE, for example due to expiration of + authorization credentials. The message enables the publisher to + PUBLISH_NAMESPACE again with refreshed authorization, or discard + associated state. After receiving an PUBLISH_NAMESPACE_CANCEL, the + publisher does not send PUBLISH_NAMESPACE_DONE. + + While PUBLISH_NAMESPACE indicates to relays how to connect publishers + and subscribers, it is not a full-fledged routing protocol and does + not protect against loops and other phenomena. In particular, + PUBLISH_NAMESPACE SHOULD NOT be used to find paths through richly + connected networks of relays. + + A subscriber MAY send a SUBSCRIBE or FETCH for a track to any + publisher. If it has accepted a PUBLISH_NAMESPACE with a namespace + that exactly matches the namespace for that track, it SHOULD only + request it from the senders of those PUBLISH_NAMESPACE messages. + +7. Priorities + + MoQ priorities allow a subscriber and original publisher to influence + the transmission order of Objects within a session in the presence of + congestion. + +7.1. Definitions + + MOQT maintains priorities between different _schedulable objects_. A + schedulable object in MOQT is either: + + 1. The first or next Object in a Subgroup that is in response to a + subscription. + + 2. An Object in response to a subscription that belongs to a Track + with delivery preference Datagram. + + 3. An Object in response to a FETCH where that Object is the next + Object in the response. + + + + + +Nandakumar, et al. Expires 6 March 2026 [Page 25] + +Internet-Draft moq-transport September 2025 + + + An Object is not schedulable if it is known that no part of it can be + written due to underlying transport flow control limits. + + A single subgroup or datagram has a single publisher priority. + Within a response to SUBSCRIBE, it can be useful to conceptualize + this process as scheduling subgroups or datagrams instead of + individual objects on them. FETCH responses however can contain + objects with different publisher priorities. + + A _priority number_ is an unsigned integer with a value between 0 and + 255. A lower priority number indicates higher priority; the highest + priority is 0. + + _Subscriber Priority_ is a priority number associated with an + individual request. It is specified in the SUBSCRIBE or FETCH + message, and can be updated via SUBSCRIBE_UPDATE message. The + subscriber priority of an individual schedulable object is the + subscriber priority of the request that caused that object to be + sent. When subscriber priority is changed, a best effort SHOULD be + made to apply the change to all objects that have not been scheduled, + but it is implementation dependent what happens to objects that have + already been scheduled. + + _Publisher Priority_ is a priority number associated with an + individual schedulable object. It is specified in the header of the + respective subgroup or datagram, or in each object in a FETCH + response. + + _Group Order_ is a property of an individual subscription. It can be + either 'Ascending' (groups with lower group ID are sent first), or + 'Descending' (groups with higher group ID are sent first). The + subscriber optionally communicates its group order preference in the + SUBSCRIBE message; the publisher's preference is used if the + subscriber did not express one (by setting Group Order field to value + 0x0). The group order of an existing subscription cannot be changed. + +7.2. Scheduling Algorithm + + When an MOQT publisher has multiple schedulable objects it can choose + between, the objects SHOULD be selected as follows: + + 1. If two objects have different subscriber priorities associated + with them, the one with *the highest subscriber priority* is + scheduled to be sent first. + + 2. If two objects have the same subscriber priority, but different + publisher priorities, the one with *the highest publisher + priority* is scheduled to be sent first. + + + +Nandakumar, et al. Expires 6 March 2026 [Page 26] + +Internet-Draft moq-transport September 2025 + + + 3. If two objects in response to the same request have the same + subscriber and publisher priority, but belong to two different + groups of the same track, *the group order* of the associated + subscription is used to decide the one that is scheduled to be + sent first. + + 4. If two objects in response to the same request belong to the same + group of the same track, the one with *the lowest Subgroup ID* + (for tracks with delivery preference Subgroup), or *the lowest + Object ID* (for tracks with delivery preference Datagram) is + scheduled to be sent first. + + The definition of "scheduled to be sent first" in the algorithm is + implementation dependent and is constrained by the prioritization + interface of the underlying transport. For some implementations, it + could mean that the object is serialized and passed to the underlying + transport first. Other implementations can control the order packets + are initially transmitted. + + This algorithm does not provide a well-defined ordering for objects + that belong to different subscriptions or FETCH responses, but have + the same subscriber and publisher priority. The ordering in those + cases is implementation-defined, though the expectation is that all + subscriptions will be able to send some data. + + Given the critical nature of control messages and their relatively + small size, the control stream SHOULD be prioritized higher than all + subscribed Objects. + +7.3. Considerations for Setting Priorities + + For downstream subscriptions, relays SHOULD respect the subscriber + and original publisher's priorities. Relays can receive + subscriptions with conflicting subscriber priorities or Group Order + preferences. Relays SHOULD NOT directly use Subscriber Priority or + Group Order from incoming subscriptions for upstream subscriptions. + Relays' use of these fields for upstream subscriptions can be based + on factors specific to it, such as the popularity of the content or + policy, or relays can specify the same value for all upstream + subscriptions. + + MoQ Sessions can span multiple namespaces, and priorities might not + be coordinated across namespaces. The subscriber's priority is + considered first, so there is a mechanism for a subscriber to fix + incompatibilities between different namespaces prioritization + schemes. Additionally, it is anticipated that when multiple + namespaces are present within a session, the namespaces could be + coordinating, possibly part of the same application. In cases when + + + +Nandakumar, et al. Expires 6 March 2026 [Page 27] + +Internet-Draft moq-transport September 2025 + + + pooling among namespaces is expected to cause issues, multiple MoQ + sessions, either within a single connection or on multiple + connections can be used. + + Implementations that have a default priority SHOULD set it to a value + in the middle of the range (eg: 128) to allow non-default priorities + to be set either higher or lower. + +8. Relays + + Relays are leveraged to enable distribution scale in the MoQ + architecture. Relays can be used to form an overlay delivery + network, similar in functionality to Content Delivery Networks + (CDNs). Additionally, relays serve as policy enforcement points by + validating subscribe and publish requests at the edge of a network. + + Relays are endpoints, which means they terminate Transport Sessions + in order to have visibility of MoQ Object metadata. + +8.1. Caching Relays + + Relays MAY cache Objects, but are not required to. + + A caching relay saves Objects to its cache identified by the Object's + Full Track Name, Group ID and Object ID. If multiple objects are + received with the same Full Track Name, Group ID and Object ID, + Relays MAY ignore subsequently received Objects or MAY use them to + update certain cached fields. Implementations that update the cache + need to protect against cache poisoning. The only Object fields that + can be updated are the following: + + 1. Object Status can transition from any status to Object Does Not + Exist in cases where the object is no longer available. + Transitions between Normal, End of Group and End of Track are + invalid. + + 2. Object Header Extensions can be added, removed or updated, + subject to the constraints of the specific header extension. + + An endpoint that receives a duplicate Object with an invalid Object + Status change, or a Forwarding Preference, Subgroup ID, Priority or + Payload that differ from a previous version MUST treat the track as + Malformed. + + + + + + + + +Nandakumar, et al. Expires 6 March 2026 [Page 28] + +Internet-Draft moq-transport September 2025 + + + Note that due to reordering, an implementation can receive a + duplicate Object with a status of Normal, End of Group or End of + Track after receiving a previous status of Object Does Not Exist. + The endpoint SHOULD NOT cache or forward the duplicate object in this + case. + + A cache MUST store all properties of an Object defined in + Section 10.2.1, with the exception of any extensions + (Section 10.2.1.2) that specify otherwise. + +8.2. Multiple Publishers + + A Relay can receive PUBLISH_NAMESPACE for the same Track Namespace or + PUBLISH messages for the same Track from multiple publishers. The + following sections explain how Relays maintain subscriptions to all + available publishers for a given Track. + + There is no specified limit to the number of publishers of a Track + Namespace or Track. An implementation can use mechanisms such as + PUBLISH_ERROR, PUBLISH_NAMESPACE_ERROR, UNSUBSCRIBE or + PUBLISH_NAMESPACE_CANCEL if it cannot accept an additional publisher + due to implementation constraints. Implementations can consider the + establishment or idle time of the session or subscription to + determine which publisher to reject or disconnect. + + Relays MUST handle Objects for the same Track from multiple + publishers and forward them to active matching subscriptions. The + Relay SHOULD attempt to deduplicate Objects before forwarding, + subject to implementation constraints. + +8.3. Subscriber Interactions + + Subscribers request Tracks by sending a SUBSCRIBE (see Section 9.7) + or FETCH (see Section 9.16) control message for each Track of + interest. Relays MUST ensure subscribers are authorized to access + the content associated with the Track. The authorization information + can be part of request itself or part of the encompassing session. + The specifics of how a relay authorizes a user are outside the scope + of this specification. + + The relay MUST have an established upstream subscription before + sending SUBSCRIBE_OK in response to a downstream SUBSCRIBE. If a + relay does not have sufficient information to send a FETCH_OK + immediately in response to a FETCH, it MUST withhold sending FETCH_OK + until it does. + + + + + + +Nandakumar, et al. Expires 6 March 2026 [Page 29] + +Internet-Draft moq-transport September 2025 + + + For successful subscriptions, the publisher maintains a list of + subscribers for each Track. Relays use the Track Alias + (Section 10.1) of an incoming Object to identify its Track and find + the active subscribers. Each new Object belonging to the Track is + forwarded to each active subscriber, as allowed by the subscription's + filter (see Section 9.7), and delivered according to the priority + (see Section 7) and delivery timeout (see Section 9.2.1.2). + + A relay MUST NOT reorder or drop objects received on a multi-object + stream when forwarding to subscribers, unless it has application + specific information. + + Relays MAY aggregate authorized subscriptions for a given Track when + multiple subscribers request the same Track. Subscription + aggregation allows relays to make only a single upstream subscription + for the Track. The published content received from the upstream + subscription request is cached and shared among the pending + subscribers. Because SUBSCRIBE_UPDATE only allows narrowing a + subscription, relays that aggregate upstream subscriptions can + subscribe using the Largest Object filter to avoid churn as + downstream subscribers with disparate filters subscribe and + unsubscribe from a Track. + + A subscriber remains subscribed to a Track at a Relay until it + unsubscribes, the upstream publisher terminates the subscription, or + the subscription expires (see Section 9.8). A subscription with a + filter can reach a state where all possible Objects matching the + filter have been delivered to the subscriber. Since tracking this + can be prohibitively expensive, Relays are not required or expected + to do so. + +8.3.1. Graceful Subscriber Relay Switchover + + This section describes behavior a subscriber MAY implement to allow + for a better user experience when a relay sends a GOAWAY. + + When a subscriber receives the GOAWAY message, it starts the process + of connecting to a new relay and sending the SUBSCRIBE requests for + all active subscriptions to the new relay. The new relay will send a + response to the subscribes and if they are successful, the + subscriptions to the old relay can be stopped with an UNSUBSCRIBE. + +8.4. Publisher Interactions + + There are two ways to publish through a relay: + + + + + + +Nandakumar, et al. Expires 6 March 2026 [Page 30] + +Internet-Draft moq-transport September 2025 + + + 1. Send a PUBLISH message for a specific Track to the relay. The + relay MAY respond with PUBLISH_OK in Forward State=0 until there + are known subscribers for new Tracks. + + 2. Send a PUBLISH_NAMESPACE message for a Track Namespace to the + relay. This enables the relay to send SUBSCRIBE or FETCH + messages to publishers for Tracks in this Namespace in response + to requests received from subscribers. + + Relays MUST verify that publishers are authorized to publish the set + of Tracks whose Track Namespace matches the namespace in a + PUBLISH_NAMESPACE, or the Full Track Name in PUBLISH. The + authorization and identification of the publisher depends on the way + the relay is managed and is application specific. + + When a publisher wants to stop new subscriptions for a published + namespace it sends a PUBLISH_NAMESPACE_DONE. A subscriber indicates + it will no longer subcribe to Tracks in a namespace it previously + responded PUBLISH_NAMESPACE_OK to by sending a + PUBLISH_NAMESPACE_CANCEL. + + A Relay connects publishers and subscribers by managing sessions + based on the Track Namespace or Full Track Name. When a SUBSCRIBE + message is sent, its Full Track Name is matched exactly against + existing upstream subscriptions. + + Namespace Prefix Matching is further used to decide which publishers + receive a SUBSCRIBE and which subscribers receive a PUBLISH. In this + process, the tuples in the Track Namespace are matched sequentially, + requiring an exact match for each field. If the published or + subscribed Track Namespace has the same or fewer fields than the + Track Namespace in the message, it qualifies as a match. + + For example: A SUBSCRIBE message with namespace=(foo, bar) and name=x + will match sessions that sent PUBLISH_NAMESPACE messages with + namespace=(foo) or namespace=(foo, bar). It will not match a session + with namespace=(foobar). + + Relays MUST forward SUBSCRIBE messages to all matching publishers and + PUBLISH_NAMESPACE or PUBLISH messages to all matching subscribers. + + When a Relay needs to make an upstream FETCH request, it determines + the available publishers using the same matching rules as SUBSCRIBE. + When more than one publisher is available, the Relay MAY send the + FETCH to any of them. + + + + + + +Nandakumar, et al. Expires 6 March 2026 [Page 31] + +Internet-Draft moq-transport September 2025 + + + When a Relay receives an authorized SUBSCRIBE for a Track with one or + more active upstream subscriptions, it MUST reply with SUBSCRIBE_OK. + If the SUBSCRIBE has Forward State=1 and the upstream subscriptions + are in Forward State=0, the Relay MUST send SUBSCRIBE_UPDATE with + Forward=1 to all publishers. If there are no active upstream + subscriptions for the requested Track, the Relay MUST send a + SUBSCRIBE request to each publisher that has published the + subscription's namespace or prefix thereof. If the SUBSCRIBE has + Forward =1, then the Relay MUST use Forward=1 when subscribing + upstream. + + When a relay receives an incoming PUBLISH message, it MUST send a + PUBLISH request to each subscriber that has subscribed (via + SUBSCRIBE_NAMESPACE) to the Track's namespace or prefix thereof. + + When a relay receives an authorized PUBLISH_NAMESPACE for a namespace + that matches one or more existing subscriptions to other upstream + sessions, it MUST send a SUBSCRIBE to the publisher that sent the + PUBLISH_NAMESPACE for each matching subscription. When it receives + an authorized PUBLISH message for a Track that has active + subscribers, it MUST respond with PUBLISH_OK. If at least one + downstream subscriber for the Track has Forward State=1, the Relay + MUST use Forward State=1 in the reply. + + If a Session is closed due to an unknown or invalid control message + or Object, the Relay MUST NOT propagate that message or Object to + another Session, because it would enable a single Session error to + force an unrelated Session, which might be handling other + subscriptions, to be closed. + +8.4.1. Graceful Publisher Network Switchover + + This section describes a behavior that a publisher MAY choose to + implement to allow for a better user experience when switching + between networks, such as WiFi to Cellular or vice versa. + + If the original publisher detects it is likely to need to switch + networks, for example because the WiFi signal is getting weaker, and + it does not have QUIC connection migration available, it establishes + a new session over the new interface and sends PUBLISH_NAMESPACE and/ + or PUBLISH messages. The relay will establish subscriptions and the + publisher publishes Objects on both sessions. Once the subscriptions + have migrated over to the session on the new network, the publisher + can stop publishing Objects on the old network. The relay will + attempt to deduplicate Objects received on both subscriptions. + Ideally, the subscriptions downstream from the relay do no observe + this change, and keep receiving the Objects on the same subscription. + + + + +Nandakumar, et al. Expires 6 March 2026 [Page 32] + +Internet-Draft moq-transport September 2025 + + +8.4.2. Graceful Publisher Relay Switchover + + This section describes a behavior that a publisher MAY choose to + implement to allow for a better user experience when a relay sends + them a GOAWAY. + + When a publisher receives a GOAWAY, it starts the process of + connecting to a new relay and sends PUBLISH_NAMESPACE and/or PUBLISH + messages, but it does not immediately stop publishing Objects to the + old Relay. The new Relay will establish subscriptions and the + publisher can start sending new Objects to the new relay instead of + the old Relay. Once Objects are going to the new Relay, the + published namespaces and subscriptions to the old relay can be + withdrawn or terminated. + +8.5. Relay Object Handling + + MOQT encodes the delivery information via Object headers + (Section 10.2). A relay MUST NOT modify Object properties when + forwarding, except for Object Extension Headers as specified in + Section 10.2.1.2. + + A relay MUST treat the object payload as opaque. A relay MUST NOT + combine, split, or otherwise modify object payloads. A relay SHOULD + prioritize sending Objects based on Section 7. + + A publisher SHOULD begin sending incomplete objects when available to + avoid incurring additional latency. + +9. Control Messages + + MOQT uses a single bidirectional stream to exchange control messages, + as defined in Section 3.3. Every single message on the control + stream is formatted as follows: + + MOQT Control Message { + Message Type (i), + Message Length (16), + Message Payload (..), + } + + Figure 3: MOQT Message + + The following Message Types are defined: + + + + + + + +Nandakumar, et al. Expires 6 March 2026 [Page 33] + +Internet-Draft moq-transport September 2025 + + + +======+============================================+ + | ID | Messages | + +======+============================================+ + | 0x01 | RESERVED (SETUP for version 00) | + +------+--------------------------------------------+ + | 0x40 | RESERVED (CLIENT_SETUP for versions <= 10) | + +------+--------------------------------------------+ + | 0x41 | RESERVED (SERVER_SETUP for versions <= 10) | + +------+--------------------------------------------+ + | 0x20 | CLIENT_SETUP (Section 9.3) | + +------+--------------------------------------------+ + | 0x21 | SERVER_SETUP (Section 9.3) | + +------+--------------------------------------------+ + | 0x10 | GOAWAY (Section 9.4) | + +------+--------------------------------------------+ + | 0x15 | MAX_REQUEST_ID (Section 9.5) | + +------+--------------------------------------------+ + | 0x1A | REQUESTS_BLOCKED (Section 9.6) | + +------+--------------------------------------------+ + | 0x3 | SUBSCRIBE (Section 9.7) | + +------+--------------------------------------------+ + | 0x4 | SUBSCRIBE_OK (Section 9.8) | + +------+--------------------------------------------+ + | 0x5 | SUBSCRIBE_ERROR (Section 9.9) | + +------+--------------------------------------------+ + | 0x2 | SUBSCRIBE_UPDATE (Section 9.10) | + +------+--------------------------------------------+ + | 0xA | UNSUBSCRIBE (Section 9.11) | + +------+--------------------------------------------+ + | 0xB | PUBLISH_DONE (Section 9.12) | + +------+--------------------------------------------+ + | 0x1D | PUBLISH (Section 9.13) | + +------+--------------------------------------------+ + | 0x1E | PUBLISH_OK (Section 9.14) | + +------+--------------------------------------------+ + | 0x1F | PUBLISH_ERROR (Section 9.15) | + +------+--------------------------------------------+ + | 0x16 | FETCH (Section 9.16) | + +------+--------------------------------------------+ + | 0x18 | FETCH_OK (Section 9.17) | + +------+--------------------------------------------+ + | 0x19 | FETCH_ERROR (Section 9.18) | + +------+--------------------------------------------+ + | 0x17 | FETCH_CANCEL (Section 9.19) | + +------+--------------------------------------------+ + | 0xD | TRACK_STATUS (Section 9.20) | + +------+--------------------------------------------+ + | 0xE | TRACK_STATUS_OK (Section 9.21 | + + + +Nandakumar, et al. Expires 6 March 2026 [Page 34] + +Internet-Draft moq-transport September 2025 + + + +------+--------------------------------------------+ + | 0xF | TRACK_STATUS_ERROR (Section 9.22) | + +------+--------------------------------------------+ + | 0x6 | PUBLISH_NAMESPACE (Section 9.23) | + +------+--------------------------------------------+ + | 0x7 | PUBLISH_NAMESPACE_OK (Section 9.24) | + +------+--------------------------------------------+ + | 0x8 | PUBLISH_NAMESPACE_ERROR (Section 9.25) | + +------+--------------------------------------------+ + | 0x9 | PUBLISH_NAMESPACE_DONE (Section 9.26) | + +------+--------------------------------------------+ + | 0xC | PUBLISH_NAMESPACE_CANCEL (Section 9.27) | + +------+--------------------------------------------+ + | 0x11 | SUBSCRIBE_NAMESPACE (Section 9.28) | + +------+--------------------------------------------+ + | 0x12 | SUBSCRIBE_NAMESPACE_OK (Section 9.29) | + +------+--------------------------------------------+ + | 0x13 | SUBSCRIBE_NAMESPACE_ERROR (Section 9.30 | + +------+--------------------------------------------+ + | 0x14 | UNSUBSCRIBE_NAMESPACE (Section 9.31) | + +------+--------------------------------------------+ + + Table 1 + + An endpoint that receives an unknown message type MUST close the + session. Control messages have a length to make parsing easier, but + no control messages are intended to be ignored. The length is set to + the number of bytes in Message Payload, which is defined by each + message type. If the length does not match the length of the Message + Payload, the receiver MUST close the session with PROTOCOL_VIOLATION. + +9.1. Request ID + + Most MOQT control messages contain a session specific Request ID. + The Request ID correlates requests and responses, allows endpoints to + update or terminate ongoing requests, and supports the endpoint's + ability to limit the concurrency and frequency of requests. There + are independent Request IDs for each endpoint. The client's Request + ID starts at 0 and are even and the server's Request ID starts at 1 + and are odd. The Request ID increments by 2 with each FETCH, + SUBSCRIBE, SUBSCRIBE_UPDATE, SUBSCRIBE_NAMESPACE, PUBLISH, + PUBLISH_NAMESPACE or TRACK_STATUS request. Other messages with a + Request ID field reference the Request ID of another message for + correlation. If an endpoint receives a Request ID that is not valid + for the peer, or a new request with a Request ID that is not + expected, it MUST close the session with INVALID_REQUEST_ID. + + + + + +Nandakumar, et al. Expires 6 March 2026 [Page 35] + +Internet-Draft moq-transport September 2025 + + +9.2. Parameters + + Some messages include a Parameters field that encodes optional + message elements. + + Senders MUST NOT repeat the same parameter type in a message unless + the parameter definition explicitly allows multiple instances of that + type to be sent in a single message. Receivers SHOULD check that + there are no unauthorized duplicate parameters and close the session + as a PROTOCOL_VIOLATION if found. Receivers MUST allow duplicates of + unknown parameters. + + Receivers ignore unrecognized parameters. + + The number of parameters in a message is not specifically limited, + but the total length of a control message is limited to 2^16-1 bytes. + + Parameters are serialized as Key-Value-Pairs Figure 2. + + Setup message parameters use a namespace that is constant across all + MoQ Transport versions. All other messages use a version-specific + namespace. For example, the integer '1' can refer to different + parameters for Setup messages and for all other message types. SETUP + message parameter types are defined in Section 9.3.2. Version- + specific parameter types are defined in Section 9.2.1. + +9.2.1. Version Specific Parameters + + Each version-specific parameter definition indicates the message + types in which it can appear. If it appears in some other type of + message, it MUST be ignored. Note that since Setup parameters use a + separate namespace, it is impossible for these parameters to appear + in Setup messages. + +9.2.1.1. AUTHORIZATION TOKEN + + The AUTHORIZATION TOKEN parameter (Parameter Type 0x03) MAY appear in + a CLIENT_SETUP, SERVER_SETUP, PUBLISH, SUBSCRIBE, SUBSCRIBE_UPDATE, + SUBSCRIBE_NAMESPACE, PUBLISH_NAMESPACE, TRACK_STATUS or FETCH + message. This parameter conveys information to authorize the sender + to perform the operation carrying the parameter. + + The AUTHORIZATION TOKEN parameter MAY be repeated within a message. + + The parameter value is a Token structure containing an optional + Session-specific Alias. The Alias allows the sender to reference a + previously transmitted Token Type and Token Value in future messages. + The Token structure is serialized as follows: + + + +Nandakumar, et al. Expires 6 March 2026 [Page 36] + +Internet-Draft moq-transport September 2025 + + + Token { + Alias Type (i), + [Token Alias (i),] + [Token Type (i),] + [Token Value (..)] + } + + Figure 4: Token structure + + * Alias Type - an integer defining both the serialization and the + processing behavior of the receiver. This Alias type has the + following code points: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Nandakumar, et al. Expires 6 March 2026 [Page 37] + +Internet-Draft moq-transport September 2025 + + + +======+===========+===============================+ + | Code | Name | Serialization and behavior | + +======+===========+===============================+ + | 0x0 | DELETE | There is an Alias but no Type | + | | | or Value. This Alias | + +------+-----------+-------------------------------+ + | | | and the Token Value it was | + | | | previously associated with | + +------+-----------+-------------------------------+ + | | | MUST be retired. Retiring | + | | | removes them from the pool | + +------+-----------+-------------------------------+ + | | | of actively registered | + | | | tokens. | + +------+-----------+-------------------------------+ + | 0x1 | REGISTER | There is an Alias, a Type and | + | | | a Value. This Alias | + +------+-----------+-------------------------------+ + | | | MUST be associated with the | + | | | Token Value for the | + +------+-----------+-------------------------------+ + | | | duration of the Session or it | + | | | is deleted. This action | + +------+-----------+-------------------------------+ + | | | is termed "registering" the | + | | | Token. | + +------+-----------+-------------------------------+ + | 0x2 | USE_ALIAS | There is an Alias but no Type | + | | | or Value. Use the Token | + +------+-----------+-------------------------------+ + | | | Type and Value previously | + | | | registered with this Alias. | + +------+-----------+-------------------------------+ + | 0x3 | USE_VALUE | There is no Alias and there | + | | | is a Type and Value. Use | + +------+-----------+-------------------------------+ + | | | the Token Value as provided. | + | | | The Token Value may be | + +------+-----------+-------------------------------+ + | | | discarded after processing. | + +------+-----------+-------------------------------+ + + Table 2 + + + + + + + + +Nandakumar, et al. Expires 6 March 2026 [Page 38] + +Internet-Draft moq-transport September 2025 + + + * Token Alias - a Session-specific integer identifier that + references a Token Value. There are separate Alias spaces for the + client and server (e.g.: they can each register Alias=1). Once a + Token Alias has been registered, it cannot be re-registered by the + same sender in the Session without first being deleted. Use of + the Token Alias is optional. + + * Token Type - a numeric identifier for the type of Token payload + being transmitted. This type is defined by the IANA table "MOQT + Auth Token Type" (see Section 13). Type 0 is reserved to indicate + that the type is not defined in the table and is negotiated out- + of-band between client and receiver. + + * Token Value - the payload of the Token. The contents and + serialization of this payload are defined by the Token Type. + + If the Token structure cannot be decoded, the receiver MUST close the + Session with Key-Value Formatting error. The receiver of a message + attempting to register an Alias which is already registered MUST + close the Session with DUPLICATE_AUTH_TOKEN_ALIAS. The receiver of a + message referencing an Alias that is not currently registered MUST + reject the message with UNKNOWN_AUTH_TOKEN_ALIAS. + + The receiver of a message containing a well-formed Token structure + but otherwise invalid AUTHORIZATION TOKEN parameter MUST reject that + message with an MALFORMED_AUTH_TOKEN error. + + The receiver of a message carrying an AUTHORIZATION TOKEN with Alias + Type REGISTER that does not result in a Session error MUST register + the Token Alias, in the token cache, even if the message fails for + other reasons, including Unauthorized. This allows senders to + pipeline messages that refer to previously registered tokens without + potentially terminating the entire Session. A receiver MAY store an + error code (eg: UNAUTHORIZED or MALFORMED_AUTH_TOKEN) in place of the + Token Type and Token Alias if any future message referencing the + Token Alias will result in that error. The size of a registered + cache entry includes the length of the Token Value, regardless of + whether it is stored. + + If a receiver detects that an authorization token has expired, it + MUST retain the registered Alias until it is deleted by the sender, + though it MAY discard other state associated with the token that is + no longer needed. Expiration does not affect the size occupied by a + token in the token cache. Any message that references the token with + Alias Type USE_ALIAS fails with EXPIRED_AUTH_TOKEN. + + + + + + +Nandakumar, et al. Expires 6 March 2026 [Page 39] + +Internet-Draft moq-transport September 2025 + + + Using an Alias to refer to a previously registered Token Type and + Value is for efficiency only and has the same effect as if the Token + Type and Value was included directly. Retiring an Alias that was + previously used to authorize a message has no retroactive effect on + the original authorization, nor does it prevent that same Token Type + and Value from being re-registered. + + Senders of tokens SHOULD only register tokens which they intend to + re-use during the Session and SHOULD retire previously registered + tokens once their utility has passed. + + By registering a Token, the sender is requiring the receiver to store + the Token Alias and Token Value until they are deleted, or the + Session ends. The receiver can protect its resources by sending a + SETUP parameter defining the MAX_AUTH_TOKEN_CACHE_SIZE limit (see + Section 9.3.2.4) it is willing to accept. If a registration is + attempted which would cause this limit to be exceeded, the receiver + MUST termiate the Session with a AUTH_TOKEN_CACHE_OVERFLOW error. + +9.2.1.2. DELIVERY TIMEOUT Parameter + + The DELIVERY TIMEOUT parameter (Parameter Type 0x02) MAY appear in a + TRACK_STATUS, TRACK_STATUS_OK, PUBLISH, PUBLISH_OK, SUBSCRIBE, + SUBSCRIBE_OK, or SUBSCRIBE_UDPATE message. It is the duration in + milliseconds the relay SHOULD continue to attempt forwarding Objects + after they have been received. The start time for the timeout is + based on when the Object Headers are received, and does not depend + upon the forwarding preference. There is no explicit signal that an + Object was not sent because the delivery timeout was exceeded. + + If both the subscriber and publisher specify the parameter, they use + the min of the two values for the subscription. The publisher SHOULD + always specify the value received from an upstream subscription when + there is one, and nothing otherwise. + + Publishers can, at their discretion, discontinue forwarding Objects + earlier than the negotiated DELIVERY TIMEOUT, subject to stream + closure and ordering constraints described in Section 10.4.3. + However, if neither the subscriber nor publisher specifies DELIVERY + TIMEOUT, all Objects in the track matching the subscription filter + are delivered as indicated by their Group Order and Priority. If a + subscriber fails to consume Objects at a sufficient rate, causing the + publisher to exceed its resource limits, the publisher MAY terminate + the subscription with error TOO_FAR_BEHIND. + + If an object in a subgroup exceeds the delivery timeout, the + publisher MUST reset the underlying transport stream (see + Section 10.4.3). + + + +Nandakumar, et al. Expires 6 March 2026 [Page 40] + +Internet-Draft moq-transport September 2025 + + + When sent by a subscriber, this parameter is intended to be specific + to a subscription, so it SHOULD NOT be forwarded upstream by a relay + that intends to serve multiple subscriptions for the same track. + + Publishers SHOULD consider whether the entire Object can likely be + successfully delivered within the timeout period before sending any + data for that Object, taking into account priorities, congestion + control, and any other relevant information. + +9.2.1.3. MAX CACHE DURATION Parameter + + The MAX_CACHE_DURATION parameter (Parameter Type 0x04) MAY appear in + a PUBLISH, SUBSCRIBE_OK, FETCH_OK or TRACK_STATUS_OK message. It is + an integer expressing the number of milliseconds an object can be + served from a cache. If present, the relay MUST NOT start forwarding + any individual Object received through this subscription or fetch + after the specified number of milliseconds has elapsed since the + beginning of the Object was received. This means Objects earlier in + a multi-object stream will expire earlier than Objects later in the + stream. Once Objects have expired from cache, their state becomes + unknown, and a relay that handles a downstream request that includes + those Objects re-requests them. + +9.3. CLIENT_SETUP and SERVER_SETUP + + The CLIENT_SETUP and SERVER_SETUP messages are the first messages + exchanged by the client and the server; they allow the endpoints to + establish the mutually supported version and agree on the initial + configuration before any objects are exchanged. It is a sequence of + key-value pairs called Setup parameters; the semantics and format of + which can vary based on whether the client or server is sending. To + ensure future extensibility of MOQT, endpoints MUST ignore unknown + setup parameters. TODO: describe GREASE for those. + + The wire format of the Setup messages are as follows: + + + + + + + + + + + + + + + + +Nandakumar, et al. Expires 6 March 2026 [Page 41] + +Internet-Draft moq-transport September 2025 + + + CLIENT_SETUP Message { + Type (i) = 0x20, + Length (16), + Number of Supported Versions (i), + Supported Versions (i) ..., + Number of Parameters (i), + Setup Parameters (..) ..., + } + + SERVER_SETUP Message { + Type (i) = 0x21, + Length (16), + Selected Version (i), + Number of Parameters (i), + Setup Parameters (..) ..., + } + + Figure 5: MOQT Setup Messages + + The available versions and Setup parameters are detailed in the next + sections. + +9.3.1. Versions + + MOQT versions are a 32-bit unsigned integer, encoded as a varint. + This version of the specification is identified by the number + 0x00000001. Versions with the most significant 16 bits of the + version number cleared are reserved for use in future IETF consensus + documents. + + The client offers the list of the protocol versions it supports; the + server MUST reply with one of the versions offered by the client. If + the server does not support any of the versions offered by the + client, or the client receives a server version that it did not + offer, the corresponding peer MUST close the session with + VERSION_NEGOTIATION_FAILED. + + [[RFC editor: please remove the remainder of this section before + publication.]] + + The version number for the final version of this specification + (0x00000001), is reserved for the version of the protocol that is + published as an RFC. Version numbers used to identify IETF drafts + are created by adding the draft number to 0xff000000. For example, + draft-ietf-moq-transport-13 would be identified as 0xff00000D. + +9.3.2. Setup Parameters + + + + +Nandakumar, et al. Expires 6 March 2026 [Page 42] + +Internet-Draft moq-transport September 2025 + + +9.3.2.1. AUTHORITY + + The AUTHORITY parameter (Parameter Type 0x05) allows the client to + specify the authority component of the MoQ URI when using native QUIC + ([QUIC]). It MUST NOT be used by the server, or when WebTransport is + used. When an AUTHORITY parameter is received from a server, or when + an AUTHORITY parameter is received while WebTransport is used, or + when an AUTHORITY parameter is received by a server but the server + does not support the specified authority, the session MUST be closed + with Invalid Authority. + + The AUTHORITY parameter follows the URI formatting rules [RFC3986]. + When connecting to a server using a URI with the "moqt" scheme, the + client MUST set the AUTHORITY parameter to the authority portion of + the URI. If an AUTHORITY parameter does not conform to these rules, + the session MUST be closed with Malformed Authority. + +9.3.2.2. PATH + + The PATH parameter (Parameter Type 0x01) allows the client to specify + the path of the MoQ URI when using native QUIC ([QUIC]). It MUST NOT + be used by the server, or when WebTransport is used. When a PATH + parameter is received from a server, or when a PATH parameter is + received while WebTransport is used, or when a PATH parameter is + received by a server but the server does not support the specified + path, the session MUST be closed with Invalid Path. + + The PATH parameter follows the URI formatting rules [RFC3986]. When + connecting to a server using a URI with the "moqt" scheme, the client + MUST set the PATH parameter to the path-abempty portion of the URI; + if query is present, the client MUST concatenate ?, followed by the + query portion of the URI to the parameter. If a PATH does not + conform to these rules, the session MUST be closed with Malformed + Path. + +9.3.2.3. MAX_REQUEST_ID + + The MAX_REQUEST_ID parameter (Parameter Type 0x02) communicates an + initial value for the Maximum Request ID to the receiving endpoint. + The default value is 0, so if not specified, the peer MUST NOT send + requests. + + + + + + + + + + +Nandakumar, et al. Expires 6 March 2026 [Page 43] + +Internet-Draft moq-transport September 2025 + + +9.3.2.4. MAX_AUTH_TOKEN_CACHE_SIZE + + The MAX_AUTH_TOKEN_CACHE_SIZE parameter (Parameter Type 0x04) + communicates the maximum size in bytes of all actively registered + Authorization tokens that the server is willing to store per Session. + This parameter is optional. The default value is 0 which prohibits + the use of token Aliases. + + The token size is calculated as 16 bytes + the size of the Token + Value field (see Figure 4). The total size as restricted by the + MAX_AUTH_TOKEN_CACHE_SIZE parameter is calculated as the sum of the + token sizes for all registered tokens (Alias Type value of 0x01) + minus the sum of the token sizes for all deregistered tokens (Alias + Type value of 0x00), since Session initiation. + +9.3.2.5. AUTHORIZATION TOKEN + + See Section 9.2.1.1. The endpoint can specify one or more tokens in + CLIENT_SETUP or SERVER_SETUP that the peer can use to authorize MOQT + session establishment. + + If a server receives an AUTHORIZATION TOKEN parameter in CLIENT_SETUP + with Alias Type REGISTER_TOKEN that exceeds its + MAX_AUTH_TOKEN_CACHE_SIZE, it MUST NOT fail the session with + AUTH_TOKEN_CACHE_OVERFLOW. Instead, it MUST treat the parameter as + Alias Type USE_VALUE. A client MUST handle registration failures of + this kind by purging any Token Aliases that failed to register based + on the MAX_AUTH_TOKEN_CACHE_SIZE parameter in SERVER_SETUP (or the + default value of 0). + +9.3.2.6. MOQT IMPLEMENTATION + + The MOQT_IMPLEMENTATION parameter (Parameter Type 0x05) identifies + the name and version of the sender's MOQT implementation. This + SHOULD be a UTF-8 encoded string [RFC3629], though the message does + not carry information, such as language tags, that would aid + comprehension by any entity other than the one that created the text. + +9.4. GOAWAY + + An endpoint sends a GOAWAY message to inform the peer it intends to + close the session soon. Servers can use GOAWAY to initiate session + migration (Section 3.5) with an optional URI. + + The GOAWAY message does not impact subscription state. A subscriber + SHOULD individually UNSUBSCRIBE for each existing subscription, while + a publisher MAY reject new requests after sending a GOAWAY. + + + + +Nandakumar, et al. Expires 6 March 2026 [Page 44] + +Internet-Draft moq-transport September 2025 + + + Upon receiving a GOAWAY, an endpoint SHOULD NOT initiate new requests + to the peer including SUBSCRIBE, PUBLISH, FETCH, PUBLISH_NAMESPACE, + SUBSCRIBE_NAMESPACE and TRACK_SATUS. + + The endpoint MUST terminate the session with a PROTOCOL_VIOLATION + (Section 3.4) if it receives multiple GOAWAY messages. + + GOAWAY Message { + Type (i) = 0x10, + Length (16), + New Session URI Length (i), + New Session URI (..), + } + + Figure 6: MOQT GOAWAY Message + + * New Session URI: When received by a client, indicates where the + client can connect to continue this session. The client MUST use + this URI for the new session if provided. If the URI is zero + bytes long, the current URI is reused instead. The new session + URI SHOULD use the same scheme as the current URI to ensure + compatibility. The maxmimum length of the New Session URI is + 8,192 bytes. If an endpoint receives a length exceeding the + maximum, it MUST close the session with a PROTOCOL_VIOLATION. + + If a server receives a GOAWAY with a non-zero New Session URI + Length it MUST terminate the session with a PROTOCOL_VIOLATION. + +9.5. MAX_REQUEST_ID + + An endpoint sends a MAX_REQUEST_ID message to increase the number of + requests the peer can send within a session. + + The Maximum Request ID MUST only increase within a session, and + receipt of a MAX_REQUEST_ID message with an equal or smaller Request + ID value is a PROTOCOL_VIOLATION. + + MAX_REQUEST_ID Message { + Type (i) = 0x15, + Length (16), + Request ID (i), + } + + Figure 7: MOQT MAX_REQUEST_ID Message + + * Request ID: The new Maximum Request ID for the session plus 1. If + a Request ID equal to or larger than this is received by the + endpoint that sent the MAX_REQUEST_ID in any request message + + + +Nandakumar, et al. Expires 6 March 2026 [Page 45] + +Internet-Draft moq-transport September 2025 + + + (PUBLISH_NAMESPACE, FETCH, SUBSCRIBE, SUBSCRIBE_NAMESPACE, + SUBSCRIBE_UDPATE or TRACK_STATUS), the endpoint MUST close the + session with an error of TOO_MANY_REQUESTS. + + MAX_REQUEST_ID is similar to MAX_STREAMS in ([RFC9000], Section 4.6), + and similar considerations apply when deciding how often to send + MAX_REQUEST_ID. For example, implementations might choose to + increase MAX_REQUEST_ID as subscriptions are closed to keep the + number of available subscriptions roughly consistent. + +9.6. REQUESTS_BLOCKED + + The REQUESTS_BLOCKED message is sent when an endpoint would like to + send a new request, but cannot because the Request ID would exceed + the Maximum Request ID value sent by the peer. The endpoint SHOULD + send only one REQUESTS_BLOCKED for a given Maximum Request ID. + + An endpoint MAY send a MAX_REQUEST_ID upon receipt of + REQUESTS_BLOCKED, but it MUST NOT rely on REQUESTS_BLOCKED to trigger + sending a MAX_REQUEST_ID, because sending REQUESTS_BLOCKED is not + required. + + REQUESTS_BLOCKED Message { + Type (i) = 0x1A, + Length (16), + Maximum Request ID (i), + } + + Figure 8: MOQT REQUESTS_BLOCKED Message + + * Maximum Request ID: The Maximum Request ID for the session on + which the endpoint is blocked. More on Request ID in Section 9.1. + +9.7. SUBSCRIBE + + A subscription causes the publisher to send newly published objects + for a track. A subscriber MUST NOT make multiple active + subscriptions for a track within a single session and publishers + SHOULD treat this as a protocol violation. + + *Filter Types* + + The subscriber specifies a filter on the subscription to allow the + publisher to identify which objects need to be delivered. + + + + + + + +Nandakumar, et al. Expires 6 March 2026 [Page 46] + +Internet-Draft moq-transport September 2025 + + + All filters have a Start Location and an optional End Group. Only + objects published or received via a subscription having Locations + greater than or equal to Start and strictly less than or equal to the + End Group (when present) pass the filter. + + The Largest Object is defined to be the object with the largest + Location (Section 1.4.1) in the track from the perspective of the + endpoint processing the SUBSCRIBE message. Largest Object updates + when the first byte of an Object with a larger Location than the + previous value is published or received through a subscription. + + There are 4 types of filters: + + Largest Object (0x2): The filter Start Location is {Largest + Object.Group, Largest Object.Object + 1} and Largest Object is + communicated in SUBSCRIBE_OK. If no content has been delivered yet, + the filter Start Location is {0, 0}. There is no End Group - the + subscription is open ended. Note that due to network reordering or + prioritization, relays can receive Objects with Locations smaller + than Largest Object after the SUBSCRIBE is processed, but these + Objects do not pass the Largest Object filter. + + Next Group Start (0x1): The filter Start Location is {Largest + Object.Group + 1, 0} and Largest Object is communicated in + SUBSCRIBE_OK. If no content has been delivered yet, the filter Start + Location is {0, 0}. There is no End Group - the subscription is open + ended. For scenarios where the subscriber intends to start from more + than one group in the future, it can use an AbsoluteStart filter + instead. + + AbsoluteStart (0x3): The filter Start Location is specified + explicitly in the SUBSCRIBE message. The Start specified in the + SUBSCRIBE message MAY be less than the Largest Object observed at the + publisher. There is no End Group - the subscription is open ended. + To receive all Objects that are published or are received after this + subscription is processed, a subscriber can use an AbsoluteStart + filter with Start = {0, 0}. + + AbsoluteRange (0x4): The filer Start Location and End Group are + specified explicitly in the SUBSCRIBE message. The Start specified + in the SUBSCRIBE message MAY be less than the Largest Object observed + at the publisher. If the specified End Group is the same group + specified in Start, the remainder of that Group passes the filter. + End Group MUST specify the same or a larger Group than specified in + Start. + + An endpoint that receives a filter type other than the above MUST be + close the session with PROTOCOL_VIOLATION. + + + +Nandakumar, et al. Expires 6 March 2026 [Page 47] + +Internet-Draft moq-transport September 2025 + + + Subscribe only delivers newly published or received Objects. Objects + from the past are retrieved using FETCH (Section 9.16). + + A Subscription can also request a publisher to not forward Objects + for a given track by setting the Forward field to 0. This allows the + publisher or relay to prepare to serve the subscription in advance, + reducing the time to receive objects in the future. Relays SHOULD + set the Forward flag to 1 if a new subscription needs to be sent + upstream, regardless of the value of the Forward field from the + downstream subscription. Subscriptions that are not forwarded + consume resources from the publisher, so a publisher might + deprioritize, reject, or close those subscriptions to ensure other + subscriptions can be delivered. Control messages, such as + SUBCRIBE_DONE (Section 9.12) are still sent. + + The format of SUBSCRIBE is as follows: + + SUBSCRIBE Message { + Type (i) = 0x3, + Length (16), + Request ID (i), + Track Namespace (tuple), + Track Name Length (i), + Track Name (..), + Subscriber Priority (8), + Group Order (8), + Forward (8), + Filter Type (i), + [Start Location (Location)], + [End Group (i)], + Number of Parameters (i), + Parameters (..) ... + } + + Figure 9: MOQT SUBSCRIBE Message + + * Request ID: See Section 9.1. + + * Track Namespace: Identifies the namespace of the track as defined + in (Section 2.4.1). + + * Track Name: Identifies the track name as defined in + (Section 2.4.1). + + * Subscriber Priority: Specifies the priority of a subscription + relative to other subscriptions in the same session. Lower + numbers get higher priority. See Section 7. + + + + +Nandakumar, et al. Expires 6 March 2026 [Page 48] + +Internet-Draft moq-transport September 2025 + + + * Group Order: Allows the subscriber to request Objects be delivered + in Ascending (0x1) or Descending (0x2) order by group. See + Section 7. A value of 0x0 indicates the original publisher's + Group Order SHOULD be used. Values larger than 0x2 are a protocol + error. + + * Forward: If 1, Objects matching the subscription are forwarded to + the subscriber. If 0, Objects are not forwarded to the + subscriber. Any other value is a protocol error and MUST + terminate the session with a PROTOCOL_VIOLATION (Section 3.4). + + * Filter Type: Identifies the type of filter, which also indicates + whether the Start and End Group fields will be present. + + * Start Location: The starting location for this subscriptions. + Only present for "AbsoluteStart" and "AbsoluteRange" filter types. + + * End Group: The end Group ID. Only present for the "AbsoluteRange" + filter type. + + * Parameters: The parameters are defined in Section 9.2.1. + + On successful subscription, the publisher MUST reply with a + SUBSCRIBE_OK, allowing the subscriber to determine the start group/ + object when not explicitly specified and the publisher SHOULD start + delivering objects. + + If a publisher cannot satisfy the requested start or end or if the + end has already been published it SHOULD send a SUBSCRIBE_ERROR with + code INVALID_RANGE. A publisher MUST NOT send objects from outside + the requested start and end. + +9.8. SUBSCRIBE_OK + + A publisher sends a SUBSCRIBE_OK control message for successful + subscriptions. + + + + + + + + + + + + + + + +Nandakumar, et al. Expires 6 March 2026 [Page 49] + +Internet-Draft moq-transport September 2025 + + + SUBSCRIBE_OK Message { + Type (i) = 0x4, + Length (16), + Request ID (i), + Track Alias (i), + Expires (i), + Group Order (8), + Content Exists (8), + [Largest Location (Location)], + Number of Parameters (i), + Parameters (..) ... + } + + Figure 10: MOQT SUBSCRIBE_OK Message + + * Request ID: The Request ID of the SUBSCRIBE this message is + replying to Section 9.7. + + * Track Alias: The identifer used for this track in Subgroups or + Datagrams (see Section 10.1). The same Track Alias MUST NOT be + used to refer to two different Tracks simultaneously. If a + subscriber receives a SUBSCRIBE_OK that uses the same Track Alias + as a different track with an active subscription, it MUST close + the session with error DUPLICATE_TRACK_ALIAS. + + * Expires: Time in milliseconds after which the subscription is no + longer valid. A value of 0 indicates that the subscription does + not expire or expires at an unknown time. Expires is advisory and + a subscription can end prior to the expiry time or last longer. + + * Group Order: Indicates the subscription will be delivered in + Ascending (0x1) or Descending (0x2) order by group. See + Section 7. Values of 0x0 and those larger than 0x2 are a protocol + error. + + * Content Exists: 1 if an object has been published on this track, 0 + if not. If 0, then the Largest Group ID and Largest Object ID + fields will not be present. Any other value is a protocol error + and MUST terminate the session with a PROTOCOL_VIOLATION + (Section 3.4). + + * Largest Location: The location of the largest object available for + this track. This field is only present if Content Exists has a + value of 1. + + * Parameters: The parameters are defined in Section 9.2.1. + + + + + +Nandakumar, et al. Expires 6 March 2026 [Page 50] + +Internet-Draft moq-transport September 2025 + + +9.9. SUBSCRIBE_ERROR + + A publisher sends a SUBSCRIBE_ERROR control message in response to a + failed SUBSCRIBE. + + SUBSCRIBE_ERROR Message { + Type (i) = 0x5, + Length (16), + Request ID (i), + Error Code (i), + Error Reason (Reason Phrase), + } + + Figure 11: MOQT SUBSCRIBE_ERROR Message + + * Request ID: The Request ID of the SUBSCRIBE this message is + replying to Section 9.7. + + * Error Code: Identifies an integer error code for subscription + failure. + + * Error Reason: Provides the reason for subscription error. See + Section 1.4.3. + + The application SHOULD use a relevant error code in SUBSCRIBE_ERROR, + as defined below: + + INTERNAL_ERROR (0x0): An implementation specific or generic error + occurred. + + UNAUTHORIZED (0x1): The subscriber is not authorized to subscribe to + the given track. + + TIMEOUT (0x2): The subscription could not be completed before an + implementation specific timeout. For example, a relay could not + establish an upstream subscription within the timeout. + + NOT_SUPPORTED (0x3): The endpoint does not support the SUBSCRIBE + method. + + TRACK_DOES_NOT_EXIST (0x4): The requested track is not available at + the publisher. + + INVALID_RANGE (0x5): The end of the SUBSCRIBE range is earlier than + the beginning, or the end of the range has already been published. + + MALFORMED_AUTH_TOKEN (0x10): Invalid Auth Token serialization during + registration (see Section 9.2.1.1). + + + +Nandakumar, et al. Expires 6 March 2026 [Page 51] + +Internet-Draft moq-transport September 2025 + + + EXPIRED_AUTH_TOKEN (0x12): Authorization token has expired + (Section 9.2.1.1). + +9.10. SUBSCRIBE_UPDATE + + A subscriber sends a SUBSCRIBE_UPDATE to a publisher to modify an + existing subscription. Subscriptions can only be narrowed, not + widened, as an attempt to widen could fail. If Objects with + Locations smaller than the current subscription's Start Location are + required, FETCH can be used to retrieve them. The Start Location + MUST NOT decrease and the End Group MUST NOT increase. A publisher + MUST terminate the session with a PROTOCOL_VIOLATION if the + SUBSCRIBE_UPDATE violates these rules or if the subscriber specifies + a request ID that has not existed within the Session. + + When a subscriber narrows their subscription, it might still receive + objects outside the new range if the publisher sent them before the + update was processed. + + There is no control message in response to a SUBSCRIBE_UPDATE, + because it is expected that it will always succeed and the worst + outcome is that it is not processed promptly and some extra objects + from the existing subscription are delivered. + + Unlike a new subscription, SUBSCRIBE_UPDATE can not cause an Object + to be delivered multiple times. Like SUBSCRIBE, End Group MUST be + greater than or equal to the Group specified in Start. + + If a parameter included in SUBSCRIBE is not present in + SUBSCRIBE_UPDATE, its value remains unchanged. There is no mechanism + to remove a parameter from a subscription. + + The format of SUBSCRIBE_UPDATE is as follows: + + SUBSCRIBE_UPDATE Message { + Type (i) = 0x2, + Length (16), + Request ID (i), + Subscription Request ID (i), + Start Location (Location), + End Group (i), + Subscriber Priority (8), + Forward (8), + Number of Parameters (i), + Parameters (..) ... + } + + Figure 12: MOQT SUBSCRIBE_UPDATE Message + + + +Nandakumar, et al. Expires 6 March 2026 [Page 52] + +Internet-Draft moq-transport September 2025 + + + * Request ID: See Section 9.1. + + * Subscription Request ID: The Request ID of the SUBSCRIBE + (Section 9.7) this message is updating. This MUST match an + existing Request ID. + + * Start Location : The starting location. + + * End Group: The end Group ID, plus 1. A value of 0 means the + subscription is open-ended. + + * Subscriber Priority: Specifies the priority of a subscription + relative to other subscriptions in the same session. Lower + numbers get higher priority. See Section 7. + + * Forward: If 1, Objects matching the subscription are forwarded to + the subscriber. If 0, Objects are not forwarded to the + subscriber. Any other value is a protocol error and MUST + terminate the session with a PROTOCOL_VIOLATION (Section 3.4). + + * Parameters: The parameters are defined in Section 9.2.1. + +9.11. UNSUBSCRIBE + + A Subscriber issues an UNSUBSCRIBE message to a Publisher indicating + it is no longer interested in receiving the specified Track, + indicating that the Publisher stop sending Objects as soon as + possible. + + The format of UNSUBSCRIBE is as follows: + + UNSUBSCRIBE Message { + Type (i) = 0xA, + Length (16), + Request ID (i) + } + + Figure 13: MOQT UNSUBSCRIBE Message + + * Request ID: The Request ID of the subscription that is being + terminated. See Section 9.7. + + + + + + + + + + +Nandakumar, et al. Expires 6 March 2026 [Page 53] + +Internet-Draft moq-transport September 2025 + + +9.12. PUBLISH_DONE + + A publisher sends a PUBLISH_DONE message to indicate it is done + publishing Objects for that subscription. The Status Code indicates + why the subscription ended, and whether it was an error. Because + PUBLISH_DONE is sent on the control stream, it is likely to arrive at + the receiver before late-arriving objects, and often even late- + opening streams. However, the receiver uses it as an indication that + it should receive any late-opening streams in a relatively short + time. + + Note that some objects in the subscribed track might never be + delivered, because a stream was reset, or never opened in the first + place, due to the delivery timeout. + + A sender MUST NOT send PUBLISH_DONE until it has closed all streams + it will ever open, and has no further datagrams to send, for a + subscription. After sending PUBLISH_DONE, the sender can immediately + destroy subscription state, although stream state can persist until + delivery completes. The sender might persist subscription state to + enforce the delivery timeout by resetting streams on which it has + already sent FIN, only deleting it when all such streams have + received ACK of the FIN. + + A sender MUST NOT destroy subscription state until it sends + PUBLISH_DONE, though it can choose to stop sending objects (and thus + send PUBLISH_DONE) for any reason. + + A subscriber that receives PUBLISH_DONE SHOULD set a timer of at + least its delivery timeout in case some objects are still inbound due + to prioritization or packet loss. The subscriber MAY dispense with a + timer if it sent UNSUBSCRIBE or is otherwise no longer interested in + objects from the track. Once the timer has expired, the receiver + destroys subscription state once all open streams for the + subscription have closed. A subscriber MAY discard subscription + state earlier, at the cost of potentially not delivering some late + objects to the application. The subscriber SHOULD send STOP_SENDING + on all streams related to the subscription when it deletes + subscription state. + + The format of PUBLISH_DONE is as follows: + + + + + + + + + + +Nandakumar, et al. Expires 6 March 2026 [Page 54] + +Internet-Draft moq-transport September 2025 + + + PUBLISH_DONE Message { + Type (i) = 0xB, + Length (16), + Request ID (i), + Status Code (i), + Stream Count (i), + Error Reason (Reason Phrase) + } + + Figure 14: MOQT PUBLISH_DONE Message + + * Request ID: The Request ID of the subscription that is being + terminated. See Section 9.7. + + * Status Code: An integer status code indicating why the + subscription ended. + + * Stream Count: An integer indicating the number of data streams the + publisher opened for this subscription. This helps the subscriber + know if it has received all of the data published in this + subscription by comparing the number of streams received. The + subscriber can immediately remove all subscription state once the + same number of streams have been processed. If the track had + Forwarding Preference = Datagram, the publisher MUST set Stream + Count to 0. If the publisher is unable to set Stream Count to the + exact number of streams opened for the subscription, it MUST set + Stream Count to 2^62 - 1. Subscribers SHOULD use a timeout or + other mechanism to remove subscription state in case the publisher + set an incorrect value, reset a stream before the SUBGROUP_HEADER, + or set the maximum value. If a subscriber receives more streams + for a subscription than specified in Stream Count, it MAY close + the session with a PROTOCOL_VIOLATION. + + * Error Reason: Provides the reason for subscription error. See + Section 1.4.3. + + The application SHOULD use a relevant status code in PUBLISH_DONE, as + defined below: + + INTERNAL_ERROR (0x0): An implementation specific or generic error + occurred. + + UNAUTHORIZED (0x1): The subscriber is no longer authorized to + subscribe to the given track. + + TRACK_ENDED (0x2): The track is no longer being published. + + SUBSCRIPTION_ENDED (0x3): The publisher reached the end of an + + + +Nandakumar, et al. Expires 6 March 2026 [Page 55] + +Internet-Draft moq-transport September 2025 + + + associated Subscribe filter. + + GOING_AWAY (0x4): The subscriber or publisher issued a GOAWAY + message. + + EXPIRED (0x5): The publisher reached the timeout specified in + SUBSCRIBE_OK. + + TOO_FAR_BEHIND (0x6): The publisher's queue of objects to be sent to + the given subscriber exceeds its implementation defined limit. + + MALFORMED_TRACK (0x7): A relay publisher detected the track was + malformed (see Section 2.5). + +9.13. PUBLISH + + The publisher sends the PUBLISH control message to initiate a + subscription to a track. The receiver verifies the publisher is + authorized to publish this track. + + PUBLISH Message { + Type (i) = 0x1D, + Length (i), + Request ID (i), + Track Namespace (tuple), + Track Name Length (i), + Track Name (..), + Track Alias (i), + Group Order (8), + Content Exists (8), + [Largest Location (Location),] + Forward (8), + Number of Parameters (i), + Parameters (..) ..., + } + + Figure 15: MOQT PUBLISH Message + + * Request ID: See Section 9.1. + + * Track Namespace: Identifies a track's namespace as defined in + (Section 2.4.1) + + * Track Name: Identifies the track name as defined in + (Section 2.4.1). + + + + + + +Nandakumar, et al. Expires 6 March 2026 [Page 56] + +Internet-Draft moq-transport September 2025 + + + * Track Alias: The identifer used for this track in Subgroups or + Datagrams (see Section 10.1). The same Track Alias MUST NOT be + used to refer to two different Tracks simultaneously. If a + subscriber receives a PUBLISH that uses the same Track Alias as a + different track with an active subscription, it MUST close the + session with error DUPLICATE_TRACK_ALIAS. + + * Group Order: Indicates the subscription will be delivered in + Ascending (0x1) or Descending (0x2) order by group. See + Section 7. Values of 0x0 and those larger than 0x2 are a protocol + error. + + * Content Exists: 1 if an object has been published on this track, 0 + if not. If 0, then the Largest Group ID and Largest Object ID + fields will not be present. Any other value is a protocol error + and MUST terminate the session with a PROTOCOL_VIOLATION + (Section 3.4). + + * Largest Location: The location of the largest object available for + this track. + + * Forward: The forward mode for this subscription. Any value other + than 0 or 1 is a PROTOCOL_VIOLATION. 0 indicates the publisher + will not transmit any objects until the subscriber sets the + Forward State to 1. 1 indicates the publisher will start + transmitting objects immediately, even before PUBLISH_OK. + + * Parameters: The parameters are defined in Section 9.2.1. + + A subscriber receiving a PUBLISH for a Track it does not wish to + receive SHOULD send PUBLISH_ERROR with error code UNINTERESTED, and + abandon reading any publisher initiated streams associated with that + subscription using a STOP_SENDING frame. + +9.14. PUBLISH_OK + + The subscriber sends a PUBLISH_OK control message to acknowledge the + successful authorization and acceptance of a PUBLISH message, and + establish a subscription. + + + + + + + + + + + + +Nandakumar, et al. Expires 6 March 2026 [Page 57] + +Internet-Draft moq-transport September 2025 + + + PUBLISH_OK Message { + Type (i) = 0x1E, + Length (i), + Request ID (i), + Forward (8), + Subscriber Priority (8), + Group Order (8), + Filter Type (i), + [Start Location (Location)], + [End Group (i)], + Number of Parameters (i), + Parameters (..) ..., + } + + Figure 16: MOQT PUBLISH_OK Message + + * Request ID: The Request ID of the PUBLISH this message is replying + to Section 9.13. + + * Forward: The Forward State for this subscription, either 0 (don't + forward) or 1 (forward). + + * Subscriber Priority: The Subscriber Priority for this + subscription. + + * Group Order: Indicates the subscription will be delivered in + Ascending (0x1) or Descending (0x2) order by group. See + Section 7. Values of 0x0 and those larger than 0x2 are a protocol + error. This overwrites the GroupOrder specified PUBLISH. + + * Filter Type, Start Location, End Group: See Section 9.7. + + * Parameters: Parameters associated with this message. + +9.15. PUBLISH_ERROR + + The subscriber sends a PUBLISH_ERROR control message to reject a + subscription initiated by PUBLISH. + + PUBLISH_ERROR Message { + Type (i) = 0x1F, + Length (i), + Request ID (i), + Error Code (i), + Error Reason (Reason Phrase), + } + + Figure 17: MOQT PUBLISH_ERROR Message + + + +Nandakumar, et al. Expires 6 March 2026 [Page 58] + +Internet-Draft moq-transport September 2025 + + + * Request ID: The Request ID of the PUBLISH this message is replying + to Section 9.13. + + * Error Code: Identifies an integer error code for failure. + + * Error Reason: Provides the reason for subscription error. See + Section 1.4.3. + + The application SHOULD use a relevant error code in PUBLISH_ERROR, as + defined below: + + INTERNAL_ERROR (0x0): An implementation specific or generic error + occurred. + + UNAUTHORIZED (0x1): The publisher is not authorized to publish the + given namespace or track. + + TIMEOUT (0x2): The subscription could not be established before an + implementation specific timeout. + + NOT_SUPPORTED (0x3): The endpoint does not support the PUBLISH + method. + + UNINTERESTED (0x4): The namespace or track is not of interest to the + endpoint. + +9.16. FETCH + + A subscriber issues a FETCH to a publisher to request a range of + already published objects within a track. + + There are three types of Fetch messages. + + +======+========================+ + | Code | Fetch Type | + +======+========================+ + | 0x1 | Standalone Fetch | + +------+------------------------+ + | 0x2 | Relative Joining Fetch | + +------+------------------------+ + | 0x3 | Absolute Joining Fetch | + +------+------------------------+ + + Table 3 + + An endpoint that receives a Fetch Type other than 0x1, 0x2 or 0x3 + MUST be close the session with a PROTOCOL_VIOLATION. + + + + +Nandakumar, et al. Expires 6 March 2026 [Page 59] + +Internet-Draft moq-transport September 2025 + + +9.16.1. Standalone Fetch + + A Fetch of Objects performed independently of any Subscribe. + + A Standalone Fetch includes this structure: + + Standalone Fetch { + Track Namespace (tuple), + Track Name Length (i), + Track Name (..), + Start Location (Location), + End Location (Location) + } + + * Track Namespace: Identifies the namespace of the track as defined + in (Section 2.4.1). + + * Track Name: Identifies the track name as defined in + (Section 2.4.1). + + * Start Location: The start Location. + + * End Location: The end Location, plus 1. A Location.Object value + of 0 means the entire group is requested. + +9.16.2. Joining Fetches + + A Joining Fetch is associated with a Subscribe request by specifying + the Request ID of an active subscription. A publisher receiving a + Joining Fetch uses properties of the associated Subscribe to + determine the Track Namespace, Track Name and End Location such that + it is contiguous with the associated Subscribe. The subscriber can + set the Start Location to an absolute Location or a Location relative + to the current group. + + A Subscriber can use a Joining Fetch to, for example, fill a playback + buffer with a certain number of groups prior to the live edge of a + track. + + A Joining Fetch is only permitted when the associated Subscribe has + the Filter Type Largest Object; any other value results in closing + the session with a PROTOCOL_VIOLATION. + + If no Objects have been published for the track, and the SUBSCRIBE_OK + has a Content Exists value of 0, the publisher MUST respond with a + FETCH_ERROR with error code INVALID_RANGE. + + A Joining Fetch includes this structure: + + + +Nandakumar, et al. Expires 6 March 2026 [Page 60] + +Internet-Draft moq-transport September 2025 + + + Joining Fetch { + Joining Request ID (i), + Joining Start (i) + } + + * Joining Request ID: The Request ID of the existing subscription to + be joined. If a publisher receives a Joining Fetch with a Request + ID that does not correspond to an existing Subscribe in the same + session, it MUST respond with a Fetch Error with code Invalid + Joining Request ID. + + * Joining Start : A relative or absolute value used to determing the + Start Location, described below. + +9.16.2.1. Joining Fetch Range Calculation + + The Largest Location value from the corresponding subscription is + used to calculate the end of a Joining Fetch so the Objects retrieved + by the FETCH and SUBSCRIBE are contiguous and non-overlapping. + + The publisher receiving a Joining Fetch sets the End Location to + {Subscribe Largest Location.Object + 1}. + + Note: the last Object included in the Joining FETCH response is + Subscribe Largest Location. The + 1 above indicates the equivalent + Standalone Fetch encoding. + + For a Relative Joining Fetch, the publisher sets the Start Location + to {Subscribe Largest Location.Group - Joining Start, 0}. + + For an Absolute Joining Fetch, the publisher sets the Start Location + to Joining Start. + +9.16.3. Fetch Handling + + The format of FETCH is as follows: + + + + + + + + + + + + + + + +Nandakumar, et al. Expires 6 March 2026 [Page 61] + +Internet-Draft moq-transport September 2025 + + + FETCH Message { + Type (i) = 0x16, + Length (16), + Request ID (i), + Subscriber Priority (8), + Group Order (8), + Fetch Type (i), + [Standalone (Standalone Fetch)], + [Joining (Joining Fetch)], + Number of Parameters (i), + Parameters (..) ... + } + + Figure 18: MOQT FETCH Message + + * Request ID: See Section 9.1. + + * Subscriber Priority: Specifies the priority of a fetch request + relative to other subscriptions or fetches in the same session. + Lower numbers get higher priority. See Section 7. + + * Group Order: Allows the subscriber to request Objects be delivered + in Ascending (0x1) or Descending (0x2) order by group. See + Section 7. A value of 0x0 indicates the original publisher's + Group Order SHOULD be used. Values larger than 0x2 are a protocol + error. + + * Fetch Type: Identifies the type of Fetch, whether Standalone, + Relative Joining or Absolute Joining. + + * Standalone: Standalone Fetch structure included when Fetch Type is + 0x1 + + * Joining: Joining Fetch structure included when Fetch Type is 0x2 + or 0x3. + + * Parameters: The parameters are defined in Section 9.2.1. + + A publisher responds to a FETCH request with either a FETCH_OK or a + FETCH_ERROR message. The publisher creates a new unidirectional + stream that is used to send the Objects. The FETCH_OK or FETCH_ERROR + can come at any time relative to object delivery. + + The publisher responding to a FETCH is responsible for delivering all + available Objects in the requested range in the requested order. The + Objects in the response are delivered on a single unidirectional + stream. Any gaps in the Group and Object IDs in the response stream + indicate objects that do not exist (eg: they implicitly have status + + + +Nandakumar, et al. Expires 6 March 2026 [Page 62] + +Internet-Draft moq-transport September 2025 + + + Object Does Not Exist). For Ascending Group Order this includes + ranges between the first requested object and the first object in the + stream; between objects in the stream; and between the last object in + the stream and the Largest Group/Object indicated in FETCH_OK, so + long as the fetch stream is terminated by a FIN. If no Objects exist + in the requested range, the publisher returns FETCH_ERROR with code + NO_OBJECTS. + + A relay that has cached objects from the beginning of the range MAY + start sending objects immediately in response to a FETCH. If it + encounters an object in the requested range that is not cached and + has unknown status, the relay MUST pause subsequent delivery until it + has confirmed the object's status upstream. If the upstream FETCH + fails, the relay sends a FETCH_ERROR and can reset the unidirectional + stream. It can choose to do so immediately or wait until the cached + objects have been delivered before resetting the stream. + + The Object Forwarding Preference does not apply to fetches. + + Fetch specifies an inclusive range of Objects starting at Start + Location and ending at End Location. End Location MUST specify the + same or a larger Location than Start Location for Standalone and + Absolute Joining Fetches. + + Objects that are not yet published will not be retrieved by a FETCH. + The Largest available Object in the requested range is indicated in + the FETCH_OK, and is the last Object a fetch will return if the End + Location have not yet been published. + + If Start Location is greater than the Largest Object (Section 9.7) + the publisher MUST return FETCH_ERROR with error code INVALID_RANGE. + + A publisher MUST send fetched groups in the determined group order, + either ascending or descending. Within each group, objects are sent + in Object ID order; subgroup ID is not used for ordering. + + If an Original Publisher receives a FETCH with a range that includes + an object with unknown status, it MUST return FETCH_ERROR with code + UNKNOWN_STATUS_IN_RANGE. + +9.17. FETCH_OK + + A publisher sends a FETCH_OK control message in response to + successful fetches. A publisher MAY send Objects in response to a + FETCH before the FETCH_OK message is sent, but the FETCH_OK MUST NOT + be sent until the End Location is known. + + + + + +Nandakumar, et al. Expires 6 March 2026 [Page 63] + +Internet-Draft moq-transport September 2025 + + + FETCH_OK Message { + Type (i) = 0x18, + Length (16), + Request ID (i), + Group Order (8), + End Of Track (8), + End Location (Location), + Number of Parameters (i), + Parameters (..) ... + } + + Figure 19: MOQT FETCH_OK Message + + * Request ID: The Request ID of the FETCH this message is replying + to Section 9.7. + + * Group Order: Indicates the fetch will be delivered in Ascending + (0x1) or Descending (0x2) order by group. See Section 7. Values + of 0x0 and those larger than 0x2 are a protocol error. + + * End Of Track: 1 if all Objects have been published on this Track, + and the End Location is the final Object in the Track, 0 if not. + + * End Location: The largest object covered by the FETCH response. + The End Location is determined as follows: + + - If the requested FETCH End Location was beyond the Largest + known (possibly final) Object, End Location is {Largest.Group, + Largest.Object + 1} + + - If End Location.Object in the FETCH request was 0 and the + response covers the last Object in the Group, End Location is + {Fetch.End Location.Group, 0} + + - Otherwise, End Location is Fetch.End Location Where Fetch.End + Location is either Fetch.Standalone.End Location or the + computed End Location described in Section 9.16.2.1. + + If the relay is subscribed to the track, it uses its knowledge of + the largest {Group, Object} to set End Location. If it is not + subscribed and the requested End Location exceeds its cached data, + the relay makes an upstream request to complete the FETCH, and + uses the upstream response to set End Location. + + If End Location is smaller than the Start Location in the + corresponding FETCH the receiver MUST close the session with + PROTOCOL_VIOLATION. + + + + +Nandakumar, et al. Expires 6 March 2026 [Page 64] + +Internet-Draft moq-transport September 2025 + + + * Parameters: The parameters are defined in Section 9.2.1. + +9.18. FETCH_ERROR + + A publisher sends a FETCH_ERROR control message in response to a + failed FETCH. + + FETCH_ERROR Message { + Type (i) = 0x19, + Length (16), + Request ID (i), + Error Code (i), + Error Reason (Reason Phrase) + } + + Figure 20: MOQT FETCH_ERROR Message + + * Request ID: The Request ID of the FETCH this message is replying + to Section 9.7. + + * Error Code: Identifies an integer error code for fetch failure. + + * Error Reason: Provides the reason for fetch error. See + Section 1.4.3. + + The application SHOULD use a relevant error code in FETCH_ERROR, as + defined below: + + INTERNAL_ERROR (0x0): An implementation specific or generic error + occurred. + + UNAUTHORIZED (0x1): The subscriber is not authorized to fetch from + the given track. + + TIMEOUT (0x2): The fetch could not be completed before an + implementation specific timeout. For example, a relay could not + FETCH missing objects within the timeout. + + NOT_SUPPORTED (0x3): The endpoint does not support the FETCH method. + + TRACK_DOES_NOT_EXIST (0x4): The requested track is not available at + the publisher. + + INVALID_RANGE (0x5): The end of the requested range is earlier than + the beginning, the start of the requested range is beyond the + Largest Location, or the track has not published any Objects yet. + + NO_OBJECTS (0x6): No Objects exist between the requested Start and + + + +Nandakumar, et al. Expires 6 March 2026 [Page 65] + +Internet-Draft moq-transport September 2025 + + + End Locations. + + INVALID_JOINING_REQUEST_ID (0x7): The joining Fetch referenced a + Request ID that did not belong to an active Subscription. + + UNKNOWN_STATUS_IN_RANGE (0x8): The requested range contains objects + with unknown status. + + MALFORMED_TRACK (0x9): A relay publisher detected the track was + malformed (see Section 2.5). + + MALFORMED_AUTH_TOKEN (0x10): Invalid Auth Token serialization during + registration (see Section 9.2.1.1). + + EXPIRED_AUTH_TOKEN (0x12): Authorization token has expired + (Section 9.2.1.1). + +9.19. FETCH_CANCEL + + A subscriber sends a FETCH_CANCEL message to a publisher to indicate + it is no longer interested in receiving objects for the fetch + identified by the 'Request ID'. The publisher SHOULD promptly close + the unidirectional stream, even if it is in the middle of delivering + an object. + + The format of FETCH_CANCEL is as follows: + + FETCH_CANCEL Message { + Type (i) = 0x17, + Length (16), + Request ID (i) + } + + Figure 21: MOQT FETCH_CANCEL Message + + * Request ID: The Request ID of the FETCH (Section 9.16) this + message is cancelling. + +9.20. TRACK_STATUS + + A potential subscriber sends a TRACK_STATUS message on the control + stream to obtain information about the current status of a given + track. + + The TRACK_STATUS message format is identical to the SUBSCRIBE message + (Section 9.7). + + + + + +Nandakumar, et al. Expires 6 March 2026 [Page 66] + +Internet-Draft moq-transport September 2025 + + + The receiver of a TRACK_STATUS message treats it identically as if it + had received a SUBSCRIBE message, except it does not create + downstream subscription state or send any Objects. Relays without an + active subscription MAY forward TRACK_STATUS to one or more + publishers, or MAY initiate a subscription (subject to authorization) + as described in Section 8.4 to determine the response. The publisher + does not send PUBLISH_DONE for this request, and the subscriber + cannot send SUBSCRIBE_UPDATE or UNSUBSCRIBE. + +9.21. TRACK_STATUS_OK + + The publisher sends a TRACK_STATUS_OK control message in response to + a successful TRACK_STATUS message. + + The TRACK_STATUS_OK message format is identical to the SUBSCRIBE_OK + message (Section 9.8). + + The publisher populates the fields of TRACK_STATUS_OK exactly as it + would have populated a SUBSCRIBE_OK, setting Track Alias to 0. It is + not considered an error if Track Alias 0 is already in use by an + active subscription. + +9.22. TRACK_STATUS_ERROR + + The publisher sends a TRACK_STATUS_ERROR control message in response + to a failed TRACK_STATUS message. + + The TRACK_STATUS_ERROR message format is identical to the + SUBSCRIBE_ERROR message (Section 9.9). + + The publisher populates the fields of TRACK_STATUS_ERROR exactly as + it would have populated a SUBSCRIBE_ERROR. + +9.23. PUBLISH_NAMESPACE + + The publisher sends the PUBLISH_NAMESPACE control message to + advertise that it has tracks available within a Track Namespace. The + receiver verifies the publisher is authorized to publish tracks under + this namespace. + + PUBLISH_NAMESPACE Message { + Type (i) = 0x6, + Length (16), + Request ID (i), + Track Namespace (tuple), + Number of Parameters (i), + Parameters (..) ..., + } + + + +Nandakumar, et al. Expires 6 March 2026 [Page 67] + +Internet-Draft moq-transport September 2025 + + + Figure 22: MOQT PUBLISH_NAMESPACE Message + + * Request ID: See Section 9.1. + + * Track Namespace: Identifies a track's namespace as defined in + (Section 2.4.1) + + * Parameters: The parameters are defined in Section 9.2.1. + +9.24. PUBLISH_NAMESPACE_OK + + The subscriber sends a PUBLISH_NAMESPACE_OK control message to + acknowledge the successful authorization and acceptance of a + PUBLISH_NAMESPACE message. + + PUBLISH_NAMESPACE_OK Message { + Type (i) = 0x7, + Length (16), + Request ID (i) + } + + Figure 23: MOQT PUBLISH_NAMESPACE_OK Message + + * Request ID: The Request ID of the PUBLISH_NAMESPACE this message + is replying to Section 9.23. + +9.25. PUBLISH_NAMESPACE_ERROR + + The subscriber sends a PUBLISH_NAMESPACE_ERROR control message for + tracks that failed authorization. + + PUBLISH_NAMESPACE_ERROR Message { + Type (i) = 0x8, + Length (16), + Request ID (i), + Error Code (i), + Error Reason (Reason Phrase) + } + + Figure 24: MOQT PUBLISH_NAMESPACE_ERROR Message + + * Request ID: The Request ID of the PUBLISH_NAMESPACE this message + is replying to Section 9.23. + + * Error Code: Identifies an integer error code for publish namespace + failure. + + + + + +Nandakumar, et al. Expires 6 March 2026 [Page 68] + +Internet-Draft moq-transport September 2025 + + + * Error Reason: Provides the reason for publish namespace error. + See Section 1.4.3. + + The application SHOULD use a relevant error code in + PUBLISH_NAMESPACE_ERROR, as defined below: + + INTERNAL_ERROR (0x0): An implementation specific or generic error + occurred. + + UNAUTHORIZED (0x1): The subscriber is not authorized to announce the + given namespace. + + TIMEOUT (0x2): The announce could not be completed before an + implementation specific timeout. + + NOT_SUPPORTED (0x3): The endpoint does not support the + PUBLISH_NAMESPACE method. + + UNINTERESTED (0x4): The namespace is not of interest to the + endpoint. + + MALFORMED_AUTH_TOKEN (0x10): Invalid Auth Token serialization during + registration (see Section 9.2.1.1). + + EXPIRED_AUTH_TOKEN (0x12): Authorization token has expired + (Section 9.2.1.1). + +9.26. PUBLISH_NAMESPACE_DONE + + The publisher sends the PUBLISH_NAMESPACE_DONE control message to + indicate its intent to stop serving new subscriptions for tracks + within the provided Track Namespace. + + PUBLISH_NAMESPACE_DONE Message { + Type (i) = 0x9, + Length (16), + Track Namespace (tuple), + } + + Figure 25: MOQT PUBLISH_NAMESPACE_DONE Message + + * Track Namespace: Identifies a track's namespace as defined in + (Section 2.4.1). + + + + + + + + +Nandakumar, et al. Expires 6 March 2026 [Page 69] + +Internet-Draft moq-transport September 2025 + + +9.27. PUBLISH_NAMESPACE_CANCEL + + The subscriber sends an PUBLISH_NAMESPACE_CANCEL control message to + indicate it will stop sending new subscriptions for tracks within the + provided Track Namespace. + + PUBLISH_NAMESPACE_CANCEL Message { + Type (i) = 0xC, + Length (16), + Track Namespace (tuple), + Error Code (i), + Error Reason (Reason Phrase), + } + + Figure 26: MOQT PUBLISH_NAMESPACE_CANCEL Message + + * Track Namespace: Identifies a track's namespace as defined in + (Section 2.4.1). + + * Error Code: Identifies an integer error code for canceling the + publish. PUBLISH_NAMESPACE_CANCEL uses the same error codes as + PUBLISH_NAMESPACE_ERROR (Section 9.25). + + * Error Reason: Provides the reason for publish cancelation. See + Section 1.4.3. + +9.28. SUBSCRIBE_NAMESPACE + + The subscriber sends the SUBSCRIBE_NAMESPACE control message to a + publisher to request the current set of matching published namespaces + and established subscriptions, as well as future updates to the set. + + SUBSCRIBE_NAMESPACE Message { + Type (i) = 0x11, + Length (16), + Request ID (i), + Track Namespace Prefix (tuple), + Number of Parameters (i), + Parameters (..) ..., + } + + Figure 27: MOQT SUBSCRIBE_NAMESPACE Message + + * Request ID: See Section 9.1. + + * Track Namespace Prefix: An ordered N-Tuple of byte fields which + are matched against track namespaces known to the publisher. For + example, if the publisher is a relay that has received + + + +Nandakumar, et al. Expires 6 March 2026 [Page 70] + +Internet-Draft moq-transport September 2025 + + + PUBLISH_NAMESPACE messages for namespaces ("example.com", + "meeting=123", "participant=100") and ("example.com", + "meeting=123", "participant=200"), a SUBSCRIBE_NAMESPACE for + ("example.com", "meeting=123") would match both. If an endpoint + receives a Track Namespace Prefix tuple with an N of 0 or more + than 32, it MUST close the session with a Protocol Violation. + + * Parameters: The parameters are defined in Section 9.2.1. + + The publisher will respond with SUBSCRIBE_NAMESPACE_OK or + SUBSCRIBE_NAMESPACE_ERROR. If the SUBSCRIBE_NAMESPACE is successful, + the publisher will immediately forward existing PUBLISH_NAMESPACE and + PUBLISH messages that match the Track Namespace Prefix that have not + already been sent to this subscriber. If the set of matching + PUBLISH_NAMESPACE messages changes, the publisher sends the + corresponding PUBLISH_NAMESPACE or PUBLISH_NAMESPACE_DONE message. + + A subscriber cannot make overlapping namespace subscriptions on a + single session. Within a session, if a publisher receives a + SUBSCRIBE_NAMESPACE with a Track Namespace Prefix that is a prefix + of, suffix of, or equal to an active SUBSCRIBE_NAMESPACE, it MUST + respond with SUBSCRIBE_NAMESPACE_ERROR, with error code + NAMESPACE_PREFIX_OVERLAP. + + The publisher MUST ensure the subscriber is authorized to perform + this namespace subscription. + + SUBSCRIBE_NAMESPACE is not required for a publisher to send + PUBLISH_NAMESPACE, PUBLISH_NAMESPACE_DONE or PUBLISH messages to a + subscriber. It is useful in applications or relays where subscribers + are only interested in or authorized to access a subset of available + namespaces and tracks. + +9.29. SUBSCRIBE_NAMESPACE_OK + + A publisher sends a SUBSCRIBE_NAMESPACE_OK control message for + successful namespace subscriptions. + + SUBSCRIBE_NAMESPACE_OK Message { + Type (i) = 0x12, + Length (16), + Request ID (i), + } + + Figure 28: MOQT SUBSCRIBE_NAMESPACE_OK Message + + * Request ID: The Request ID of the SUBSCRIBE_NAMESPACE this message + is replying to Section 9.28. + + + +Nandakumar, et al. Expires 6 March 2026 [Page 71] + +Internet-Draft moq-transport September 2025 + + +9.30. SUBSCRIBE_NAMESPACE_ERROR + + A publisher sends a SUBSCRIBE_NAMESPACE_ERROR control message in + response to a failed SUBSCRIBE_NAMESPACE. + + SUBSCRIBE_NAMESPACE_ERROR Message { + Type (i) = 0x13, + Length (16), + Request ID (i), + Error Code (i), + Error Reason (Reason Phrase) + } + + Figure 29: MOQT SUBSCRIBE_NAMESPACE_ERROR Message + + * Request ID: The Request ID of the SUBSCRIBE_NAMESPACE this message + is replying to Section 9.28. + + * Error Code: Identifies an integer error code for the namespace + subscription failure. + + * Error Reason: Provides the reason for the namespace subscription + error. See Section 1.4.3. + + The application SHOULD use a relevant error code in + SUBSCRIBE_NAMESPACE_ERROR, as defined below: + + INTERNAL_ERROR (0x0): An implementation specific or generic error + occurred. + + UNAUTHORIZED (0x1): The subscriber is not authorized to subscribe to + the given namespace prefix. + + TIMEOUT (0x2): The operation could not be completed before an + implementation specific timeout. + + NOT_SUPPORTED (0x3): The endpoint does not support the + SUBSCRIBE_NAMESPACE method. + + NAMESPACE_PREFIX_UNKNOWN (0x4): The namespace prefix is not + available for subscription. + + NAMESPACE_PREFIX_OVERLAP (0x5): The namespace prefix overlaps with + another SUBSCRIBE_NAMESPACE in the same session. + + MALFORMED_AUTH_TOKEN (0x10): Invalid Auth Token serialization during + registration (see Section 9.2.1.1). + + + + +Nandakumar, et al. Expires 6 March 2026 [Page 72] + +Internet-Draft moq-transport September 2025 + + + EXPIRED_AUTH_TOKEN (0x12): Authorization token has expired + (Section 9.2.1.1). + +9.31. UNSUBSCRIBE_NAMESPACE + + A subscriber issues a UNSUBSCRIBE_NAMESPACE message to a publisher + indicating it is no longer interested in PUBLISH_NAMESPACE, + PUBLISH_NAMESPACE_DONE and PUBLISH messages for the specified track + namespace prefix. + + The format of UNSUBSCRIBE_NAMESPACE is as follows: + + UNSUBSCRIBE_NAMESPACE Message { + Type (i) = 0x14, + Length (16), + Track Namespace Prefix (tuple) + } + + Figure 30: MOQT UNSUBSCRIBE_NAMESPACE Message + + * Track Namespace Prefix: As defined in Section 9.28. + +10. Data Streams and Datagrams + + A publisher sends Objects matching a subscription on Data Streams or + Datagrams. + + All unidirectional MOQT streams start with a variable-length integer + indicating the type of the stream in question. + + +===========+==================================+ + | ID | Type | + +===========+==================================+ + | 0x10-0x1D | SUBGROUP_HEADER (Section 10.4.2) | + +-----------+----------------------------------+ + | 0x05 | FETCH_HEADER (Section 10.4.4) | + +-----------+----------------------------------+ + + Table 4 + + All MOQT datagrams start with a variable-length integer indicating + the type of the datagram. + + + + + + + + + +Nandakumar, et al. Expires 6 March 2026 [Page 73] + +Internet-Draft moq-transport September 2025 + + + +===================+==================================+ + | ID | Type | + +===================+==================================+ + | 0x00-0x07,0x20-21 | OBJECT_DATAGRAM (Section 10.3.1) | + +-------------------+----------------------------------+ + + Table 5 + + An endpoint that receives an unknown stream or datagram type MUST + close the session. + + Every Track has a single 'Object Forwarding Preference' and the + Original Publisher MUST NOT mix different forwarding preferences + within a single track (see Section 2.5). + +10.1. Track Alias + + To optimize wire efficiency, Subgroups and Datagrams refer to a track + by a numeric identifier, rather than the Full Track Name. Track + Alias is chosen by the publisher and included in SUBSCRIBE_OK + (Section 9.8) or PUBLISH (Section 9.13). + + Objects can arrive after a subscription has been cancelled. + Subscribers SHOULD retain sufficient state to quickly discard these + unwanted Objects, rather than treating them as belonging to an + unknown Track Alias. + +10.2. Objects + + An Object contains a range of contiguous bytes from the specified + track, as well as associated metadata required to deliver, cache, and + forward it. Objects are sent by publishers. + +10.2.1. Canonical Object Properties + + A canonical MoQ Object has the following information: + + * Track Namespace and Track Name: The track this object belongs to. + + * Group ID: The object is a member of the indicated group ID + Section 2.3 within the track. + + * Object ID: The order of the object within the group. + + * Publisher Priority: An 8 bit integer indicating the publisher's + priority for the Object Section 7. + + + + + +Nandakumar, et al. Expires 6 March 2026 [Page 74] + +Internet-Draft moq-transport September 2025 + + + * Object Forwarding Preference: An enumeration indicating how a + publisher sends an object. The preferences are Subgroup and + Datagram. When in response to a SUBSCRIBE, an Object MUST be sent + according to its Object Forwarding Preference, described below. + + * Subgroup ID: The object is a member of the indicated subgroup ID + (Section 2.2) within the group. This field is omitted if the + Object Forwarding Preference is Datagram. + + * Object Status: As enumeration used to indicate missing objects or + mark the end of a group or track. See Section 10.2.1.1 below. + + * Object Extension Length: The total length of the Object Extension + Headers block, in bytes. + + * Object Extensions : A sequence of Object Extension Headers. See + Section 10.2.1.2 below. + + * Object Payload: An opaque payload intended for an End Subscriber + and SHOULD NOT be processed by a relay. Only present when 'Object + Status' is Normal (0x0). + +10.2.1.1. Object Status + + The Object Status informs subscribers what objects will not be + received because they were never produced, are no longer available, + or because they are beyond the end of a group or track. + + Status can have following values: + + * 0x0 := Normal object. This status is implicit for any non-zero + length object. Zero-length objects explicitly encode the Normal + status. + + * 0x1 := Indicates Object Does Not Exist. Indicates that this + Object does not exist at any publisher and it will not be + published in the future. This SHOULD be cached. + + * 0x3 := Indicates End of Group. Object ID is one greater than the + largest Object produced in the Group identified by the Group ID. + If the Object ID is 0, it indicates there are no Objects in this + Group. This SHOULD be cached. A publisher MAY use an end of + Group object to signal the end of all open Subgroups in a Group. + A non-zero-length Object can be the End of Group, as signaled in + the DATAGRAM or SUBGROUP_HEADER Type field (see Section 10.3.1 and + Section 10.4.2). + + + + + +Nandakumar, et al. Expires 6 March 2026 [Page 75] + +Internet-Draft moq-transport September 2025 + + + * 0x4 := Indicates End of Track. Group ID is either the largest + Group produced in this Track with Object ID one greater than the + largest Object produced in that Group, or Group ID is one greater + than the largest Group produced in this Track with Object ID zero. + This status also indicates the specified Group has ended. + Publishers MUST NOT publish an Object with a Location larger than + this Location (see Section 2.5). This SHOULD be cached. + + Any other value SHOULD be treated as a protocol error and the session + SHOULD be terminated with a PROTOCOL_VIOLATION (Section 3.4). Any + object with a status code other than zero MUST have an empty payload. + +10.2.1.2. Object Extension Header + + Any Object may have extension headers except those with Object Status + 'Object Does Not Exist'. If an endpoint receives a non-existent + Object containing extension headers it MUST close the session with a + PROTOCOL_VIOLATION. + + Object Extension Headers are visible to relays and allow the + transmission of future metadata relevant to MOQT Object distribution. + Any Object metadata never accessed by the transport or relays SHOULD + be serialized as part of the Object payload and not as an extension + header. + + Extension Headers are defined in external specifications and + registered in an IANA table Section 13. These specifications define + the type and value of the header, along with any rules concerning + processing, modification, caching and forwarding. A relay which is + coded to implement these rules is said to "support" the extension. + + If unsupported by the relay, Extension Headers MUST NOT be modified, + MUST be cached as part of the Object and MUST be forwarded by relays. + + If supported by the relay and subject to the processing rules + specified in the definition of the extension, Extension Headers MAY + be modified, added, removed, and/or cached by relays. + + Object Extension Headers are serialized as Key-Value-Pairs (see + Figure 2). + + Header types are registered in the IANA table 'MOQ Extension + Headers'. See Section 13. + + + + + + + + +Nandakumar, et al. Expires 6 March 2026 [Page 76] + +Internet-Draft moq-transport September 2025 + + +10.3. Datagrams + + A single object can be conveyed in a datagram. The Track Alias field + Section 10.1 indicates the track this Datagram belongs to. If an + endpoint receives a datagram with an unknown Track Alias, it MAY drop + the datagram or choose to buffer it for a brief period to handle + reordering with the control message that establishes the Track Alias. + + An Object received in an OBJECT_DATAGRAM or OBJECT_DATAGRAM_STATUS + message has an Object Forwarding Preference = Datagram. + + To send an Object with Object Forwarding Preference = Datagram, + determine the length of the header and payload and send the Object as + datagram. When the total size is larger than maximum datagram size + for the session, the Object will be dropped without any explicit + notification. + + Each session along the path between the Original Publisher and End + Subscriber might have different maximum datagram sizes. + Additionally, Object Extension Headers (Section 10.2.1.2) can be + added to Objects as they pass through the MOQT network, increasing + the size of the Object and the chances it will exceed the maximum + datagram size of a downstream session and be dropped. + +10.3.1. Object Datagram + + An OBJECT_DATAGRAM carries a single object in a datagram. + + OBJECT_DATAGRAM { + Type (i) = 0x0-0x7,0x20-21 + Track Alias (i), + Group ID (i), + [Object ID (i),] + Publisher Priority (8), + [Extension Headers Length (i), + Extension headers (...)], + [Object Status (i),] + [Object Payload (..),] + } + + Figure 31: MOQT OBJECT_DATAGRAM + + The Type value determines which fields are present in the + OBJECT_DATAGRAM. There are 10 defined Type values for + OBJECT_DATAGRAM. + + + + + + +Nandakumar, et al. Expires 6 March 2026 [Page 77] + +Internet-Draft moq-transport September 2025 + + + +======+==============+============+===========+==================+ + | Type | End Of Group | Extensions | Object ID | Status / Payload | + +======+==============+============+===========+==================+ + | | | Present | Present | | + +------+--------------+------------+-----------+------------------+ + | 0x00 | No | No | Yes | Payload | + +------+--------------+------------+-----------+------------------+ + | 0x01 | No | Yes | Yes | Payload | + +------+--------------+------------+-----------+------------------+ + | 0x02 | Yes | No | Yes | Payload | + +------+--------------+------------+-----------+------------------+ + | 0x03 | Yes | Yes | Yes | Payload | + +------+--------------+------------+-----------+------------------+ + | 0x04 | No | No | No | Payload | + +------+--------------+------------+-----------+------------------+ + | 0x05 | No | Yes | No | Payload | + +------+--------------+------------+-----------+------------------+ + | 0x06 | Yes | No | No | Payload | + +------+--------------+------------+-----------+------------------+ + | 0x07 | Yes | Yes | No | Payload | + +------+--------------+------------+-----------+------------------+ + | 0x20 | No | No | Yes | Status | + +------+--------------+------------+-----------+------------------+ + | 0x21 | No | Yes | Yes | Status | + +------+--------------+------------+-----------+------------------+ + + Table 6 + + * End of Group: For Type values where End of Group is "Yes" the + Object is the last Object in the Group. + + * Extensions Present: If Extensions Present is "Yes" the Extension + Headers Length and Extension headers fields are included. If an + endpoint receives a datagram with Extensions Present as "Yes" and + a Extension Headers Length of 0, it MUST close the session with + PROTOCOL_VIOLATION. + + * Object ID Present: If Object ID Present is No, the Object ID field + is omitted and the Object ID is 0. When Object ID Present is Yes, + the Object ID field is present and encodes the Object ID. + + * Payload and Status: The Object Status field and Object Payload are + mutually exclusive. + + - For Type values 0x00 through 0x07, the Object Payload is + present and the Object Status field is omitted. + + + + + +Nandakumar, et al. Expires 6 March 2026 [Page 78] + +Internet-Draft moq-transport September 2025 + + + There is no explicit length field for the Object Payload. The + entirety of the transport datagram following the Object header + fields contains the payload. + + - For Type values 0x20 and 0x21, the Object Status field is + present and there is no Object Payload. + +10.4. Streams + + When Objects are sent on streams, the stream begins with a Subgroup + or Fetch Header and is followed by one or more sets of serialized + Object fields. If a stream ends gracefully (i.e., the stream + terminates with a FIN) in the middle of a serialized Object, the + session SHOULD be terminated with a PROTOCOL_VIOLATION. + + A publisher SHOULD NOT open more than one stream at a time with the + same Subgroup Header field values. + +10.4.1. Stream Cancellation + + Streams aside from the control stream MAY be canceled due to + congestion or other reasons by either the publisher or subscriber. + Early termination of a stream does not affect the MoQ application + state, and therefore has no effect on outstanding subscriptions. + +10.4.2. Subgroup Header + + All Objects on a Subgroup stream belong to the track identified by + Track Alias (see Section 10.1) and the Subgroup indicated by 'Group + ID' and Subgroup ID in the SUBGROUP_HEADER. + + If an endpoint receives a subgroup with an unknown Track Alias, it + MAY abandon the stream, or choose to buffer it for a brief period to + handle reordering with the control message that establishes the Track + Alias. The endpoint MAY withhold stream flow control beyond the + SUBGROUP_HEADER until the Track Alias has been established. To + prevent deadlocks, the publisher MUST allocate connection flow + control to the control stream before allocating it any data streams. + Otherwise, a receiver might wait for a control message containing a + Track Alias to release flow control, while the sender waits for flow + control to send the message. + + + + + + + + + + +Nandakumar, et al. Expires 6 March 2026 [Page 79] + +Internet-Draft moq-transport September 2025 + + + SUBGROUP_HEADER { + Type (i) = 0x10..0x1D, + Track Alias (i), + Group ID (i), + [Subgroup ID (i),] + Publisher Priority (8), + } + + Figure 32: MOQT SUBGROUP_HEADER + + All Objects received on a stream opened with SUBGROUP_HEADER have an + Object Forwarding Preference = Subgroup. + + There are 12 defined Type values for SUBGROUP_HEADER: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Nandakumar, et al. Expires 6 March 2026 [Page 80] + +Internet-Draft moq-transport September 2025 + + + +======+===============+=============+============+==============+ + | Type | Subgroup ID | Subgroup ID | Extensions | Contains End | + +======+===============+=============+============+==============+ + | | Field Present | Value | Present | of Group | + +------+---------------+-------------+------------+--------------+ + | 0x10 | No | 0 | No | No | + +------+---------------+-------------+------------+--------------+ + | 0x11 | No | 0 | Yes | No | + +------+---------------+-------------+------------+--------------+ + | 0x12 | No | First | No | No | + | | | Object ID | | | + +------+---------------+-------------+------------+--------------+ + | 0x13 | No | First | Yes | No | + | | | Object ID | | | + +------+---------------+-------------+------------+--------------+ + | 0x14 | Yes | N/A | No | No | + +------+---------------+-------------+------------+--------------+ + | 0x15 | Yes | N/A | Yes | No | + +------+---------------+-------------+------------+--------------+ + | 0x18 | No | 0 | No | Yes | + +------+---------------+-------------+------------+--------------+ + | 0x19 | No | 0 | Yes | Yes | + +------+---------------+-------------+------------+--------------+ + | 0x1A | No | First | No | Yes | + | | | Object ID | | | + +------+---------------+-------------+------------+--------------+ + | 0x1B | No | First | Yes | Yes | + | | | Object ID | | | + +------+---------------+-------------+------------+--------------+ + | 0x1C | Yes | N/A | No | Yes | + +------+---------------+-------------+------------+--------------+ + | 0x1D | Yes | N/A | Yes | Yes | + +------+---------------+-------------+------------+--------------+ + + Table 7 + + For Type values where Contains End of Group is Yes, the last Object + in this Subgroup stream before a FIN is the last Object in the Group. + If the Subgroup stream is terminated with a RESET_STREAM or + RESET_STREAM_AT, the receiver cannot determine the End of Group + Object ID. + + For Type values where Subgroup ID Field Present is No, there is no + explicit Subgroup ID field in the header and the Subgroup ID is + either 0 (for Types 0x10-11 and 0x18-19) or the Object ID of the + first object transmitted in this subgroup (for Types 0x12-13 and + 0x1A-1B). + + + + +Nandakumar, et al. Expires 6 March 2026 [Page 81] + +Internet-Draft moq-transport September 2025 + + + For Type values where Extensions Present is No, Extensions Headers + Length is not present and all Objects have no extensions. When + Extensions Present is Yes, Extension Headers Length is present in all + Objects in this subgroup. Objects with no extensions set Extension + Headers Length to 0. + + To send an Object with Object Forwarding Preference = Subgroup, find + the open stream that is associated with the subscription, Group ID + and Subgroup ID, or open a new one and send the SUBGROUP_HEADER. + Then serialize the following fields. + + The Object Status field is only sent if the Object Payload Length is + zero. + + The Object ID Delta + 1 is added to the previous Object ID in the + Subgroup stream if there was one. The Object ID is the Object ID + Delta if it's the first Object in the Subgroup stream. For example, + a Subgroup of sequential Object IDs starting at 0 will have 0 for all + Object ID Delta values. A consumer cannot infer information about + the existence of Objects between the current and previous Object ID + in the Subgroup (e.g. when Object ID Delta is non-zero) unless there + is an Prior Object ID Gap extesnion header (see Section 11.3). + + { + Object ID Delta (i), + [Extension Headers Length (i), + Extension headers (...)], + Object Payload Length (i), + [Object Status (i)], + Object Payload (..), + } + + Figure 33: MOQT Subgroup Object Fields + +10.4.3. Closing Subgroup Streams + + Subscribers will often need to know if they have received all objects + in a Subgroup, particularly if they serve as a relay or cache. QUIC + and Webtransport streams provide signals that can be used for this + purpose. Closing Subgroups promptly frees system resources and often + unlocks flow control credit to open more streams. + + If a sender has delivered all objects in a Subgroup to the QUIC + stream, except any Objects with Locations smaller than the + subscription's Start Location, it MUST close the stream with a FIN. + + + + + + +Nandakumar, et al. Expires 6 March 2026 [Page 82] + +Internet-Draft moq-transport September 2025 + + + If a sender closes the stream before delivering all such objects to + the QUIC stream, it MUST use a RESET_STREAM or RESET_STREAM_AT + [I-D.draft-ietf-quic-reliable-stream-reset] frame. This includes an + open Subgroup exceeding its Delivery Timeout, early termination of + subscription due to an UNSUBSCRIBE message, a publisher's decision to + end the subscription early, or a SUBSCRIBE_UPDATE moving the + subscription's End Group to a smaller Group or the Start Location to + a larger Location. When RESET_STREAM_AT is used, the reliable_size + SHOULD include the stream header so the receiver can identify the + corresponding subscription and accurately account for reset data + streams when handling PUBLISH_DONE (see Section 9.12). Publishers + that reset data streams without using RESET_STREAM_AT with an + appropriate reliable_size can cause subscribers to hold on to + subscription state until a timeout expires. + + A sender might send all objects in a Subgroup and the FIN on a QUIC + stream, and then reset the stream. In this case, the receiving + application would receive the FIN if and only if all objects were + received. If the application receives all data on the stream and the + FIN, it can ignore any RESET_STREAM it receives. + + If a sender will not deliver any objects from a Subgroup, it MAY send + a SUBGROUP_HEADER on a new stream, with no objects, and then send + RESET_STREAM_AT with a reliable_size equal to the length of the + stream header. This explicitly tells the receiver there is an unsent + Subgroup. + + A relay MUST NOT forward an Object on an existing Subgroup stream + unless it is the next Object in that Subgroup. A relay knows that an + Object is the next Object in the Subgroup if at least one of the + following is true: + + * the Object ID is one greater than the previous Object sent on this + Subgroup stream. + + * the Object was received on the same upstream Subgroup stream as + the previously sent Object on the downstream Subgroup stream, with + no other Objects in between. + + * it knows all Object IDs between the current and previous Object + IDs on the Subgroup stream belong to different Subgroups or do not + exist. + + If the relay does not know if an Object is the next Object, it MUST + reset the Subgroup stream and open a new one to forward it. + + + + + + +Nandakumar, et al. Expires 6 March 2026 [Page 83] + +Internet-Draft moq-transport September 2025 + + + Since SUBSCRIBEs always end on a group boundary, an ending + subscription can always cleanly close all its subgroups. A sender + that terminates a stream early for any other reason (e.g., to handoff + to a different sender) MUST use RESET_STREAM or RESET_STREAM_AT. + Senders SHOULD terminate a stream on Group boundaries to avoid doing + so. + + An MOQT implementation that processes a stream FIN is assured it has + received all objects in a subgroup from the start of the + subscription. If a relay, it can forward stream FINs to its own + subscribers once those objects have been sent. A relay MAY treat + receipt of EndOfGroup, GroupDoesNotExist, or EndOfTrack objects as a + signal to close corresponding streams even if the FIN has not + arrived, as further objects on the stream would be a protocol + violation. + + Similarly, an EndOfGroup message indicates the maximum Object ID in + the Group, so if all Objects in the Group have been received, a FIN + can be sent on any stream where the entire subgroup has been sent. + This might be complex to implement. + + Processing a RESET_STREAM or RESET_STREAM_AT means that there might + be other objects in the Subgroup beyond the last one received. A + relay might immediately reset the corresponding downstream stream, or + it might attempt to recover the missing Objects in an effort to send + all the Objects in the subgroups and the FIN. It also might send + RESET_STREAM_AT with reliable_size set to the last Object it has, so + as to reliably deliver the Objects it has while signaling that other + Objects might exist. + + A subscriber MAY send a QUIC STOP_SENDING frame for a subgroup stream + if the Group or Subgroup is no longer of interest to it. The + publisher SHOULD respond with RESET_STREAM or RESET_STREAM_AT. If + RESET_STREAM_AT is sent, note that the receiver has indicated no + interest in the objects, so setting a reliable_size beyond the stream + header is of questionable utility. + + RESET_STREAM and STOP_SENDING on SUBSCRIBE data streams have no + impact on other Subgroups in the Group or the subscription, although + applications might cancel all Subgroups in a Group at once. + + The application SHOULD use a relevant error code in RESET_STREAM or + RESET_STREAM_AT, as defined below: + + INTERNAL_ERROR (0x0): An implementation specific error. + + CANCELLED (0x1): The subscriber requested cancellation via + + + + +Nandakumar, et al. Expires 6 March 2026 [Page 84] + +Internet-Draft moq-transport September 2025 + + + UNSUBSCRIBE, FETCH_CANCEL or STOP_SENDING, or the publisher ended + the subscription, in which case PUBLISH_DONE (Section 9.12) will + have a more detailed status code. + + DELIVERY_TIMEOUT (0x2): The DELIVERY TIMEOUT Section 9.2.1.2 was + exceeded for this stream. + + SESSION_CLOSED (0x3): The publisher session is being closed. + +10.4.4. Fetch Header + + When a stream begins with FETCH_HEADER, all objects on the stream + belong to the track requested in the Fetch message identified by + Request ID. + + FETCH_HEADER { + Type (i) = 0x5, + Request ID (i), + } + + Figure 34: MOQT FETCH_HEADER + + Each object sent on a fetch stream after the FETCH_HEADER has the + following format: + + { + Group ID (i), + Subgroup ID (i), + Object ID (i), + Publisher Priority (8), + Extension Headers Length (i), + [Extension headers (...)], + Object Payload Length (i), + [Object Status (i)], + Object Payload (..), + } + + Figure 35: MOQT Fetch Object Fields + + The Object Status field is only sent if the Object Payload Length is + zero. + + The Subgroup ID field of an object with a Forwarding Preference of + "Datagram" (see Section 10.2.1) is set to the Object ID. + + + + + + + +Nandakumar, et al. Expires 6 March 2026 [Page 85] + +Internet-Draft moq-transport September 2025 + + +10.5. Examples + + Sending a subgroup on one stream: + + Stream = 2 + + SUBGROUP_HEADER { + Type = 0x14 + Track Alias = 2 + Group ID = 0 + Subgroup ID = 0 + Publisher Priority = 0 + } + { + Object ID = 0 + Object Payload Length = 4 + Payload = "abcd" + } + { + Object ID = 1 + Object Payload Length = 4 + Payload = "efgh" + } + + Sending a group on one stream, with the first object containing two + Extension Headers. + + + + + + + + + + + + + + + + + + + + + + + + + +Nandakumar, et al. Expires 6 March 2026 [Page 86] + +Internet-Draft moq-transport September 2025 + + + Stream = 2 + + SUBGROUP_HEADER { + Type = 0x15 + Track Alias = 2 + Group ID = 0 + Subgroup ID = 0 + Publisher Priority = 0 + } + { + Object ID Delta = 0 (Object ID is 0) + Extension Headers Length = 33 + { Type = 4 + Value = 2186796243 + }, + { Type = 77 + Length = 21 + Value = "traceID:123456" + } + Object Payload Length = 4 + Payload = "abcd" + } + { + Object ID Delta = 0 (Object ID is 1) + Extension Headers Length = 0 + Object Payload Length = 4 + Payload = "efgh" + } + +11. Extension Headers + + The following Object Extension Headers are defined in MOQT. + +11.1. Prior Group ID Gap + + Prior Group ID Gap (Extension Header Type 0x3C) is a variable length + integer containing the number of Groups prior to the current Group + that do not and will never exist. This is equivalent to receiving an + End of Group status with Object ID 0 for each skipped Group. For + example, if the Original Publisher is publishing an Object in Group 7 + and knows it will never publish any Objects in Group 8 or Group 9, it + can include Prior Group ID Gap = 2 in any number of Objects in Group + 10, as it sees fit. A Track is considered malformed (see + Section 2.5) if any of the following conditions are detected: + + * An Object contains more than one instance of Prior Group ID Gap + + + + + +Nandakumar, et al. Expires 6 March 2026 [Page 87] + +Internet-Draft moq-transport September 2025 + + + * A Group contains more than one Object with different values for + Prior Group ID Gap + + * An Object has a Prior Group ID Gap larger than the Group ID + + * An endpoint receives an Object with a Prior Group ID Gap covering + an Object it previously received + + * An endpoint receives an Object with a Group ID within a previously + communicated gap + + This extension is optional, as publishers might not know the prior + gap gize, or there may not be a gap. If Prior Group ID Gap is not + present, the receiver cannot infer any information about the + existence of prior groups (see Section 2.3.1). + + This extension can be added by the Original Publisher, but MUST NOT + be added by relays. This extension MUST NOT be modified or removed. + +11.2. Immutable Extensions + + The Immutable Extensions (Extension Header Type 0xB) contains a + sequence of Key-Value-Pairs (see Figure 2) which are also Object + Extension Headers of the Object. + + Immutable Extensions { + Type (0xB), + Length (i), + Key-Value-Pair (..) ... + } + + This extension can be added by the Original Publisher, but MUST NOT + be added by Relays. This extension MUST NOT be modified or removed. + Relays MUST cache this extension if the Object is cached and MUST + forward this extension if the enclosing Object is forwarded. Relays + MAY decode and view these extensions. + + A Track is considered malformed (see Section 2.5) if any of the + following conditions are detected: + + * An Object contains an Immutable Extensions header that contains + another Immutable Extensions key + + * A Key-Value-Pair cannot be parsed + + The following figure shows an example Object structure with a + combination of mutable and immutable extensions and end to end + encrypted metadata in the Object payload. + + + +Nandakumar, et al. Expires 6 March 2026 [Page 88] + +Internet-Draft moq-transport September 2025 + + + Object Header Object Payload +<------------------------------------------------> <-------------------> ++--------+-------+------------+-------+-----------+--------------------+ +| Object | Ext 1 | Immutable | Ext N | [Payload] | Private Extensions | +| Fields | | Extensions | | [Length] | App Payload | ++--------+-------+------------+-------+-----------+--------------------+ + xxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxx + yyyyyyyyyyyyyyyyyyyy +x = e2e Authenticated Data +y = e2e Encrypted Data +EXT 1 and EXT N can be modified or removed by Relays + +11.3. Prior Object ID Gap + + Prior Object ID Gap (Extension Header Type 0x3E) is a variable length + integer containing the number of Objects prior to the current Object + that do not and will never exist. This is equivalent to receiving an + Object Does Not Exist status for each skipped Object ID. For + example, if the Original Publisher is publishing Object 10 in Group 3 + and knows it will never publish Objects 8 or 9 in this Group, it can + include Prior Object ID Gap = 2. A Track is considered malformed + (see Section 2.5) if any of the following conditions are detected: + + * An Object contains more than one instance of Prior Object ID Gap + + * An Object has a Prior Object ID Gap larger than the Object ID + + * An endpoint receives an Object with a Prior Object ID Gap covering + an Object it previously received + + * An endpoint receives an Object with an Object ID within a + previously communicated gap + + This extension is optional, as publishers might not know the prior + gap gize, or there may not be a gap. If Prior Object ID Gap is not + present, the receiver cannot infer any information about the + existence of prior objects (see Section 2.1). + + This extension can be added by the Original Publisher, but MUST NOT + be added by relays. This extension MUST NOT be modified or removed. + +12. Security Considerations + + TODO: Expand this section, including subscriptions. + + TODO: Describe Cache Poisoning attacks + + + + + +Nandakumar, et al. Expires 6 March 2026 [Page 89] + +Internet-Draft moq-transport September 2025 + + +12.1. Resource Exhaustion + + Live content requires significant bandwidth and resources. Failure + to set limits will quickly cause resource exhaustion. + + MOQT uses stream limits and flow control to impose resource limits at + the network layer. Endpoints SHOULD set flow control limits based on + the anticipated bitrate. + + Endpoints MAY impose a MAX STREAM count limit which would restrict + the number of concurrent streams which an application could have in + flight. + + The publisher prioritizes and transmits streams out of order. + Streams might be starved indefinitely during congestion. The + publisher and subscriber MUST cancel a stream, preferably the lowest + priority, after reaching a resource limit. + +12.2. Timeouts + + Implementations are advised to use timeouts to prevent resource + exhaustion attacks by a peer that does not send expected data within + an expected time. Each implementation is expected to set its own + limits. + +12.3. Relay security considerations + +12.3.1. State maintenance + + A Relay SHOULD have mechanisms to prevent malicious endpoints from + flooding it with PUBLISH_NAMESPACE or SUBSCRIBE_NAMESPACE requests + that could bloat data structures. It could use the advertised + MAX_REQUEST_ID to limit the number of such requests, or could have + application-specific policies that can reject incoming + PUBLISH_NAMESPACE or SUBSCRIBE_NAMESPACE requests that cause the + state maintenance for the session to be excessive. + +12.3.2. SUBSCRIBE_NAMESPACE with short prefixes + + A Relay can use authorization rules in order to prevent subscriptions + closer to the root of a large prefix tree. Otherwise, if an entity + sends a relay a SUBSCRIBE_NAMESPACE message with a short prefix, it + can cause the relay to send a large volume of PUBLISH_NAMESPACE + messages. As churn continues in the tree of prefixes, the relay + would have to continue to send PUBLISH_NAMESPACE/ + PUBLISH_NAMESPACE_DONE messages to the entity that had sent the + SUBSCRIBE_NAMESPACE. + + + + +Nandakumar, et al. Expires 6 March 2026 [Page 90] + +Internet-Draft moq-transport September 2025 + + + TODO: Security/Privacy Considerations of MOQT_IMPLEMENTATION + parameter + +13. IANA Considerations + + TODO: fill out currently missing registries: + + * MOQT version numbers + + * Setup parameters + + * Non-setup Parameters - List which params can be repeated in the + table. + + * Message types + + * MOQ Extension headers - we wish to reserve extension types 0-63 + for standards utilization where space is a premium, 64 - 16383 for + standards utilization where space is less of a concern, and 16384 + and above for first-come-first-served non-standardization usage. + List which headers can be repeated in the table. + + * MOQT Auth Token Type + + TODO: register the URI scheme and the ALPN and grease the Extension + types + +13.1. Error Codes + +13.1.1. Session Termination Error Codes + + +============================+======+===============+ + | Name | Code | Specification | + +============================+======+===============+ + | NO_ERROR | 0x0 | Section 3.4 | + +----------------------------+------+---------------+ + | INTERNAL_ERROR | 0x1 | Section 3.4 | + +----------------------------+------+---------------+ + | UNAUTHORIZED | 0x2 | Section 3.4 | + +----------------------------+------+---------------+ + | PROTOCOL_VIOLATION | 0x3 | Section 3.4 | + +----------------------------+------+---------------+ + | INVALID_REQUEST_ID | 0x4 | Section 3.4 | + +----------------------------+------+---------------+ + | DUPLICATE_TRACK_ALIAS | 0x5 | Section 3.4 | + +----------------------------+------+---------------+ + | KEY_VALUE_FORMATTING_ERROR | 0x6 | Section 3.4 | + +----------------------------+------+---------------+ + + + +Nandakumar, et al. Expires 6 March 2026 [Page 91] + +Internet-Draft moq-transport September 2025 + + + | TOO_MANY_REQUESTS | 0x7 | Section 3.4 | + +----------------------------+------+---------------+ + | INVALID_PATH | 0x8 | Section 3.4 | + +----------------------------+------+---------------+ + | MALFORMED_PATH | 0x9 | Section 3.4 | + +----------------------------+------+---------------+ + | GOAWAY_TIMEOUT | 0x10 | Section 3.4 | + +----------------------------+------+---------------+ + | CONTROL_MESSAGE_TIMEOUT | 0x11 | Section 3.4 | + +----------------------------+------+---------------+ + | DATA_STREAM_TIMEOUT | 0x12 | Section 3.4 | + +----------------------------+------+---------------+ + | AUTH_TOKEN_CACHE_OVERFLOW | 0x13 | Section 3.4 | + +----------------------------+------+---------------+ + | DUPLICATE_AUTH_TOKEN_ALIAS | 0x14 | Section 3.4 | + +----------------------------+------+---------------+ + | VERSION_NEGOTIATION_FAILED | 0x15 | Section 3.4 | + +----------------------------+------+---------------+ + | MALFORMED_AUTH_TOKEN | 0x16 | Section 3.4 | + +----------------------------+------+---------------+ + | UNKNOWN_AUTH_TOKEN_ALIAS | 0x17 | Section 3.4 | + +----------------------------+------+---------------+ + | EXPIRED_AUTH_TOKEN | 0x18 | Section 3.4 | + +----------------------------+------+---------------+ + | INVALID_AUTHORITY | 0x19 | Section 3.4 | + +----------------------------+------+---------------+ + | MALFORMED_AUTHORITY | 0x1A | Section 3.4 | + +----------------------------+------+---------------+ + + Table 8 + +13.1.2. SUBSCRIBE_ERROR Codes + + +======================+======+===============+ + | Name | Code | Specification | + +======================+======+===============+ + | INTERNAL_ERROR | 0x0 | Section 9.9 | + +----------------------+------+---------------+ + | UNAUTHORIZED | 0x1 | Section 9.9 | + +----------------------+------+---------------+ + | TIMEOUT | 0x2 | Section 9.9 | + +----------------------+------+---------------+ + | NOT_SUPPORTED | 0x3 | Section 9.9 | + +----------------------+------+---------------+ + | TRACK_DOES_NOT_EXIST | 0x4 | Section 9.9 | + +----------------------+------+---------------+ + | INVALID_RANGE | 0x5 | Section 9.9 | + +----------------------+------+---------------+ + + + +Nandakumar, et al. Expires 6 March 2026 [Page 92] + +Internet-Draft moq-transport September 2025 + + + | MALFORMED_AUTH_TOKEN | 0x10 | Section 9.9 | + +----------------------+------+---------------+ + | EXPIRED_AUTH_TOKEN | 0x12 | Section 9.9 | + +----------------------+------+---------------+ + + Table 9 + +13.1.3. PUBLISH_DONE Codes + + +====================+======+===============+ + | Name | Code | Specification | + +====================+======+===============+ + | INTERNAL_ERROR | 0x0 | Section 9.12 | + +--------------------+------+---------------+ + | UNAUTHORIZED | 0x1 | Section 9.12 | + +--------------------+------+---------------+ + | TRACK_ENDED | 0x2 | Section 9.12 | + +--------------------+------+---------------+ + | SUBSCRIPTION_ENDED | 0x3 | Section 9.12 | + +--------------------+------+---------------+ + | GOING_AWAY | 0x4 | Section 9.12 | + +--------------------+------+---------------+ + | EXPIRED | 0x5 | Section 9.12 | + +--------------------+------+---------------+ + | TOO_FAR_BEHIND | 0x6 | Section 9.12 | + +--------------------+------+---------------+ + | MALFORMED_TRACK | 0x7 | Section 9.12 | + +--------------------+------+---------------+ + + Table 10 + +13.1.4. PUBLISH_ERROR Codes + + +================+======+===============+ + | Name | Code | Specification | + +================+======+===============+ + | INTERNAL_ERROR | 0x0 | Section 9.15 | + +----------------+------+---------------+ + | UNAUTHORIZED | 0x1 | Section 9.15 | + +----------------+------+---------------+ + | TIMEOUT | 0x2 | Section 9.15 | + +----------------+------+---------------+ + | NOT_SUPPORTED | 0x3 | Section 9.15 | + +----------------+------+---------------+ + | UNINTERESTED | 0x4 | Section 9.15 | + +----------------+------+---------------+ + + Table 11 + + + +Nandakumar, et al. Expires 6 March 2026 [Page 93] + +Internet-Draft moq-transport September 2025 + + +13.1.5. FETCH_ERROR Codes + + +============================+======+===============+ + | Name | Code | Specification | + +============================+======+===============+ + | INTERNAL_ERROR | 0x0 | Section 9.18 | + +----------------------------+------+---------------+ + | UNAUTHORIZED | 0x1 | Section 9.18 | + +----------------------------+------+---------------+ + | TIMEOUT | 0x2 | Section 9.18 | + +----------------------------+------+---------------+ + | NOT_SUPPORTED | 0x3 | Section 9.18 | + +----------------------------+------+---------------+ + | TRACK_DOES_NOT_EXIST | 0x4 | Section 9.18 | + +----------------------------+------+---------------+ + | INVALID_RANGE | 0x5 | Section 9.18 | + +----------------------------+------+---------------+ + | NO_OBJECTS | 0x6 | Section 9.18 | + +----------------------------+------+---------------+ + | INVALID_JOINING_REQUEST_ID | 0x7 | Section 9.18 | + +----------------------------+------+---------------+ + | UNKNOWN_STATUS_IN_RANGE | 0x8 | Section 9.18 | + +----------------------------+------+---------------+ + | MALFORMED_TRACK | 0x9 | Section 9.18 | + +----------------------------+------+---------------+ + | MALFORMED_AUTH_TOKEN | 0x10 | Section 9.18 | + +----------------------------+------+---------------+ + | EXPIRED_AUTH_TOKEN | 0x12 | Section 9.18 | + +----------------------------+------+---------------+ + + Table 12 + +13.1.6. ANNOUNCE_ERROR Codes + + +======================+======+===============+ + | Name | Code | Specification | + +======================+======+===============+ + | INTERNAL_ERROR | 0x0 | Section 9.25 | + +----------------------+------+---------------+ + | UNAUTHORIZED | 0x1 | Section 9.25 | + +----------------------+------+---------------+ + | TIMEOUT | 0x2 | Section 9.25 | + +----------------------+------+---------------+ + | NOT_SUPPORTED | 0x3 | Section 9.25 | + +----------------------+------+---------------+ + | UNINTERESTED | 0x4 | Section 9.25 | + +----------------------+------+---------------+ + | MALFORMED_AUTH_TOKEN | 0x10 | Section 9.25 | + + + +Nandakumar, et al. Expires 6 March 2026 [Page 94] + +Internet-Draft moq-transport September 2025 + + + +----------------------+------+---------------+ + | EXPIRED_AUTH_TOKEN | 0x12 | Section 9.25 | + +----------------------+------+---------------+ + + Table 13 + +13.1.7. SUBSCRIBE_NAMESPACE_ERROR Codes + + +==========================+======+===============+ + | Name | Code | Specification | + +==========================+======+===============+ + | INTERNAL_ERROR | 0x0 | Section 9.30 | + +--------------------------+------+---------------+ + | UNAUTHORIZED | 0x1 | Section 9.30 | + +--------------------------+------+---------------+ + | TIMEOUT | 0x2 | Section 9.30 | + +--------------------------+------+---------------+ + | NOT_SUPPORTED | 0x3 | Section 9.30 | + +--------------------------+------+---------------+ + | NAMESPACE_PREFIX_UNKNOWN | 0x4 | Section 9.30 | + +--------------------------+------+---------------+ + | NAMESPACE_PREFIX_OVERLAP | 0x5 | Section 9.30 | + +--------------------------+------+---------------+ + | MALFORMED_AUTH_TOKEN | 0x10 | Section 9.30 | + +--------------------------+------+---------------+ + | EXPIRED_AUTH_TOKEN | 0x12 | Section 9.30 | + +--------------------------+------+---------------+ + + Table 14 + +13.1.8. Data Stream Reset Error Codes + + +==================+======+================+ + | Name | Code | Specification | + +==================+======+================+ + | INTERNAL_ERROR | 0x0 | Section 10.4.3 | + +------------------+------+----------------+ + | CANCELLED | 0x1 | Section 10.4.3 | + +------------------+------+----------------+ + | DELIVERY_TIMEOUT | 0x2 | Section 10.4.3 | + +------------------+------+----------------+ + | SESSION_CLOSED | 0x3 | Section 10.4.3 | + +------------------+------+----------------+ + + Table 15 + + + + + + +Nandakumar, et al. Expires 6 March 2026 [Page 95] + +Internet-Draft moq-transport September 2025 + + +Contributors + + The original design behind this protocol was inspired by three + independent proposals: WARP [I-D.draft-lcurley-warp] by Luke Curley, + RUSH [I-D.draft-kpugin-rush] by Kirill Pugin, Nitin Garg, Alan + Frindell, Jordi Cenzano and Jake Weissman, and QUICR + [I-D.draft-jennings-moq-quicr-proto] by Cullen Jennings, Suhas + Nandakumar and Christian Huitema. The authors of those documents + merged their proposals to create the first draft of moq-transport. + The IETF MoQ Working Group received an enormous amount of support + from many people. The following people provided substantive + contributions to this document: + + * Ali Begen + + * Charles Krasic + + * Christian Huitema + + * Cullen Jennings + + * James Hurley + + * Jordi Cenzano + + * Kirill Pugin + + * Luke Curley + + * Martin Duke + + * Mike English + + * Mo Zanaty + + * Will Law + +References + +Normative References + + [I-D.draft-ietf-quic-reliable-stream-reset] + Seemann, M. and K. Oku, "QUIC Stream Resets with Partial + Delivery", Work in Progress, Internet-Draft, draft-ietf- + quic-reliable-stream-reset-07, 14 June 2025, + . + + + + +Nandakumar, et al. Expires 6 March 2026 [Page 96] + +Internet-Draft moq-transport September 2025 + + + [QUIC] Iyengar, J., Ed. and M. Thomson, Ed., "QUIC: A UDP-Based + Multiplexed and Secure Transport", RFC 9000, + DOI 10.17487/RFC9000, May 2021, + . + + [RFC2119] Bradner, S., "Key words for use in RFCs to Indicate + Requirement Levels", BCP 14, RFC 2119, + DOI 10.17487/RFC2119, March 1997, + . + + [RFC3986] Berners-Lee, T., Fielding, R., and L. Masinter, "Uniform + Resource Identifier (URI): Generic Syntax", STD 66, + RFC 3986, DOI 10.17487/RFC3986, January 2005, + . + + [RFC7301] Friedl, S., Popov, A., Langley, A., and E. Stephan, + "Transport Layer Security (TLS) Application-Layer Protocol + Negotiation Extension", RFC 7301, DOI 10.17487/RFC7301, + July 2014, . + + [RFC8174] Leiba, B., "Ambiguity of Uppercase vs Lowercase in RFC + 2119 Key Words", BCP 14, RFC 8174, DOI 10.17487/RFC8174, + May 2017, . + + [RFC8615] Nottingham, M., "Well-Known Uniform Resource Identifiers + (URIs)", RFC 8615, DOI 10.17487/RFC8615, May 2019, + . + + [RFC9110] Fielding, R., Ed., Nottingham, M., Ed., and J. Reschke, + Ed., "HTTP Semantics", STD 97, RFC 9110, + DOI 10.17487/RFC9110, June 2022, + . + + [WebTransport] + Frindell, A., Kinnear, E., and V. Vasiliev, "WebTransport + over HTTP/3", Work in Progress, Internet-Draft, draft- + ietf-webtrans-http3-13, 7 July 2025, + . + +Informative References + + [I-D.draft-jennings-moq-quicr-proto] + Jennings, C. F., Nandakumar, S., and C. Huitema, "QuicR - + Media Delivery Protocol over QUIC", Work in Progress, + Internet-Draft, draft-jennings-moq-quicr-proto-01, 11 July + 2022, . + + + +Nandakumar, et al. Expires 6 March 2026 [Page 97] + +Internet-Draft moq-transport September 2025 + + + [I-D.draft-kpugin-rush] + Pugin, K., Garg, N., Frindell, A., Ferret, J. C., and J. + Weissman, "RUSH - Reliable (unreliable) streaming + protocol", Work in Progress, Internet-Draft, draft-kpugin- + rush-03, 21 April 2025, + . + + [I-D.draft-lcurley-warp] + Curley, L., Pugin, K., Nandakumar, S., and V. Vasiliev, + "Warp - Live Media Transport over QUIC", Work in Progress, + Internet-Draft, draft-lcurley-warp-04, 13 March 2023, + . + + [I-D.ietf-webtrans-overview] + Kinnear, E. and V. Vasiliev, "The WebTransport Protocol + Framework", Work in Progress, Internet-Draft, draft-ietf- + webtrans-overview-10, 7 July 2025, + . + + [RFC6582] Henderson, T., Floyd, S., Gurtov, A., and Y. Nishida, "The + NewReno Modification to TCP's Fast Recovery Algorithm", + RFC 6582, DOI 10.17487/RFC6582, April 2012, + . + + [RFC9000] Iyengar, J., Ed. and M. Thomson, Ed., "QUIC: A UDP-Based + Multiplexed and Secure Transport", RFC 9000, + DOI 10.17487/RFC9000, May 2021, + . + + [RFC9438] Xu, L., Ha, S., Rhee, I., Goel, V., and L. Eggert, Ed., + "CUBIC for Fast and Long-Distance Networks", RFC 9438, + DOI 10.17487/RFC9438, August 2023, + . + +Appendix A. Change Log + + RFC Editor's Note: Please remove this section prior to publication of + a final version of this document. + + Issue and pull request numbers are listed with a leading octothorp. + +A.1. Since draft-ietf-moq-transport-13 + + *Setup and Control Plane* + + + + +Nandakumar, et al. Expires 6 March 2026 [Page 98] + +Internet-Draft moq-transport September 2025 + + + * Add an AUTHORITY parameter (#1058) + + * Add a free-form SETUP parameter identifying the implementation + (#1114) + + * Add a Request ID to SUBSCRIBE_UDPATE (#1106) + + * Indicate which params can appear PUBLISH* messages (#1071) + + * Add TRACK_STATUS to the list of request types affected by GOAWAY + (#1105) + + *Data Plane Wire Format and Handling* + + * Delta encode Object IDs within Subgroups (#1042) + + * Use a bit in Datagram Type to convey Object ID = 0 (#1055) + + * Corrected missed code point updates to Object Datagram Status + (#1082) + + * Merge OBJECT_DATAGRAM and OBJECT_DATAGRAM_STATUS description + (#1179) + + * Objects are not schedulable if flow-control blocked (#1054) + + * Clarify DELIVERY_TIMEOUT reordering computation (#1120) + + * Receiving unrequested Objects (#1112) + + * Clarify End of Track (#1111) + + * Malformed tracks apply to FETCH (#1083) + + * Remove early FIN from the definition of malformed tracks (#1096) + + * Prior Object ID Gap Extension header (#939) + + * Add Extension containing immutable extensions (#1025) + + *Relay Handling* + + * Explain FETCH routing for relays (#1165) + + * MUST for multi-publisher relay handling (#1115) + + * Filters don't (usually) determine the end of subscription (#1113) + + + + +Nandakumar, et al. Expires 6 March 2026 [Page 99] + +Internet-Draft moq-transport September 2025 + + + * Allow self-subscriptions (#1110) + + * Explain Namespace Prefix Matching in more detail (#1116) + + *Explanatory* + + * Explain Modularity of MOQT (#1107) + + * Explain how to resume publishing after losing state (#1087) + + *Major Editorial Changes* + + * Rename ANNOUNCE to PUBLISH_NAMESPACE (#1104) + + * Rename SUBSCRIBE_DONE to PUBLISH_DONE (#1108) + + * Major FETCH Reorganization (#1173) + + * Reformat Error Codes (#1091) + +A.2. Since draft-ietf-moq-transport-12 + + * TRACK_STATUS_REQUEST and TRACK_STATUS have changed to directly + mirror SUBSCRIBE/OK/ERROR (#1015) + + * SUBSCRIBE_ANNOUNCES was renamed back to SUBSCRIBE_NAMESPACE + (#1049) + +A.3. Since draft-ietf-moq-transport-11 + + * Move Track Alias from SUBSCRIBE to SUBSCRIBE_OK (#977) + + * Expand cases FETCH_OK returns Invalid Range (#946) and clarify + fields (#936) + + * Add an error code to FETCH_ERROR when an Object status is unknown + (#825) + + * Rename Latest Object to Largest Object (#1024) and clarify what to + do when it's incomplete (#937) + + * Explain Malformed Tracks and what to do with them (#938) + + * Allow End of Group to be indicated in a normal Object (#1011) + + * Relays MUST have an upstream subscription to send SUBSCRIBE_OK + (#1017) + + + + +Nandakumar, et al. Expires 6 March 2026 [Page 100] + +Internet-Draft moq-transport September 2025 + + + * Allow AUTHORIZATION TOKEN in CLIENT_SETUP, SERVER_SETUP and other + fixes (#1013) + + * Add PUBLISH for publisher initiated subscriptions (#995) and fix + the PUBLISH codepoints (#1048, #1051) + +A.4. Since draft-ietf-moq-transport-10 + + * Added Common Structure definitions - Location, Key-Value-Pair and + Reason Phrase + + * Limit lengths of all variable length fields, including Track + Namespace and Name + + * Control Message length is now 16 bits instead of variable length + + * Subscribe ID became Request ID, and was added to most control + messages. Request ID is used to correlate OK/ERROR responses for + ANNOUNCE, SUBSCRIBE_NAMESPACE, and TRACK_STATUS. Like Subscribe + ID, Request IDs are flow controlled. + + * Explain rules for caching in more detail + + * Changed the SETUP parameter format for even number parameters to + match the Object Header Extension format + + * Rotated SETUP code points + + * Added Parameters to TRACK_STATUS and TRACK_STATUS_REQUEST + + * Clarified how subscribe filters work + + * Added Next Group Filter to SUBSCRIBE + + * Added Forward flag to SUBSCRIBE + + * Renamed FETCH_OK field to End and clarified how to set it + + * Added Absolute Joining Fetch + + * Clarified No Error vs Invalid Range FETCH_ERROR cases + + * Use bits in SUBGROUP_HEADER and DATAGRAM* types to compress + subgroup ID and extensions + + * Coalesced END_OF_GROUP and END_OF_TRACK_AND_GROUP status + + + + + +Nandakumar, et al. Expires 6 March 2026 [Page 101] + +Internet-Draft moq-transport September 2025 + + + * Objects that Do Not Exist cannot have extensions when sent on the + wire + + * Specified error codes for resetting data streams + + * Defined an Object Header Extension for communicating a known Group + ID gap + + * Replaced AUTHORIZATION_INFO with AUTHORIZATION_TOKEN, which has + more structure, compression, and additional Auth related error + codes (#760) + +Authors' Addresses + + Suhas Nandakumar + Cisco + Email: snandaku@cisco.com + + + Victor Vasiliev + Google + Email: vasilvv@google.com + + + Ian Swett (editor) + Google + Email: ianswett@google.com + + + Alan Frindell (editor) + Meta + Email: afrind@meta.com + + + + + + + + + + + + + + + + + + + +Nandakumar, et al. Expires 6 March 2026 [Page 102] \ No newline at end of file diff --git a/draft-16.txt b/draft-16.txt new file mode 100644 index 0000000..b4485cb --- /dev/null +++ b/draft-16.txt @@ -0,0 +1,6104 @@ + + + + +Media Over QUIC S. Nandakumar +Internet-Draft Cisco +Intended status: Standards Track V. Vasiliev +Expires: 17 July 2026 I. Swett, Ed. + Google + A. Frindell, Ed. + Meta + 13 January 2026 + + + Media over QUIC Transport + draft-ietf-moq-transport-16 + +Abstract + + This document defines the core behavior for Media over QUIC Transport + (MOQT), a media transport protocol designed to operate over QUIC and + WebTransport, which have similar functionality. MOQT allows a + producer of media to publish data and have it consumed via + subscription by a multiplicity of endpoints. It supports + intermediate content distribution networks and is designed for high + scale and low latency distribution. + +About This Document + + This note is to be removed before publishing as an RFC. + + The latest revision of this draft can be found at https://moq- + wg.github.io/moq-transport/draft-ietf-moq-transport.html. Status + information for this document may be found at + https://datatracker.ietf.org/doc/draft-ietf-moq-transport/. + + Discussion of this document takes place on the Media Over QUIC + Working Group mailing list (mailto:moq@ietf.org), which is archived + at https://mailarchive.ietf.org/arch/browse/moq/. Subscribe at + https://www.ietf.org/mailman/listinfo/moq/. + + Source for this draft and an issue tracker can be found at + https://github.com/moq-wg/moq-transport. + +Status of This Memo + + This Internet-Draft is submitted in full conformance with the + provisions of BCP 78 and BCP 79. + + + + + + + +Nandakumar, et al. Expires 17 July 2026 [Page 1] + +Internet-Draft moq-transport January 2026 + + + Internet-Drafts are working documents of the Internet Engineering + Task Force (IETF). Note that other groups may also distribute + working documents as Internet-Drafts. The list of current Internet- + Drafts is at https://datatracker.ietf.org/drafts/current/. + + Internet-Drafts are draft documents valid for a maximum of six months + and may be updated, replaced, or obsoleted by other documents at any + time. It is inappropriate to use Internet-Drafts as reference + material or to cite them other than as "work in progress." + + This Internet-Draft will expire on 17 July 2026. + +Copyright Notice + + Copyright (c) 2026 IETF Trust and the persons identified as the + document authors. All rights reserved. + + This document is subject to BCP 78 and the IETF Trust's Legal + Provisions Relating to IETF Documents (https://trustee.ietf.org/ + license-info) in effect on the date of publication of this document. + Please review these documents carefully, as they describe your rights + and restrictions with respect to this document. Code Components + extracted from this document must include Revised BSD License text as + described in Section 4.e of the Trust Legal Provisions and are + provided without warranty as described in the Revised BSD License. + +Table of Contents + + 1. Introduction . . . . . . . . . . . . . . . . . . . . . . . . 5 + 1.1. Motivation . . . . . . . . . . . . . . . . . . . . . . . 6 + 1.1.1. Latency . . . . . . . . . . . . . . . . . . . . . . . 6 + 1.1.2. Leveraging QUIC . . . . . . . . . . . . . . . . . . . 6 + 1.1.3. Convergence . . . . . . . . . . . . . . . . . . . . . 6 + 1.1.4. Relays . . . . . . . . . . . . . . . . . . . . . . . 7 + 1.2. Terms and Definitions . . . . . . . . . . . . . . . . . . 7 + 1.3. Stream Management Terms . . . . . . . . . . . . . . . . . 8 + 1.4. Notational Conventions . . . . . . . . . . . . . . . . . 8 + 1.4.1. Location Structure . . . . . . . . . . . . . . . . . 8 + 1.4.2. Key-Value-Pair Structure . . . . . . . . . . . . . . 9 + 1.4.3. Reason Phrase Structure . . . . . . . . . . . . . . . 10 + 1.5. Representing Namespace and Track Names . . . . . . . . . 10 + 2. Object Data Model . . . . . . . . . . . . . . . . . . . . . . 11 + 2.1. Objects . . . . . . . . . . . . . . . . . . . . . . . . . 11 + 2.2. Subgroups . . . . . . . . . . . . . . . . . . . . . . . . 12 + 2.3. Groups . . . . . . . . . . . . . . . . . . . . . . . . . 13 + 2.3.1. Group IDs . . . . . . . . . . . . . . . . . . . . . . 13 + 2.4. Track . . . . . . . . . . . . . . . . . . . . . . . . . . 14 + 2.4.1. Track Naming . . . . . . . . . . . . . . . . . . . . 14 + + + +Nandakumar, et al. Expires 17 July 2026 [Page 2] + +Internet-Draft moq-transport January 2026 + + + 2.4.2. Malformed Tracks . . . . . . . . . . . . . . . . . . 15 + 2.4.3. Scope . . . . . . . . . . . . . . . . . . . . . . . . 16 + 2.5. Extension Headers . . . . . . . . . . . . . . . . . . . . 17 + 3. Sessions . . . . . . . . . . . . . . . . . . . . . . . . . . 18 + 3.1. Session establishment . . . . . . . . . . . . . . . . . . 18 + 3.1.1. WebTransport . . . . . . . . . . . . . . . . . . . . 18 + 3.1.2. QUIC . . . . . . . . . . . . . . . . . . . . . . . . 19 + 3.1.3. Connection URL . . . . . . . . . . . . . . . . . . . 19 + 3.2. Extension Negotiation . . . . . . . . . . . . . . . . . . 19 + 3.3. Session initialization . . . . . . . . . . . . . . . . . 20 + 3.4. Termination . . . . . . . . . . . . . . . . . . . . . . . 20 + 3.5. Migration . . . . . . . . . . . . . . . . . . . . . . . . 22 + 3.6. Congestion Control . . . . . . . . . . . . . . . . . . . 23 + 3.6.1. Bufferbloat . . . . . . . . . . . . . . . . . . . . . 23 + 3.6.2. Application-Limited . . . . . . . . . . . . . . . . . 23 + 3.6.3. Consistent Throughput . . . . . . . . . . . . . . . . 23 + 4. Modularity . . . . . . . . . . . . . . . . . . . . . . . . . 23 + 5. Publishing and Retrieving Tracks . . . . . . . . . . . . . . 24 + 5.1. Subscriptions . . . . . . . . . . . . . . . . . . . . . . 24 + 5.1.1. Subscription State Management . . . . . . . . . . . . 26 + 5.1.2. Subscription Filters . . . . . . . . . . . . . . . . 27 + 5.1.3. Joining an Ongoing Track . . . . . . . . . . . . . . 28 + 5.2. Fetch State Management . . . . . . . . . . . . . . . . . 29 + 6. Namespace Discovery . . . . . . . . . . . . . . . . . . . . . 29 + 6.1. Subscribing to Namespaces . . . . . . . . . . . . . . . . 30 + 6.2. Publishing Namespaces . . . . . . . . . . . . . . . . . . 30 + 7. Priorities . . . . . . . . . . . . . . . . . . . . . . . . . 31 + 7.1. Definitions . . . . . . . . . . . . . . . . . . . . . . . 31 + 7.2. Scheduling Algorithm . . . . . . . . . . . . . . . . . . 32 + 7.3. Considerations for Setting Priorities . . . . . . . . . . 33 + 8. Relays . . . . . . . . . . . . . . . . . . . . . . . . . . . 34 + 8.1. Caching Relays . . . . . . . . . . . . . . . . . . . . . 34 + 8.2. Forward Handling . . . . . . . . . . . . . . . . . . . . 35 + 8.3. Multiple Publishers . . . . . . . . . . . . . . . . . . . 35 + 8.4. Subscriber Interactions . . . . . . . . . . . . . . . . . 36 + 8.4.1. Graceful Subscriber Relay Switchover . . . . . . . . 37 + 8.5. Publisher Interactions . . . . . . . . . . . . . . . . . 37 + 8.5.1. Graceful Publisher Relay Switchover . . . . . . . . . 39 + 8.6. Relay Track Handling . . . . . . . . . . . . . . . . . . 39 + 8.7. Relay Object Handling . . . . . . . . . . . . . . . . . . 39 + 9. Control Messages . . . . . . . . . . . . . . . . . . . . . . 39 + 9.1. Request ID . . . . . . . . . . . . . . . . . . . . . . . 41 + 9.2. Parameters . . . . . . . . . . . . . . . . . . . . . . . 42 + 9.2.1. Parameter Scope . . . . . . . . . . . . . . . . . . . 42 + 9.2.2. Message Parameters . . . . . . . . . . . . . . . . . 43 + 9.3. CLIENT_SETUP and SERVER_SETUP . . . . . . . . . . . . . . 49 + 9.3.1. Setup Parameters . . . . . . . . . . . . . . . . . . 50 + 9.4. GOAWAY . . . . . . . . . . . . . . . . . . . . . . . . . 52 + + + +Nandakumar, et al. Expires 17 July 2026 [Page 3] + +Internet-Draft moq-transport January 2026 + + + 9.5. MAX_REQUEST_ID . . . . . . . . . . . . . . . . . . . . . 53 + 9.6. REQUESTS_BLOCKED . . . . . . . . . . . . . . . . . . . . 53 + 9.7. REQUEST_OK . . . . . . . . . . . . . . . . . . . . . . . 54 + 9.8. REQUEST_ERROR . . . . . . . . . . . . . . . . . . . . . . 54 + 9.9. SUBSCRIBE . . . . . . . . . . . . . . . . . . . . . . . . 56 + 9.10. SUBSCRIBE_OK . . . . . . . . . . . . . . . . . . . . . . 57 + 9.11. REQUEST_UPDATE . . . . . . . . . . . . . . . . . . . . . 58 + 9.11.1. Updating Subscriptions . . . . . . . . . . . . . . . 59 + 9.12. UNSUBSCRIBE . . . . . . . . . . . . . . . . . . . . . . . 60 + 9.13. PUBLISH . . . . . . . . . . . . . . . . . . . . . . . . . 60 + 9.14. PUBLISH_OK . . . . . . . . . . . . . . . . . . . . . . . 61 + 9.15. PUBLISH_DONE . . . . . . . . . . . . . . . . . . . . . . 62 + 9.16. FETCH . . . . . . . . . . . . . . . . . . . . . . . . . . 64 + 9.16.1. Standalone Fetch . . . . . . . . . . . . . . . . . . 64 + 9.16.2. Joining Fetches . . . . . . . . . . . . . . . . . . 65 + 9.16.3. Fetch Handling . . . . . . . . . . . . . . . . . . . 66 + 9.17. FETCH_OK . . . . . . . . . . . . . . . . . . . . . . . . 68 + 9.18. FETCH_CANCEL . . . . . . . . . . . . . . . . . . . . . . 70 + 9.19. TRACK_STATUS . . . . . . . . . . . . . . . . . . . . . . 70 + 9.20. PUBLISH_NAMESPACE . . . . . . . . . . . . . . . . . . . . 71 + 9.21. NAMESPACE . . . . . . . . . . . . . . . . . . . . . . . . 71 + 9.22. PUBLISH_NAMESPACE_DONE . . . . . . . . . . . . . . . . . 72 + 9.23. NAMESPACE_DONE . . . . . . . . . . . . . . . . . . . . . 72 + 9.24. PUBLISH_NAMESPACE_CANCEL . . . . . . . . . . . . . . . . 72 + 9.25. SUBSCRIBE_NAMESPACE . . . . . . . . . . . . . . . . . . . 73 + 10. Data Streams and Datagrams . . . . . . . . . . . . . . . . . 75 + 10.1. Track Alias . . . . . . . . . . . . . . . . . . . . . . 75 + 10.2. Objects . . . . . . . . . . . . . . . . . . . . . . . . 76 + 10.2.1. Canonical Object Properties . . . . . . . . . . . . 76 + 10.3. Datagrams . . . . . . . . . . . . . . . . . . . . . . . 78 + 10.3.1. Object Datagram . . . . . . . . . . . . . . . . . . 78 + 10.4. Streams . . . . . . . . . . . . . . . . . . . . . . . . 80 + 10.4.1. Stream Cancellation . . . . . . . . . . . . . . . . 80 + 10.4.2. Subgroup Header . . . . . . . . . . . . . . . . . . 80 + 10.4.3. Closing Subgroup Streams . . . . . . . . . . . . . . 82 + 10.4.4. Fetch Header . . . . . . . . . . . . . . . . . . . . 85 + 10.5. Examples . . . . . . . . . . . . . . . . . . . . . . . . 88 + 11. Extension Headers . . . . . . . . . . . . . . . . . . . . . . 89 + 11.1. DELIVERY TIMEOUT . . . . . . . . . . . . . . . . . . . . 89 + 11.1.1. MAX CACHE DURATION . . . . . . . . . . . . . . . . . 90 + 11.2. Immutable Extensions . . . . . . . . . . . . . . . . . . 91 + 11.3. Prior Group ID Gap . . . . . . . . . . . . . . . . . . . 92 + 11.4. Prior Object ID Gap . . . . . . . . . . . . . . . . . . 93 + 12. Security Considerations . . . . . . . . . . . . . . . . . . . 94 + 12.1. Resource Exhaustion . . . . . . . . . . . . . . . . . . 94 + 12.2. Timeouts . . . . . . . . . . . . . . . . . . . . . . . . 94 + 12.3. Relay security considerations . . . . . . . . . . . . . 94 + 12.3.1. State maintenance . . . . . . . . . . . . . . . . . 94 + + + +Nandakumar, et al. Expires 17 July 2026 [Page 4] + +Internet-Draft moq-transport January 2026 + + + 12.3.2. SUBSCRIBE_NAMESPACE with short prefixes . . . . . . 95 + 13. IANA Considerations . . . . . . . . . . . . . . . . . . . . . 95 + 13.1. Authorization Token Alias Type . . . . . . . . . . . . . 95 + 13.2. Message Parameters . . . . . . . . . . . . . . . . . . . 96 + 13.3. Extension Headers . . . . . . . . . . . . . . . . . . . 96 + 13.4. Error Codes . . . . . . . . . . . . . . . . . . . . . . 97 + 13.4.1. Session Termination Error Codes . . . . . . . . . . 97 + 13.4.2. REQUEST_ERROR Codes . . . . . . . . . . . . . . . . 98 + 13.4.3. PUBLISH_DONE Codes . . . . . . . . . . . . . . . . . 99 + 13.4.4. Data Stream Reset Error Codes . . . . . . . . . . . 99 + Contributors . . . . . . . . . . . . . . . . . . . . . . . . . . 100 + References . . . . . . . . . . . . . . . . . . . . . . . . . . . 100 + Normative References . . . . . . . . . . . . . . . . . . . . . 100 + Informative References . . . . . . . . . . . . . . . . . . . . 102 + Appendix A. Change Log . . . . . . . . . . . . . . . . . . . . . 103 + A.1. Since draft-ietf-moq-transport-15 . . . . . . . . . . . . 103 + A.2. Since draft-ietf-moq-transport-14 . . . . . . . . . . . . 104 + A.3. Since draft-ietf-moq-transport-13 . . . . . . . . . . . . 106 + A.4. Since draft-ietf-moq-transport-12 . . . . . . . . . . . . 107 + A.5. Since draft-ietf-moq-transport-11 . . . . . . . . . . . . 107 + A.6. Since draft-ietf-moq-transport-10 . . . . . . . . . . . . 108 + Authors' Addresses . . . . . . . . . . . . . . . . . . . . . . . 109 + +1. Introduction + + Media Over QUIC Transport (MOQT) is a protocol that is optimized for + the QUIC protocol [QUIC], either directly or via WebTransport + [WebTransport], for the dissemination of media. MOQT utilizes a + publish/subscribe workflow in which producers of media publish data + in response to subscription requests from a multiplicity of + endpoints. MOQT supports wide range of use-cases with different + resiliency and latency (live, interactive) needs without compromising + the scalability and cost effectiveness associated with content + delivery networks. + + MOQT is a generic protocol designed to work in concert with multiple + MoQ Streaming Formats. These MoQ Streaming Formats define how + content is encoded, packaged, and mapped to MOQT objects, along with + policies for discovery and subscription. + + * Section 2 describes the data model employed by MOQT. + + * Section 3 covers aspects of setting up an MOQT session. + + * Section 7 covers mechanisms for prioritizing subscriptions. + + * Section 8 covers behavior at the relay entities. + + + + +Nandakumar, et al. Expires 17 July 2026 [Page 5] + +Internet-Draft moq-transport January 2026 + + + * Section 9 covers how control messages are encoded on the wire. + + * Section 10 covers how data messages are encoded on the wire. + +1.1. Motivation + + The development of MOQT is driven by goals in a number of areas - + specifically latency, the robust feature set of QUIC and relay + support. + +1.1.1. Latency + + Latency is necessary to correct for variable network throughput. + Ideally live content is consumed at the same bitrate it is produced. + End-to-end latency would be fixed and only subject to encoding and + transmission delays. Unfortunately, networks have variable + throughput, primarily due to congestion. Attempting to deliver + content encoded at a higher bitrate than the network can cause + queuing along the path from producer to consumer. The speed at which + a protocol can detect and respond to congestion determines the + overall latency. TCP-based protocols are simple but are slow to + detect congestion and suffer from head-of-line blocking. Protocols + utilizing UDP directly can avoid queuing, but the application is then + responsible for the complexity of fragmentation, congestion control, + retransmissions, receiver feedback, reassembly, and more. One goal + of MOQT is to achieve the best of both these worlds: leverage the + features of QUIC to create a simple yet flexible low latency protocol + that can rapidly detect and respond to congestion. + +1.1.2. Leveraging QUIC + + The parallel nature of QUIC streams can provide improvements in the + face of loss. A goal of MOQT is to design a streaming protocol to + leverage the transmission benefits afforded by parallel QUIC streams + as well exercising options for flexible loss recovery. + +1.1.3. Convergence + + Some live media architectures today have separate protocols for + ingest and distribution, for example RTMP and HTTP based HLS or DASH. + Switching protocols necessitates intermediary origins which re- + package the media content. While specialization can have its + benefits, there are efficiency gains to be had in not having to re- + package content. A goal of MOQT is to develop a single protocol + which can be used for transmission from contribution to distribution. + A related goal is the ability to support existing encoding and + packaging schemas, both for backwards compatibility and for + interoperability with the established content preparation ecosystem. + + + +Nandakumar, et al. Expires 17 July 2026 [Page 6] + +Internet-Draft moq-transport January 2026 + + +1.1.4. Relays + + An integral feature of a protocol being successful is its ability to + deliver media at scale. Greatest scale is achieved when third-party + networks, independent of both the publisher and subscriber, can be + leveraged to relay the content. These relays must cache content for + distribution efficiency while simultaneously routing content and + deterministically responding to congestion in a multi-tenant network. + A goal of MOQT is to treat relays as first-class citizens of the + protocol and ensure that objects are structured such that information + necessary for distribution is available to relays while the media + content itself remains opaque and private. + +1.2. Terms and Definitions + + The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", + "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and + "OPTIONAL" in this document are to be interpreted as described in + BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all + capitals, as shown here. + + The following terms are used with the first letter capitalized. + + Application: The entity using MOQT to transmit and receive data. + + Client: The party initiating a Transport Session. + + Server: The party accepting an incoming Transport Session. + + Endpoint: A Client or Server. + + Peer: The other endpoint than the one being described. + + Publisher: An endpoint that handles subscriptions by sending + requested Objects from the requested track. + + Subscriber: An endpoint that subscribes to and receives tracks. + + Original Publisher: The initial publisher of a given track. + + End Subscriber: A subscriber that initiates a subscription and does + not send the data on to other subscribers. + + Relay: An entity that is both a Publisher and a Subscriber, is not + the Original Publisher or End Subscriber, and conforms to all + requirements in Section 8. + + Upstream: In the direction of the Original Publisher. + + + +Nandakumar, et al. Expires 17 July 2026 [Page 7] + +Internet-Draft moq-transport January 2026 + + + Downstream: In the direction of the End Subscriber(s). + + Transport Session: A raw QUIC connection or a WebTransport session. + + Stream: A bidirectional or unidirectional bytestream provided by the + QUIC transport or WebTransport. + + Congestion: Packet loss and queuing caused by degraded or overloaded + networks. + + Group: A temporal sequence of objects. A group represents a join + point in a track. See (Section 2.3). + + Object: An object is an addressable unit whose payload is a sequence + of bytes. Objects form the base element in the MOQT data model. + See (Section 2.1). + + Track: A track is a collection of groups. See (Section 2.4). + +1.3. Stream Management Terms + + This document uses stream management terms described in [RFC9000], + Section 1.3 including STOP_SENDING, RESET_STREAM and FIN. + +1.4. Notational Conventions + + This document uses the conventions detailed in ([RFC9000], + Section 1.3) when describing the binary encoding. + + To reduce unnecessary use of bandwidth, variable length integers + SHOULD be encoded using the least number of bytes possible to + represent the required value. + +1.4.1. Location Structure + + Location identifies a particular Object in a Group within a Track. + + Location { + Group (i), + Object (i) + } + + Figure 1: Location structure + + In this document, a Location can be expressed in the form of + {GroupID, ObjectID}, where GroupID and ObjectID indicate the Group ID + and Object ID of the Location, respectively. The constituent parts + of any Location A can be referred to using A.Group or A.Object. + + + +Nandakumar, et al. Expires 17 July 2026 [Page 8] + +Internet-Draft moq-transport January 2026 + + + Location A < Location B if: + + A.Group < B.Group || (A.Group == B.Group && A.Object < B.Object) + +1.4.2. Key-Value-Pair Structure + + Key-Value-Pair is a flexible structure designed to carry key/value + pairs in which the key is a variable length integer and the value is + either a variable length integer or a byte field of arbitrary length. + + Key-Value-Pairs encode a Type value as a delta from the previous Type + value, or from 0 if there is no previous Type value. This is + efficient on the wire and makes it easy to ensure there is only one + instance of a type when needed. The previous Type value plus the + Delta Type MUST NOT be greater than 2^64 - 1. If a Delta Type is + received that would be too large, the Session MUST be closed with a + PROTOCOL_VIOLATION. + + Key-Value-Pair is used in both the data plane and control plane, but + is optimized for use in the data plane. + + Key-Value-Pair { + Delta Type (i), + [Length (i),] + Value (..) + } + + Figure 2: MOQT Key-Value-Pair + + * Delta Type: an unsigned integer, encoded as a varint, identifying + the Type as a delta encoded value from the previous Type, if any. + The Type identifies the type of value and also the subsequent + serialization. + + * Length: Only present when Type is odd. Specifies the length of + the Value field in bytes. The maximum length of a value is 2^16-1 + bytes. If an endpoint receives a length larger than the maximum, + it MUST close the session with a PROTOCOL_VIOLATION. + + * Value: A single varint encoded value when Type is even, otherwise + a sequence of Length bytes. + + If a receiver understands a Type, and the following Value or Length/ + Value does not match the serialization defined by that Type, the + receiver MUST close the session with error code + KEY_VALUE_FORMATTING_ERROR. + + + + + +Nandakumar, et al. Expires 17 July 2026 [Page 9] + +Internet-Draft moq-transport January 2026 + + +1.4.3. Reason Phrase Structure + + Reason Phrase provides a way for the sender to encode additional + diagnostic information about the error condition, where appropriate. + + Reason Phrase { + Reason Phrase Length (i), + Reason Phrase Value (..) + } + + * Reason Phrase Length: A variable-length integer specifying the + length of the reason phrase in bytes. The reason phrase length + has a maximum value of 1024 bytes. If an endpoint receives a + length exceeding the maximum, it MUST close the session with a + PROTOCOL_VIOLATION + + * Reason Phrase Value: Additional diagnostic information about the + error condition. The reason phrase value is encoded as UTF-8 + string and does not carry information, such as language tags, that + would aid comprehension by any entity other than the one that + created the text. + +1.5. Representing Namespace and Track Names + + There is often a need to render namespace tuples and track names for + purposes such as logging, representing track filenames, or use in + certain authorization verification schemes. The namespace and track + name are binary, so they need to be converted to a safe form. + + The following format is RECOMMENDED: + + * Each of the namespace tuples are rendered in order with a hyphen + (-) between them followed by the track name with a double hyphen + (--) between the last namespace and track name. + + * Bytes in the range a-z, A-Z, 0-9 as well as _ (0x5f) are output as + is, while all other bytes are encoded as a period (.) symbol + followed by exactly two lower case hex digits. + + The goal of this format is to have a format that is both filename and + URL safe. It allows many common names to be rendered in an easily + human readable form while still supporting binary values. + + Example: + + example.2enet-team2-project_x--report + Namespace tuples: (example.net, team2, project_x) + Track name: report + + + +Nandakumar, et al. Expires 17 July 2026 [Page 10] + +Internet-Draft moq-transport January 2026 + + +2. Object Data Model + + MOQT has a hierarchical data model, comprised of tracks which contain + groups, and groups that contain objects. Inside of a group, the + objects can be organized into subgroups. + + To give an example of how an application might use this data model, + consider an application sending high and low resolution video using a + codec with temporal scalability. Each resolution is sent as a + separate track to allow the subscriber to pick the appropriate + resolution given the display environment and available bandwidth. + Each independently coded sequence of pictures in a resolution is sent + as a group as the first picture in the sequence can be used as a + random access point. This allows the client to join at the logical + points where decoding of the media can start without needing + information before the join points. The temporal layers are sent as + separate subgroups to allow the priority mechanism to favor lower + temporal layers when there is not enough bandwidth to send all + temporal layers. Each frame of video is sent as a single object. + +2.1. Objects + + The basic data element of MOQT is an object. An object is an + addressable unit whose payload is a sequence of bytes. All objects + belong to a group, indicating ordering and potential dependencies + (see Section 2.3). An object is uniquely identified by its track + namespace, track name, group ID, and object ID, and must be an + identical sequence of bytes regardless of how or where it is + retrieved. An Object can become unavailable, but its contents MUST + NOT change over time. + + Objects are comprised of two parts: metadata and a payload. The + metadata is never encrypted and is always visible to relays (see + Section 8). The payload portion may be encrypted, in which case it + is only visible to the Original Publisher and End Subscribers. The + Original Publisher is solely responsible for the content of the + object payload. This includes the underlying encoding, compression, + any end-to-end encryption, or authentication. A relay MUST NOT + combine, split, or otherwise modify object payloads. + + Objects within a Group are in ascending order by Object ID. + + From the perspective of a subscriber or a cache, an Object can be in + three possible states: + + + + + + + +Nandakumar, et al. Expires 17 July 2026 [Page 11] + +Internet-Draft moq-transport January 2026 + + + 1. The Object is known to not exist. This state is permanent. MOQT + has multiple ways to communicate that a certain range of objects + does not exist, including the Object Status field, and the use of + gaps in FETCH responses. + + 2. The Object is known to exist. From this state, it can transition + to not existing, but not vice versa. + + 3. The state of the Object is unknown, either because it has not + been yet received, or it has not been produced yet. + + Whenever the publisher communicates that certain objects do not + exist, this fact is expressed as a contiguous range of non-existent + objects and by include extension headers indicating the group/object + gaps; MOQT implementers should take that into account when selecting + appropriate data structures. + +2.2. Subgroups + + A subgroup is a sequence of one or more objects from the same group + (Section 2.3) in ascending order by Object ID. Objects in a subgroup + have a dependency and priority relationship consistent with sharing a + stream and are sent on a single stream whenever possible. A Group is + delivered using at least as many streams as there are Subgroups, + typically with a one-to-one mapping between Subgroups and streams. + + When an Object's forwarding preference (see Section 10.2.1) is + "Datagram", it is not sent in Subgroups, does not belong to a + Subgroup in any way, and the description in the remainder of this + section does not apply. + + Streams offer in-order reliable delivery and the ability to cancel + sending and retransmission of data. Furthermore, many QUIC and + WebTransport implementations offer the ability to control the + relative scheduling priority of pending stream data. + + Every Object within a Group belongs to exactly one Subgroup or + Datagram. + + When Objects are sent in a subscription (see Section 5.1), Objects + from two subgroups MUST NOT be sent on the same stream, and Objects + from the same Subgroup MUST NOT be sent on different streams, unless + one of the streams was reset prematurely, or upstream conditions have + forced objects from a Subgroup to be sent out of Object ID order. + + Original publishers assign each Subgroup a Subgroup ID, and do so as + they see fit. The scope of a Subgroup ID is a Group, so Subgroups + from different Groups MAY share a Subgroup ID without implying any + + + +Nandakumar, et al. Expires 17 July 2026 [Page 12] + +Internet-Draft moq-transport January 2026 + + + relationship between them. In general, publishers assign objects to + subgroups in order to leverage the features of streams as described + above. + + In general, if Object B is dependent on Object A, then delivery of B + can follow A, i.e. A and B can be usefully delivered over a single + stream. If an Object is dependent on all previous Objects in a + Subgroup, it likely fits best in that Subgroup. If an Object is not + dependent on any of the Objects in a Subgroup, it likely belongs in a + different Subgroup. + + When assigning Objects to different Subgroups, the Original Publisher + makes a reasonable tradeoff between having an optimal mapping of + Object relationships in a Group and minimizing the number of streams + used. + +2.3. Groups + + A group is a collection of Objects and is a sub-unit of a Track + (Section 2.4). Groups SHOULD be independently useful, so Objects + within a Group SHOULD NOT depend on Objects in other Groups. A Group + provides a join point for subscriptions, so a subscriber that does + not want to receive the entire Track can opt to receive only Groups + starting from a given Group ID. Groups can contain any number of + Objects. + +2.3.1. Group IDs + + Within a track, the original publisher SHOULD publish Group IDs which + increase with time (where "time" is defined according to the internal + clock of the media being sent). In some cases, Groups will be + produced in increasing order, but sent to subscribers in a different + order, for example when the subscription's Group Order is Descending. + Due to network reordering and the partial reliability features of + MOQT, Groups can always be received out of order. + + As a result, subscribers cannot infer the existence of a Group until + an object in the Group is received. This can create gaps in a cache + that can be filled by doing a Fetch upstream, if necessary. + + Applications that cannot produce Group IDs that increase with time + are limited to the subset of MOQT that does not compare group IDs. + Subscribers to these Tracks SHOULD NOT use range filters which span + multiple Groups in FETCH or SUBSCRIBE. SUBSCRIBE and FETCH delivery + use Group Order, so they could have an unexpected delivery order if + Group IDs do not increase with time. + + + + + +Nandakumar, et al. Expires 17 July 2026 [Page 13] + +Internet-Draft moq-transport January 2026 + + + The amount of time elapsed between publishing an Object in Group ID N + and in a Group ID > N, or even which will be published first, is not + defined by this specification and is defined by the applications + using MOQT. + +2.4. Track + + A track is a sequence of groups (Section 2.3). It is the entity + against which a subscriber issues a subscription request. A + subscriber can request to receive individual tracks starting at a + group boundary, including any new objects pushed by the publisher + while the track is active. + +2.4.1. Track Naming + + In MOQT, every track is identified by a Full Track Name, consisting + of a Track Namespace and a Track Name. + + Track Namespace is an ordered set of between 1 and 32 Track Namespace + Fields, encoded as follows: + + Track Namespace { + Number of Track Namespace Fields (i), + Track Namespace Field (..) ... + } + + * Number of Track Namespace Fields: A variable-length integer + specifying the number of Track Namespace Fields in the Track + Namespace. + + Each Track Namespace Field is encoded as follows: + + Track Namespace Field { + Track Namespace Field Length (i), + Track Namespace Field Value (..) + } + + * Track Namespace Field Length: A variable-length integer specifying + the length of the Track Namespace Field in bytes. + + * Track Namespace Field Value: A sequence of bytes that forms a + Track Namespace Field. + + Each Track Namespace Field Value MUST contain at least one byte. If + an endpoint receives a Track Namespace Field with a Track Namespace + Field Length of 0, it MUST close the session with a + PROTOCOL_VIOLATION. + + + + +Nandakumar, et al. Expires 17 July 2026 [Page 14] + +Internet-Draft moq-transport January 2026 + + + The structured nature of Track Namespace allows relays and + applications to manipulate prefixes of a namespace. If an endpoint + receives a Track Namespace consisting of 0 or greater than 32 Track + Namespace Fields, it MUST close the session with a + PROTOCOL_VIOLATION. + + Track Name is a sequence of bytes, possibly empty, that identifies an + individual track within the namespace. + + The maximum total length of a Full Track Name is 4,096 bytes. The + length of a Full Track Name is computed as the sum of the Track + Namespace Field Length fields and the Track Name Length field. The + length of a Track Namespace is the sum of the Track Namespace Field + Length fields. If an endpoint receives a Track Namespace or a Full + Track Name exceeding 4,096 bytes, it MUST close the session with a + PROTOCOL_VIOLATION. + + In this specification, both the Track Namespace Fields and the Track + Name are not constrained to a specific encoding. They carry a + sequence of bytes and comparison between two Track Namespace Fields + or Track Names is done by exact comparison of the bytes. + Specifications that use MOQT may constrain the information in these + fields, for example by restricting them to UTF-8. Any such + specification needs to specify the canonicalization into the bytes in + the Track Namespace Fields or Track Name such that exact comparison + works. + +2.4.2. Malformed Tracks + + There are multiple ways a publisher can transmit a Track that does + not conform to MOQT constraints. Such a Track is considered + malformed. Some example conditions that constitute a malformed track + when detected by a receiver include: + + 1. An Object is received in a FETCH response with the same Group ID + as the previous Object, but whose Object ID is not strictly + larger than the previous object. + + 2. In a FETCH response, an Object with a particular Subgroup ID is + received, but its Publisher Priority is different from that of + the previous Object with the same Subgroup ID. + + 3. An Object is received in an Ascending FETCH response whose Group + ID is smaller than the previous Object in the response. + + 4. An Object is received in a Descending FETCH response whose Group + ID is larger than the previous Object in the resopnse. + + + + +Nandakumar, et al. Expires 17 July 2026 [Page 15] + +Internet-Draft moq-transport January 2026 + + + 5. An Object is received whose Object ID is larger than the final + Object in the Subgroup. The final Object in a Subgroup is the + last Object received on a Subgroup stream before a FIN. + + 6. A Subgroup is received over multiple transport streams + terminated by FIN with different final Objects. + + 7. An Object is received in a Group whose Object ID is larger than + the final Object in the Group. The final Object in a Group is + the Object with Status END_OF_GROUP or the last Object sent in a + FETCH that requested the entire Group. + + 8. An Object is received on a Track whose Group and Object ID are + larger than the final Object in the Track. The final Object in + a Track is the Object with Status END_OF_TRACK or the last + Object sent in a FETCH whose response indicated End of Track. + + 9. The same Object is received more than once with different + Payload or other immutable properties. + + 10. An Object is received with a different Forwarding Preference + than previously observed. + + The above list of conditions is not considered exhaustive. + + When a subscriber detects a Malformed Track, it MUST UNSUBSCRIBE any + subscription and FETCH_CANCEL any fetch for that Track from that + publisher, and SHOULD deliver an error to the application. If a + relay detects a Malformed Track, it MUST immediately terminate + downstream subscriptions with PUBLISH_DONE and reset any fetch + streams with Status Code MALFORMED_TRACK. Object(s) triggering + Malformed Track status MUST NOT be cached. + +2.4.3. Scope + + An MOQT scope is a set of servers (as identified by their connection + URIs) for which a Full Track Name is guaranteed to be unique and + identify a specific track. It is up to the application using MOQT to + define how broad or narrow the scope is. An application that deals + with connections between devices on a local network may limit the + scope to a single connection; by contrast, an application that uses + multiple CDNs to serve media may require the scope to include all of + those CDNs. + + Because each Full Track Name is unique within an MOQT scope, they can + be used as a cache key for the track. If, at a given moment in time, + two tracks within the same scope contain different data, they MUST + have different names and/or namespaces. MOQT provides subscribers + + + +Nandakumar, et al. Expires 17 July 2026 [Page 16] + +Internet-Draft moq-transport January 2026 + + + with the ability to alter the specific manner in which tracks are + delivered via Parameters, but the actual content of the tracks does + not depend on those parameters; this is in contrast to protocols like + HTTP, where request headers can alter the server response. + + A publisher that loses state (e.g. crashes) and intends to resume + publishing on the same Track risks colliding with previously + published Objects and violating the above requirements. A publisher + can handle this in application specific ways, for example: + + 1. Select a unique Track Name or Track Namespace whenever it resumes + publishing. For example, it can base one of the Namespace Fields + on the current time, or select a sufficiently large random value. + + 2. Resume publishing under a previous Track Name and Namespace and + set the initial Group ID to a unique value guaranteed to be + larger than all previously used groups. This can be done by + choosing a Group ID based on the current time. + + 3. Use TRACK_STATUS or similar mechanism to query the previous state + to determine the largest published Group ID. + +2.5. Extension Headers + + Tracks and Objects can have additional relay-visible fields, known as + Extension Headers, which do not require negotiation, and can be used + to alter MoQT Object distribution. + + Extension Headers are defined in Section 11 as well as external + specifications and are registered in an IANA table Section 13. These + specifications define the type and value of the header, along with + any rules concerning processing, modification, caching and + forwarding. + + If unsupported by the relay, Extension Headers MUST NOT be modified, + MUST be cached as part of the Track or Object and MUST be forwarded + by relays. If a Track or Object arrives with a different set of + unknown extensions than previously cached, the most recent set SHOULD + replace any cached values, removing any unknown values not present in + the new set. Relays MUST NOT attempt to merge sets of unknown + extensions received in different messages. + + If supported by the relay and subject to the processing rules + specified in the definition of the extension, Extension Headers MAY + be modified, added, removed, and/or cached by relays. + + Extension Headers are serialized as Key-Value-Pairs (see Figure 2). + + + + +Nandakumar, et al. Expires 17 July 2026 [Page 17] + +Internet-Draft moq-transport January 2026 + + + Header types are registered in the IANA table 'MOQ Extension + Headers'. See Section 13. + +3. Sessions + +3.1. Session establishment + + This document defines a protocol that can be used interchangeably + both over a QUIC connection directly [QUIC], and over WebTransport + [WebTransport]. Both provide streams and datagrams with similar + semantics (see [I-D.ietf-webtrans-overview], Section 4); thus, the + main difference lies in how the servers are identified and how the + connection is established. The QUIC DATAGRAM extension ([RFC9221]) + MUST be supported and negotiated in the QUIC connection used for + MOQT, which is already a requirement for WebTransport over HTTP/3. + The RESET_STREAM_AT [I-D.draft-ietf-quic-reliable-stream-reset] + extension to QUIC can be used by MOQT, but the protocol is also + designed to work correctly when the extension is not supported. + + There is no definition of the protocol over other transports, such as + TCP, and applications using MoQ might need to fallback to another + protocol when QUIC or WebTransport aren't available. + + MOQT uses ALPN in QUIC and "WT-Available-Protocols" in WebTransport + ([WebTransport], Section 3.3) to perform version negotiation. + + [[RFC editor: please remove the remainder of this section before + publication.]] + + The ALPN value [RFC7301] for the final version of this specification + is moqt. ALPNs used to identify IETF drafts are created by appending + the draft number to "moqt-". For example, draft-ietf-moq- + transport-13 would be identified as "moqt-13". + + Note: Draft versions prior to -15 all used moq-00 ALPN, followed by + version negotiation in the CLIENT_SETUP and SERVER_SETUP messages. + +3.1.1. WebTransport + + An MOQT server that is accessible via WebTransport can be identified + using an HTTPS URI ([RFC9110], Section 4.2.2). An MOQT session can + be established by sending an extended CONNECT request to the host and + the path indicated by the URI, as described in ([WebTransport], + Section 3). + + + + + + + +Nandakumar, et al. Expires 17 July 2026 [Page 18] + +Internet-Draft moq-transport January 2026 + + +3.1.2. QUIC + + An MOQT server that is accessible via native QUIC can be identified + by a URI with a "moqt" scheme. The "moqt" URI scheme is defined as + follows, using definitions from [RFC3986]: + + moqt-URI = "moqt" "://" authority path-abempty [ "?" query ] + + The authority portion MUST NOT contain an empty host portion. The + moqt URI scheme supports the /.well-known/ path prefix defined in + [RFC8615]. + + This protocol does not specify any semantics on the path-abempty and + query portions of the URI. The contents of those are left up to the + application. + + The client can establish a connection to an MOQT server identified by + a given URI by setting up a QUIC connection to the host and port + identified by the authority section of the URI. The authority, path- + abempty and query portions of the URI are also transmitted in SETUP + parameters (see Section 9.3.1). If the port is omitted in the URI, a + default port of 443 is used for setting up the QUIC connection. + +3.1.3. Connection URL + + Each track MAY have one or more associated connection URLs specifying + network hosts through which a track may be accessed. The syntax of + the Connection URL and the associated connection setup procedures are + specific to the underlying transport protocol usage (see Section 3). + +3.2. Extension Negotiation + + Endpoints use the exchange of Setup messages to negotiate MOQT + extensions. Extensions can define new Message types, new Parameters, + or new framing for Data Streams and Datagrams. + + The client and server MUST include all Setup Parameters Section 9.3.1 + required for the negotiated MOQT version in CLIENT_SETUP and + SERVER_SETUP. + + Clients request the use of extensions by specifying Parameters in + CLIENT_SETUP. The Server responds with Parameters in the + SERVER_SETUP to indicate any extensions it supports. + + New versions of MOQT MUST specify which existing extensions can be + used with that version. New extensions MUST specify the existing + versions with which they can be used. + + + + +Nandakumar, et al. Expires 17 July 2026 [Page 19] + +Internet-Draft moq-transport January 2026 + + +3.3. Session initialization + + The first stream opened is a client-initiated bidirectional control + stream where the endpoints exchange Setup messages (Section 9.3), + followed by other messages defined in Section 9. + + This specification only specifies two uses of bidirectional streams, + the control stream, which begins with CLIENT_SETUP, and + SUBSCRIBE_NAMESPACE. Bidirectional streams MUST NOT begin with any + other message type unless negotiated. If they do, the peer MUST + close the Session with a Protocol Violation. Objects are sent on + unidirectional streams. + + A unidirectional stream containing Objects or bidirectional stream(s) + containing a SUBSCRIBE_NAMESPACE could arrive prior to the control + stream, in which case the data SHOULD be buffered until the control + stream arrives and setup is complete. If an implementation does not + want to buffer, it MAY reset other bidirectional streams before the + session and control stream are established. + + The control stream MUST NOT be closed at the underlying transport + layer during the session's lifetime. Doing so results in the session + being closed as a PROTOCOL_VIOLATION. + +3.4. Termination + + The Transport Session can be terminated at any point. When native + QUIC is used, the session is closed using the CONNECTION_CLOSE frame + ([QUIC], Section 19.19). When WebTransport is used, the session is + closed using the CLOSE_WEBTRANSPORT_SESSION capsule ([WebTransport], + Section 6). + + When terminating the Session, the application MAY use any error + message and SHOULD use a relevant code, as defined below: + + NO_ERROR (0x0): The session is being terminated without an error. + + INTERNAL_ERROR (0x1): An implementation specific error occurred. + + UNAUTHORIZED (0x2): The client is not authorized to establish a + session. + + PROTOCOL_VIOLATION (0x3): The remote endpoint performed an action + that was disallowed by the specification. + + INVALID_REQUEST_ID (0x4): The session was closed because the + + + + + +Nandakumar, et al. Expires 17 July 2026 [Page 20] + +Internet-Draft moq-transport January 2026 + + + endpoint used a Request ID that was smaller than or equal to a + previously received request ID, or the least- significant bit of + the request ID was incorrect for the endpoint. + + DUPLICATE_TRACK_ALIAS (0x5): The endpoint attempted to use a Track + Alias that was already in use. + + KEY_VALUE_FORMATTING_ERROR (0x6): The key-value pair has a + formatting error. + + TOO_MANY_REQUESTS (0x7): The session was closed because the endpoint + used a Request ID equal to or larger than the current Maximum + Request ID. + + INVALID_PATH (0x8): The PATH parameter was used by a server, on a + WebTransport session, or the server does not support the path. + + MALFORMED_PATH (0x9): The PATH parameter does not conform to the + rules in Section 9.3.1.2. + + GOAWAY_TIMEOUT (0x10): The session was closed because the peer took + too long to close the session in response to a GOAWAY + (Section 9.4) message. See session migration (Section 3.5). + + CONTROL_MESSAGE_TIMEOUT (0x11): The session was closed because the + peer took too long to respond to a control message. + + DATA_STREAM_TIMEOUT (0x12): The session was closed because the peer + took too long to send data expected on an open Data Stream (see + Section 10). This includes fields of a stream header or an object + header within a data stream. If an endpoint times out waiting for + a new object header on an open subgroup stream, it MAY send a + STOP_SENDING on that stream or terminate the subscription. + + AUTH_TOKEN_CACHE_OVERFLOW (0x13): The Session limit Section 9.3.1.4 + of the size of all registered Authorization tokens has been + exceeded. + + DUPLICATE_AUTH_TOKEN_ALIAS (0x14): Authorization Token attempted to + register an Alias that was in use (see Section 9.2.2.1). + + VERSION_NEGOTIATION_FAILED (0x15): The client didn't offer a version + supported by the server. + + MALFORMED_AUTH_TOKEN (0x16): Invalid Auth Token serialization during + registration (see Section 9.2.2.1). + + UNKNOWN_AUTH_TOKEN_ALIAS (0x17): No registered token found for the + + + +Nandakumar, et al. Expires 17 July 2026 [Page 21] + +Internet-Draft moq-transport January 2026 + + + provided Alias (see Section 9.2.2.1). + + EXPIRED_AUTH_TOKEN (0x18): Authorization token has expired + (Section 9.2.2.1). + + INVALID_AUTHORITY (0x19): The specified AUTHORITY does not + correspond to this server or cannot be used in this context. + + MALFORMED_AUTHORITY (0x1A): The AUTHORITY value is syntactically + invalid. + + An endpoint MAY choose to treat a subscription or request specific + error as a session error under certain circumstances, closing the + entire session in response to a condition with a single subscription + or message. Implementations need to consider the impact on other + outstanding subscriptions before making this choice. + +3.5. Migration + + MOQT requires a long-lived and stateful session. However, a service + provider needs the ability to shutdown/restart a server without + waiting for all sessions to drain naturally, as that can take days + for long-form media. MOQT enables proactively draining sessions via + the GOAWAY message (Section 9.4). + + The server sends a GOAWAY message, signaling the client to establish + a new session and migrate any Established subscriptions. The GOAWAY + message optionally contains a new URI for the new session, otherwise + the current URI is reused. The server SHOULD close the session with + GOAWAY_TIMEOUT after a sufficient timeout if there are still open + subscriptions or fetches on a connection. + + When the server is a subscriber, it SHOULD send a GOAWAY message to + downstream subscribers prior to any UNSUBSCRIBE messages to upstream + publishers. + + After the client receives a GOAWAY, it's RECOMMENDED that the client + waits until there are no more Established subscriptions before + closing the session with NO_ERROR. Ideally this is transparent to + the application using MOQT, which involves establishing a new session + in the background and migrating Established subscriptions and + published namespaces. The client can choose to delay closing the + session if it expects more OBJECTs to be delivered. The server + closes the session with a GOAWAY_TIMEOUT if the client doesn't close + the session quickly enough. + + + + + + +Nandakumar, et al. Expires 17 July 2026 [Page 22] + +Internet-Draft moq-transport January 2026 + + +3.6. Congestion Control + + MOQT does not specify a congestion controller, but there are + important attributes to consider when selecting a congestion + controller for use with an application built on top of MOQT. + +3.6.1. Bufferbloat + + Traditional AIMD congestion controllers (ex. CUBIC [RFC9438] and + Reno [RFC6582]) are prone to Bufferbloat. Bufferbloat occurs when + elements along the path build up a substantial queue of packets, + commonly more than doubling the round trip time. These queued + packets cause head-of-line blocking and latency, even when there is + no packet loss. + +3.6.2. Application-Limited + + The average bitrate for latency sensitive content needs to be less + than the available bandwidth, otherwise data will be queued and/or + dropped. As such, many MOQT applications will typically be limited + by the available data to send, and not the congestion controller. + Many congestion control algorithms only increase the congestion + window or bandwidth estimate if fully utilized. This combination can + lead to underestimating the available network bandwidth. As a + result, applications might need to periodically ensure the congestion + controller is not app-limited for at least a full round trip to + ensure the available bandwidth can be measured. + +3.6.3. Consistent Throughput + + Congestion control algorithms are commonly optimized for throughput, + not consistency. For example, BBR's PROBE_RTT state halves the + sending rate for more than a round trip in order to obtain an + accurate minimum RTT. Similarly, Reno halves it's congestion window + upon detecting loss. In both cases, the large reduction in sending + rate might cause issues with latency sensitive applications. + +4. Modularity + + MOQT defines all messages necessary to implement both simple + publishing or subscribing endpoints as well as highly functional + Relays. Non-Relay endpoints MAY implement only the subset of + functionality required to perform necessary tasks. For example, a + limited media player could operate using only SUBSCRIBE related + messages. Limited endpoints SHOULD respond to any unsupported + messages with the appropriate NOT_SUPPORTED error code, rather than + ignoring them. + + + + +Nandakumar, et al. Expires 17 July 2026 [Page 23] + +Internet-Draft moq-transport January 2026 + + + Relays MUST implement all MOQT messages defined in this document, as + well as processing rules described in Section 8. + +5. Publishing and Retrieving Tracks + +5.1. Subscriptions + + All subscriptions begin in the Idle state. A subscription can be + initiated and moved to the Pending state by either a publisher or a + subscriber. A publisher initiates a subscription to a track by + sending the PUBLISH message. The subscriber either accepts or + rejects the subscription using PUBLISH_OK or REQUEST_ERROR. A + subscriber initiates a subscription to a track by sending the + SUBSCRIBE message. The publisher either accepts or rejects the + subscription using SUBSCRIBE_OK or REQUEST_ERROR. Once either of + these sequences is successful, the subscription moves to the + Established state and can be updated by the subscriber using + REQUEST_UPDATE. Either endpoint can terminate an Established + subscription, moving it to the Terminated state. The subscriber + terminates a subscription in the Pending (Subscriber) or Established + states using UNSUBSCRIBE, the publisher terminates a subscription in + the Pending (Publisher) or Established states using PUBLISH_DONE. + + This diagram shows the subscription state machine: + + + + + + + + + + + + + + + + + + + + + + + + + + + +Nandakumar, et al. Expires 17 July 2026 [Page 24] + +Internet-Draft moq-transport January 2026 + + + +--------+ + | Idle | + +--------+ + | | + SUBSCRIBE | | PUBLISH + (subscriber)| | (publisher) + V V + +--------------+ +--------------+ + | Pending | | Pending | + +----| (Subscriber) | | (Publisher) |----+ + | +--------------+ +--------------+ | + | | | | + REQUEST_ERROR | SUBSCRIBE_OK | | PUBLISH_OK | REQUEST_ERROR + (publisher) | (publisher)| | (subscriber) | (subscriber) + | V V | + | +-------------+ | + | | Established | ------+ + | | | | REQUEST_UPDATE + | +-------------+ <-----+ + | | | | + +---- UNSUBSCRIBE | | PUBLISH_DONE ----+ + | (subscriber)| | (publisher) | + | V V | + | +-------------+ | + +----------->| Terminated | <------------+ + +-------------+ + + A publisher MUST send exactly one SUBSCRIBE_OK or REQUEST_ERROR in + response to a SUBSCRIBE. A subscriber MUST send exactly one + PUBLISH_OK or REQUEST_ERROR in response to a PUBLISH. The peer + SHOULD close the session with a protocol error if it receives more + than one. + + A publisher MUST save the Largest Location communicated in PUBLISH or + SUBSCRIBE_OK when establishing a subscription. This value can be + used in a Joining FETCH (see Section 9.16.2) at any time while the + subscription is active. + + All Established subscriptions have a Forward State which is either 0 + or 1. The publisher does not send Objects if the Forward State is 0, + and does send them if the Forward State is 1. The initiator of the + subscription sets the initial Forward State in either PUBLISH or + SUBSCRIBE. The subscriber can send PUBLISH_OK or REQUEST_UPDATE to + update the Forward State. Control messages, such as PUBLISH_DONE + (Section 9.15) are sent regardless of the forward state. + + + + + + +Nandakumar, et al. Expires 17 July 2026 [Page 25] + +Internet-Draft moq-transport January 2026 + + + Either endpoint can initiate a subscription to a track without + exchanging any prior messages other than SETUP. Relays MUST NOT send + any PUBLISH messages without knowing the client is interested in and + authorized to receive the content. The communication of intent and + authorization can be accomplished by the client sending + SUBSCRIBE_NAMESPACE, or conveyed in other mechanisms out of band. + + An endpoint MAY SUBSCRIBE to a Track it is publishing, though only + Relays are required to handle such a SUBSCRIBE. Such self- + subscriptions are identical to subscriptions initiated by other + endpoints, and all published Objects will be forwarded back to the + endpoint, subject to priority and congestion response rules. + + For a given Track, an endpoint can have at most one subscription to a + Track acting as the publisher and at most one acting as a subscriber. + If an endpoint receives a message attempting to establish a second + subscription to a Track with the same role, it MUST fail that request + with a DUPLICATE_SUBSCRIPTION error. + + If a publisher receives a SUBSCRIBE request for a Track with an + existing subscription in Pending (publisher) state, it MUST fail that + request with a DUPLICATE_SUBSCRIPTION error. If a subscriber + receives a PUBLISH for a Track with a subscription in the Pending + (Subscriber) state, it MUST ensure the subscription it initiated + transitions to the Terminated state before sending PUBLISH_OK. + + A publisher SHOULD begin sending incomplete objects when available to + avoid incurring additional latency. + + Publishers MAY start sending Objects on PUBLISH-initiated + subscriptions before receiving a PUBLISH_OK response to reduce + latency. Doing so can consume unnecessary resources in cases where + the Subscriber rejects the subscription with REQUEST_ERROR or sets + Forward State=0 in PUBLISH_OK. It can also result in the Subscriber + dropping Objects if its buffering limits are exceeded (see + Section 10.3 and Section 10.4.2). + +5.1.1. Subscription State Management + + A subscriber keeps subscription state until it sends UNSUBSCRIBE, or + after receipt of a PUBLISH_DONE or REQUEST_ERROR. Note that + PUBLISH_DONE does not usually indicate that state can immediately be + destroyed, see Section 9.15. + + The Publisher can destroy subscription state as soon as it has + received UNSUBSCRIBE. It MUST reset any open streams associated with + the SUBSCRIBE. + + + + +Nandakumar, et al. Expires 17 July 2026 [Page 26] + +Internet-Draft moq-transport January 2026 + + + The Publisher can also immediately delete subscription state after + sending PUBLISH_DONE, but MUST NOT send it until it has closed all + related streams. + + A REQUEST_ERROR indicates no objects will be delivered, and both + endpoints can immediately destroy relevant state. Objects MUST NOT + be sent for requests that end with an error. + +5.1.2. Subscription Filters + + Subscribers can specify a filter on a subscription indicating to the + publisher which Objects to send. Subscriptions without a filter pass + all Objects published or received via upstream subscriptions. + + All filters have a Start Location and an optional End Group. Only + objects published or received via a subscription having Locations + greater than or equal to Start Location and strictly less than or + equal to the End Group (when present) pass the filter. + + Some filters are defined to be relative to the Largest Object. The + Largest Object is the Object with the largest Location + (Section 1.4.1) in the Track from the perspective of the publisher + processing the message. Largest Object updates when the first byte + of an Object with a Location larger than the previous value is + published or received through a subscription. + + A Subscription Filter has the following structure: + + Subscription Filter { + Filter Type (i), + [Start Location (Location),] + [End Group (i),] + } + + Filter Type can have one of the following values: + + Largest Object (0x2): The filter Start Location is {Largest + Object.Group, Largest Object.Object + 1} and Largest Object is + communicated in SUBSCRIBE_OK. If no content has been delivered yet, + the filter Start Location is {0, 0}. There is no End Group - the + subscription is open ended. Note that due to network reordering or + prioritization, relays can receive Objects with Locations smaller + than Largest Object after the SUBSCRIBE is processed, but these + Objects do not pass the Largest Object filter. + + Next Group Start (0x1): The filter Start Location is {Largest + Object.Group + 1, 0} and Largest Object is communicated in + SUBSCRIBE_OK. If no content has been delivered yet, the filter Start + + + +Nandakumar, et al. Expires 17 July 2026 [Page 27] + +Internet-Draft moq-transport January 2026 + + + Location is {0, 0}. There is no End Group - the subscription is open + ended. For scenarios where the subscriber intends to start from more + than one group in the future, it can use an AbsoluteStart filter + instead. + + AbsoluteStart (0x3): The filter Start Location is specified + explicitly. The specified Start Location MAY be less than the + Largest Object observed at the publisher. There is no End Group - + the subscription is open ended. An AbsoluteStart filter with Start = + {0, 0} is equivalent to an unfiltered subscription. + + AbsoluteRange (0x4): The filter Start Location and End Group are + specified explicitly. The specified Start Location MAY be less than + the Largest Object observed at the publisher. If the specified End + Group is the same group specified in Start Location, the remainder of + that Group passes the filter. End Group MUST specify the same or a + larger Group than specified in Start Location. + + An endpoint that receives a filter type other than the above MUST + close the session with PROTOCOL_VIOLATION. + +5.1.3. Joining an Ongoing Track + + The MOQT Object model is designed with the concept that the beginning + of a Group is a join point, so in order for a subscriber to join a + Track, it needs to request an existing Group or wait for a future + Group. Different applications will have different approaches for + when to begin a new Group. + + To join a Track at a past Group, the subscriber sends a SUBSCRIBE + with Filter Type Largest Object followed by a Joining FETCH (see + Section 9.16.2) for the intended start Group, which can be relative. + To join a Track at the next Group, the subscriber sends a SUBSCRIBE + with Filter Type Next Group Start. + +5.1.3.1. Dynamically Starting New Groups + + While some publishers will deterministically create new Groups, other + applications might want to only begin a new Group when needed. A + subscriber joining a Track might detect that it is more efficient to + request the Original Publisher create a new group than issue a + Joining FETCH. Publishers indicate a Track supports dynamic group + creation using the DYNAMIC_GROUPS parameter (Section 11.1.1.3). + + One possible subscriber pattern is to SUBSCRIBE to a Track using + Filter Type Largest Object and observe the Largest Location in the + response. If the Object ID is below the application's threshold, the + subscriber sends a FETCH for the beginning of the Group. If the + + + +Nandakumar, et al. Expires 17 July 2026 [Page 28] + +Internet-Draft moq-transport January 2026 + + + Object ID is above the threshold and the Track supports dynamic + groups, the subscriber sends a REQUEST_UPDATE message with the + NEW_GROUP_REQUEST parameter equal to the Largest Location's Group, + plus one (see Section 9.2.2.9). + + Another possible subscriber pattern is to send a SUBSCRIBE with + Filter Type Next Group Start and NEW_GROUP_REQUEST equal to 0. The + value of DYNAMIC_GROUPS in SUBSCRIBE_OK will indicate if the + publisher supports dynamic groups. A publisher that does will begin + the next group as soon as practical. + +5.2. Fetch State Management + + The publisher MUST send exactly one FETCH_OK or REQUEST_ERROR in + response to a FETCH. + + A subscriber keeps FETCH state until it sends FETCH_CANCEL, receives + REQUEST_ERROR, or receives a FIN or RESET_STREAM for the FETCH data + stream. If the data stream is already open, it MAY send STOP_SENDING + for the data stream along with FETCH_CANCEL, but MUST send + FETCH_CANCEL. + + The Publisher can destroy fetch state as soon as it has received a + FETCH_CANCEL. It MUST reset any open streams associated with the + FETCH. It can also destroy state after closing the FETCH data + stream. + + It can destroy all FETCH state after closing the data stream with a + FIN. + + A REQUEST_ERROR indicates that both endpoints can immediately destroy + state. Since a relay can start delivering FETCH Objects from cache + before determining the result of the request, some Objects could be + received even if the FETCH results in error. + +6. Namespace Discovery + + Discovery of MOQT servers is always done out-of-band. Namespace + discovery can be done in the context of an established MOQT session. + + Given sufficient out of band information, it is valid for a + subscriber to send a SUBSCRIBE or FETCH message to a publisher + (including a relay) without any previous MOQT messages besides SETUP. + However, SUBSCRIBE_NAMESPACE, PUBLISH and PUBLISH_NAMESPACE messages + provide an in-band means of discovery of publishers for a namespace. + + The syntax of these messages is described in Section 9. + + + + +Nandakumar, et al. Expires 17 July 2026 [Page 29] + +Internet-Draft moq-transport January 2026 + + +6.1. Subscribing to Namespaces + + If the subscriber is aware of a namespace of interest, it can send + SUBSCRIBE_NAMESPACE to publishers/relays it has established a session + with. The recipient of this message will send any relevant + NAMESPACE, NAMESPACE_DONE or PUBLISH messages for that namespace, or + more specific part of that namespace. This includes echoing back + published Tracks and/or Track Namespaces under the + SUBSCRIBE_NAMESPACE prefix to the endpoint that sent them. If an + endpoint accepts its own PUBLISH, this behaves as self-subscription + described in Section 5.1. + + The subscriber sends SUBSCRIBE_NAMESPACE on a new bidirectional + stream and the publisher MUST send a single REQUEST_OK or + REQUEST_ERROR as the first message on the bidirectional stream in + response to a SUBSCRIBE_NAMESPACE. The subscriber SHOULD close the + session with a protocol error if it detects receiving more than one. + + The receiver of a REQUEST_OK or REQUEST_ERROR ought to forward the + result to the application, so the application can decide which other + publishers to contact, if any. + + A SUBSCRIBE_NAMESPACE can be cancelled by closing the stream with + either a FIN or RESET_STREAM. Cancelling does not prohibit original + publishers from sending further PUBLISH_NAMESPACE or PUBLISH + messages, but relays MUST NOT send any further PUBLISH messages to a + client without knowing the client is interested in and authorized to + receive the content. + +6.2. Publishing Namespaces + + A publisher MAY send PUBLISH_NAMESPACE messages to any subscriber. A + PUBLISH_NAMESPACE indicates to the subscriber that the publisher has + tracks available in that namespace. A subscriber MAY send SUBSCRIBE + or FETCH for tracks in a namespace without having received a + PUBLISH_NAMESPACE for it. + + If a publisher is authoritative for a given namespace, or is a relay + that has received an authorized PUBLISH_NAMESPACE for that namespace + from an upstream publisher, it MUST send a PUBLISH_NAMESPACE to any + subscriber that has subscribed via SUBSCRIBE_NAMESPACE for that + namespace, or a prefix of that namespace. A publisher MAY send the + PUBLISH_NAMESPACE to any other subscriber. + + An endpoint SHOULD report the reception of a REQUEST_OK or + REQUEST_ERROR to the application to inform the search for additional + subscribers for a namespace, or to abandon the attempt to publish + under this namespace. This might be especially useful in upload or + + + +Nandakumar, et al. Expires 17 July 2026 [Page 30] + +Internet-Draft moq-transport January 2026 + + + chat applications. A subscriber MUST send exactly one REQUEST_OK or + REQUEST_ERROR in response to a PUBLISH_NAMESPACE. The publisher + SHOULD close the session with a protocol error if it receives more + than one. + + A PUBLISH_NAMESPACE_DONE message withdraws a previous + PUBLISH_NAMESPACE, although it is not a protocol error for the + subscriber to send a SUBSCRIBE or FETCH message for a track in a + namespace after receiving an PUBLISH_NAMESPACE_DONE. + + A subscriber can send PUBLISH_NAMESPACE_CANCEL to revoke acceptance + of an PUBLISH_NAMESPACE, for example due to expiration of + authorization credentials. The message enables the publisher to + PUBLISH_NAMESPACE again with refreshed authorization, or discard + associated state. After receiving an PUBLISH_NAMESPACE_CANCEL, the + publisher does not send PUBLISH_NAMESPACE_DONE. + + While PUBLISH_NAMESPACE indicates to relays how to connect publishers + and subscribers, it is not a full-fledged routing protocol and does + not protect against loops and other phenomena. In particular, + PUBLISH_NAMESPACE SHOULD NOT be used to find paths through richly + connected networks of relays. + + A subscriber MAY send a SUBSCRIBE or FETCH for a track to any + publisher. If it has accepted a PUBLISH_NAMESPACE with a namespace + that exactly matches the namespace for that track, it SHOULD only + request it from the senders of those PUBLISH_NAMESPACE messages. + +7. Priorities + + MoQ priorities allow a subscriber and original publisher to influence + the transmission order of Objects within a session in the presence of + congestion. + +7.1. Definitions + + MOQT maintains priorities between different schedulable objects. A + schedulable object in MOQT is either: + + 1. The first or next Object in a Subgroup that is in response to a + subscription. + + 2. An Object with forwarding preference Datagram. + + 3. An Object in response to a FETCH where that Object is the next + Object in the response. + + + + + +Nandakumar, et al. Expires 17 July 2026 [Page 31] + +Internet-Draft moq-transport January 2026 + + + An Object is not schedulable if it is known that no part of it can be + written due to underlying transport flow control limits. + + A single subgroup or datagram has a single publisher priority. + Within a response to SUBSCRIBE, it can be useful to conceptualize + this process as scheduling subgroups or datagrams instead of + individual objects on them. FETCH responses however can contain + objects with different publisher priorities. + + A priority numberis an unsigned integer with a value between 0 and + 255. A lower priority number indicates higher priority; the highest + priority is 0. + + Subscriber Priority is a priority number associated with an + individual request. It is specified in the SUBSCRIBE or FETCH + message, and can be updated via REQUEST_UPDATE message. The + subscriber priority of an individual schedulable object is the + subscriber priority of the request that caused that object to be + sent. When subscriber priority is changed, a best effort SHOULD be + made to apply the change to all objects that have not been scheduled, + but it is implementation dependent what happens to objects that have + already been scheduled. + + Publisher Priority is a priority number associated with an individual + schedulable object. A default can be specified in the parameters of + PUBLISH, or SUBSCRIBE_OK. Publisher priority can also be specified + in a subgroup header or datagram (see Section 10). + + Group Order is a property of an individual subscription. It can be + either 'Ascending' (groups with lower group ID are sent first), or + 'Descending' (groups with higher group ID are sent first). The + subscriber optionally communicates its group order preference in the + SUBSCRIBE message; the publisher's preference is used if the + subscriber did not express one (by setting Group Order field to value + 0x0). The group order of an existing subscription cannot be changed. + +7.2. Scheduling Algorithm + + When an MOQT publisher has multiple schedulable objects it can choose + between, the objects SHOULD be selected as follows: + + 1. If two objects have different subscriber priorities associated + with them, the one with *the highest subscriber priority* is + scheduled to be sent first. + + 2. If two objects have the same subscriber priority, but different + publisher priorities, the one with *the highest publisher + priority* is scheduled to be sent first. + + + +Nandakumar, et al. Expires 17 July 2026 [Page 32] + +Internet-Draft moq-transport January 2026 + + + 3. If two objects in response to the same request have the same + subscriber and publisher priority, but belong to two different + groups of the same track, *the group order* of the associated + subscription is used to decide the one that is scheduled to be + sent first. + + 4. If two objects in response to the same request have the same + subscriber and publisher priority and belong to the same group of + the same track, the one with *the lowest Subgroup ID* (for + objects with forwarding preference Subgroup), or *the lowest + Object ID* (for objects with forwarding preference Datagram) is + scheduled to be sent first. If the two objects have different + Forwarding Preferences the order is implementation dependent. + + The definition of "scheduled to be sent first" in the algorithm is + implementation dependent and is constrained by the prioritization + interface of the underlying transport. For some implementations, it + could mean that the object is serialized and passed to the underlying + transport first. Other implementations can control the order packets + are initially transmitted. + + This algorithm does not provide a well-defined ordering for objects + that belong to different subscriptions or FETCH responses, but have + the same subscriber and publisher priority. The ordering in those + cases is implementation-defined, though the expectation is that all + subscriptions will be able to send some data. + + A publisher might not utilize the entire available congestion window, + session flow control, or all available streams for lower priority + Objects if it expects higher priority Objects will be available to + send in the near future or it wants to reserve some bandwidth for + control messages. + + Given the critical nature of control messages and their relatively + small size, the control stream SHOULD be prioritized higher than all + subscribed Objects. + +7.3. Considerations for Setting Priorities + + For downstream subscriptions, relays SHOULD respect the subscriber + and original publisher's priorities. Relays can receive + subscriptions with conflicting subscriber priorities or Group Order + preferences. Relays SHOULD NOT directly use Subscriber Priority or + Group Order from incoming subscriptions for upstream subscriptions. + Relays' use of these fields for upstream subscriptions can be based + on factors specific to it, such as the popularity of the content or + policy, or relays can specify the same value for all upstream + subscriptions. + + + +Nandakumar, et al. Expires 17 July 2026 [Page 33] + +Internet-Draft moq-transport January 2026 + + + MoQ Sessions can span multiple namespaces, and priorities might not + be coordinated across namespaces. The subscriber's priority is + considered first, so there is a mechanism for a subscriber to fix + incompatibilities between different namespaces prioritization + schemes. Additionally, it is anticipated that when multiple + namespaces are present within a session, the namespaces could be + coordinating, possibly part of the same application. In cases when + pooling among namespaces is expected to cause issues, multiple MoQ + sessions, either within a single connection or on multiple + connections can be used. + + Implementations that have a default priority SHOULD set it to a value + in the middle of the range (eg: 128) to allow non-default priorities + to be set either higher or lower. + +8. Relays + + Relays are leveraged to enable distribution scale in the MoQ + architecture. Relays can be used to form an overlay delivery + network, similar in functionality to Content Delivery Networks + (CDNs). Additionally, relays serve as policy enforcement points by + validating subscribe and publish requests at the edge of a network. + + Relays are endpoints, which means they terminate Transport Sessions + in order to have visibility of MoQ Object metadata. + +8.1. Caching Relays + + Relays MAY cache Objects, but are not required to. + + A caching relay saves Objects to its cache identified by the Object's + Full Track Name, Group ID and Object ID. If multiple objects are + received with the same Full Track Name, Group ID and Object ID, + Relays MAY ignore subsequently received Objects or MAY use them to + update certain cached fields. Implementations that update the cache + need to protect against cache poisoning. The only Object fields that + can be updated are the following: + + 1. Object can transition from existing to not existing in cases + where the object is no longer available. + + 2. Object Extension Headers can be added, removed or updated, + subject to the constraints of the specific header extension. + + An endpoint that receives a duplicate Object with a different + Forwarding Preference, Subgroup ID, Priority or Payload MUST treat + the track as Malformed. + + + + +Nandakumar, et al. Expires 17 July 2026 [Page 34] + +Internet-Draft moq-transport January 2026 + + + For ranges of objects that do not exist, relays MAY change the + representation of a missing range to a semantically equivalent one. + For instance, a relay may change an End-of-Group="Y" Subgroup Header + to an equivalent object with an End of Group status, or a Prior Group + ID Gap extension could be removed in FETCH, where it's redundant. + + Note that due to reordering, an implementation can receive an Object + after receiving an indication that the Object in question does not + exist. The endpoint SHOULD NOT cache or forward the object in this + case. + + A cache MUST store all properties of an Object defined in + Section 10.2.1, with the exception of any extensions + (Section 10.2.1.2) that specify otherwise. + +8.2. Forward Handling + + Relays SHOULD set the Forward flag to 1 when a new subscription needs + to be sent upstream, regardless of the value of the Forward field + from the downstream subscription. Subscriptions that are not + forwarded consume resources from the publisher, so a publisher might + deprioritize, reject, or close those subscriptions to ensure other + subscriptions can be delivered. + +8.3. Multiple Publishers + + A Relay can receive PUBLISH_NAMESPACE for the same Track Namespace or + PUBLISH messages for the same Track from multiple publishers. The + following sections explain how Relays maintain subscriptions to all + available publishers for a given Track. + + There is no specified limit to the number of publishers of a Track + Namespace or Track. An implementation can use mechanisms such as + REQUEST_ERROR, UNSUBSCRIBE or PUBLISH_NAMESPACE_CANCEL if it cannot + accept an additional publisher due to implementation constraints. + Implementations can consider the establishment or idle time of the + session or subscription to determine which publisher to reject or + disconnect. + + Relays MUST handle Objects for the same Track from multiple + publishers and forward them to matching Established subscriptions. + The Relay SHOULD attempt to deduplicate Objects before forwarding, + subject to implementation constraints. + + + + + + + + +Nandakumar, et al. Expires 17 July 2026 [Page 35] + +Internet-Draft moq-transport January 2026 + + +8.4. Subscriber Interactions + + Subscribers request Tracks by sending a SUBSCRIBE (see Section 9.9) + or FETCH (see Section 9.16) control message for each Track of + interest. Relays MUST ensure subscribers are authorized to access + the content associated with the Track. The authorization information + can be part of request itself or part of the encompassing session. + The specifics of how a relay authorizes a user are outside the scope + of this specification. + + The relay MUST have an Established upstream subscription before + sending SUBSCRIBE_OK in response to a downstream SUBSCRIBE. If a + relay does not have sufficient information to send a FETCH_OK + immediately in response to a FETCH, it MUST withhold sending FETCH_OK + until it does. + + Publishers maintain a list of Established downstream subscriptions + for each Track. Relays use the Track Alias (Section 10.1) of an + incoming Object to identify its Track and find the current + subscribers. Each new Object belonging to the Track is forwarded to + each subscriber, as allowed by the subscription's filter (see + Section 9.9), and delivered according to the priority (see Section 7) + and delivery timeout (see Section 9.2.2.2). + + A relay MUST NOT reorder or drop objects received on a multi-object + stream when forwarding to subscribers, unless it has application + specific information. + + Relays MAY aggregate authorized subscriptions for a given Track when + multiple subscribers request the same Track. Subscription + aggregation allows relays to make only a single upstream subscription + for the Track. The published content received from the upstream + subscription request is cached and shared among the pending + subscribers. Because MOQT restricts widening a subscription, relays + that aggregate upstream subscriptions can subscribe using the Largest + Object filter to avoid churn as downstream subscribers with disparate + filters subscribe and unsubscribe from a Track. + + A subscriber remains subscribed to a Track at a Relay until it + unsubscribes, the upstream publisher terminates the subscription, or + the subscription expires (see Section 9.10). A subscription with a + filter can reach a state where all possible Objects matching the + filter have been delivered to the subscriber. Since tracking this + can be prohibitively expensive, Relays are not required or expected + to do so. + + + + + + +Nandakumar, et al. Expires 17 July 2026 [Page 36] + +Internet-Draft moq-transport January 2026 + + +8.4.1. Graceful Subscriber Relay Switchover + + This section describes a behavior that a Subscriber MAY implement to + improve user experience when a relay sends a GOAWAY or the Subscriber + switches between networks, such as WiFi to Cellular, and QUIC + Connection Migration is not possible. + + When a subscriber receives the GOAWAY message, it starts the process + of connecting to a new relay and sending the SUBSCRIBE requests for + all Established subscriptions to the new relay. The new relay will + send a response to the subscribes and if they are successful, the + subscriptions to the old relay can be stopped with an UNSUBSCRIBE. + +8.5. Publisher Interactions + + There are two ways to publish through a relay: + + 1. Send a PUBLISH message for a specific Track to the relay. The + relay MAY respond with PUBLISH_OK in Forward State=0 until there + are known subscribers for new Tracks. + + 2. Send a PUBLISH_NAMESPACE message for a Track Namespace to the + relay. This enables the relay to send SUBSCRIBE or FETCH + messages to publishers for Tracks in this Namespace in response + to requests received from subscribers. + + Relays MUST verify that publishers are authorized to publish the set + of Tracks whose Track Namespace matches the namespace in a + PUBLISH_NAMESPACE, or the Full Track Name in PUBLISH. Relays MUST + NOT assume that an authorized publisher of a single Track is + implicitly authorized to publish any other Tracks or Track + Namespaces. If a Publisher would like Subscriptions in a Namespace + routed to it, it MUST send an explicit PUBLISH_NAMESPACE. The + authorization and identification of the publisher depends on the way + the relay is managed and is application specific. + + When a publisher wants to stop new subscriptions for a published + namespace it sends a PUBLISH_NAMESPACE_DONE. A subscriber indicates + it will no longer subcribe to Tracks in a namespace it previously + responded REQUEST_OK to by sending a PUBLISH_NAMESPACE_CANCEL. + + A Relay connects publishers and subscribers by managing sessions + based on the Track Namespace or Full Track Name. When a SUBSCRIBE + message is sent, its Full Track Name is matched exactly against + existing upstream subscriptions. + + + + + + +Nandakumar, et al. Expires 17 July 2026 [Page 37] + +Internet-Draft moq-transport January 2026 + + + Namespace Prefix Matching is further used to decide which publishers + receive a SUBSCRIBE and which subscribers receive a PUBLISH. In this + process, the fields in the Track Namespace are matched sequentially, + requiring an exact match for each field. If the published or + subscribed Track Namespace has the same or fewer fields than the + Track Namespace in the message, it qualifies as a match. + + For example: A SUBSCRIBE message with namespace=(foo, bar) and name=x + will match sessions that sent PUBLISH_NAMESPACE messages with + namespace=(foo) or namespace=(foo, bar). It will not match a session + with namespace=(foobar). + + Relays MUST send SUBSCRIBE messages to all matching publishers. This + includes matching both Established subscriptions on the Full Track + Name and Namespace Prefix Matching against published Namespaces. + Relays MUST forward PUBLISH_NAMESPACE or PUBLISH messages to all + matching subscribers. + + When a Relay needs to make an upstream FETCH request, it determines + the available publishers using the same matching rules as SUBSCRIBE. + When more than one publisher is available, the Relay MAY send the + FETCH to any of them. + + When a Relay receives an authorized SUBSCRIBE for a Track with one or + more Established upstream subscriptions, it MUST reply with + SUBSCRIBE_OK. If the SUBSCRIBE has Forward State=1 and the upstream + subscriptions are in Forward State=0, the Relay MUST send + REQUEST_UPDATE with Forward=1 to all publishers. If there are no + Established upstream subscriptions for the requested Track, the Relay + MUST send a SUBSCRIBE request to each publisher that has published + the subscription's namespace or prefix thereof. If the SUBSCRIBE has + Forward=1, then the Relay MUST use Forward=1 when subscribing + upstream. + + When a relay receives an incoming PUBLISH message, it MUST send a + PUBLISH request to each subscriber that has subscribed (via + SUBSCRIBE_NAMESPACE) to the Track's namespace or prefix thereof. + + When a relay receives an authorized PUBLISH_NAMESPACE for a namespace + that matches one or more existing subscriptions to other upstream + sessions, it MUST send a SUBSCRIBE to the publisher that sent the + PUBLISH_NAMESPACE for each matching subscription. When it receives + an authorized PUBLISH message for a Track that has Established + downstream subscriptions, it MUST respond with PUBLISH_OK. If at + least one downstream subscriber for the Track has Forward State=1, + the Relay MUST use Forward State=1 in the reply. + + + + + +Nandakumar, et al. Expires 17 July 2026 [Page 38] + +Internet-Draft moq-transport January 2026 + + + If a Session is closed due to an unknown or invalid control message + or Object, the Relay MUST NOT propagate that message or Object to + another Session, because it would enable a single Session error to + force an unrelated Session, which might be handling other + subscriptions, to be closed. + +8.5.1. Graceful Publisher Relay Switchover + + This section describes a behavior that a publisher MAY implement to + improve user experience when a relay sends a GOAWAY or the publisher + switches between networks, such as WiFi to Cellular, and QUIC + Connection Migration is not possible. + + A new Session is established, to a new URI if specified in a GOAWAY. + The publisher sends PUBLISH_NAMESPACE and/or PUBLISH messages to + begin publishing on the new Session, but it does not immediately stop + publishing Objects on the old Session. + + Once the subscriptions have migrated over to the new session, the + publisher can stop publishing Objects on the old session. The relay + will attempt to deduplicate Objects received on both subscriptions. + Ideally, the subscriptions downstream from the relay do not observe + this change, and keep receiving the Objects on the same subscription. + +8.6. Relay Track Handling + + A relay MUST include all Extension Headers associated with a Track + when sending any PUBLISH, SUBSCRIBE_OK, REQUEST_OK when in response + to a TRACK_STATUS, or FETCH_OK, unless allowed by the extension's + specification (see Section 2.5). + +8.7. Relay Object Handling + + MOQT encodes the delivery information via Object headers + (Section 10.2). A relay MUST NOT modify Object properties when + forwarding, except for Object Extension Headers as specified in + Section 2.5. + + A relay MUST treat the object payload as opaque. A relay MUST NOT + combine, split, or otherwise modify object payloads. A relay SHOULD + prioritize sending Objects based on Section 7. + +9. Control Messages + + MOQT uses a single bidirectional stream to exchange control messages, + as defined in Section 3.3. Every single message on the control + stream is formatted as follows: + + + + +Nandakumar, et al. Expires 17 July 2026 [Page 39] + +Internet-Draft moq-transport January 2026 + + + MOQT Control Message { + Message Type (i), + Message Length (16), + Message Payload (..), + } + + Figure 3: MOQT Control Message + + The following Message Types are defined: + + +======+============================================+ + | ID | Messages | + +======+============================================+ + | 0x01 | RESERVED (SETUP for version 00) | + +------+--------------------------------------------+ + | 0x40 | RESERVED (CLIENT_SETUP for versions <= 10) | + +------+--------------------------------------------+ + | 0x41 | RESERVED (SERVER_SETUP for versions <= 10) | + +------+--------------------------------------------+ + | 0x20 | CLIENT_SETUP (Section 9.3) | + +------+--------------------------------------------+ + | 0x21 | SERVER_SETUP (Section 9.3) | + +------+--------------------------------------------+ + | 0x10 | GOAWAY (Section 9.4) | + +------+--------------------------------------------+ + | 0x15 | MAX_REQUEST_ID (Section 9.5) | + +------+--------------------------------------------+ + | 0x1A | REQUESTS_BLOCKED (Section 9.6) | + +------+--------------------------------------------+ + | 0x7 | REQUEST_OK (Section 9.7) | + +------+--------------------------------------------+ + | 0x5 | REQUEST_ERROR (Section 9.8) | + +------+--------------------------------------------+ + | 0x3 | SUBSCRIBE (Section 9.9) | + +------+--------------------------------------------+ + | 0x4 | SUBSCRIBE_OK (Section 9.10) | + +------+--------------------------------------------+ + | 0x2 | REQUEST_UPDATE (Section 9.11) | + +------+--------------------------------------------+ + | 0xA | UNSUBSCRIBE (Section 9.12) | + +------+--------------------------------------------+ + | 0x1D | PUBLISH (Section 9.13) | + +------+--------------------------------------------+ + | 0x1E | PUBLISH_OK (Section 9.14) | + +------+--------------------------------------------+ + | 0xB | PUBLISH_DONE (Section 9.15) | + +------+--------------------------------------------+ + | 0x16 | FETCH (Section 9.16) | + + + +Nandakumar, et al. Expires 17 July 2026 [Page 40] + +Internet-Draft moq-transport January 2026 + + + +------+--------------------------------------------+ + | 0x18 | FETCH_OK (Section 9.17) | + +------+--------------------------------------------+ + | 0x17 | FETCH_CANCEL (Section 9.18) | + +------+--------------------------------------------+ + | 0xD | TRACK_STATUS (Section 9.19) | + +------+--------------------------------------------+ + | 0x6 | PUBLISH_NAMESPACE (Section 9.20) | + +------+--------------------------------------------+ + | 0x8 | NAMESPACE (Section 9.21) | + +------+--------------------------------------------+ + | 0x9 | PUBLISH_NAMESPACE_DONE (Section 9.22) | + +------+--------------------------------------------+ + | 0xE | NAMESPACE_DONE (Section 9.23) | + +------+--------------------------------------------+ + | 0xC | PUBLISH_NAMESPACE_CANCEL (Section 9.24) | + +------+--------------------------------------------+ + | 0x11 | SUBSCRIBE_NAMESPACE (Section 9.25) | + +------+--------------------------------------------+ + + Table 1 + + An endpoint that receives an unknown message type MUST close the + session. Control messages have a length to make parsing easier, but + no control messages are intended to be ignored. The length is set to + the number of bytes in Message Payload, which is defined by each + message type. If the length does not match the length of the Message + Payload, the receiver MUST close the session with a + PROTOCOL_VIOLATION. + +9.1. Request ID + + Most MOQT control messages contain a session specific Request ID. + The Request ID correlates requests and responses, allows endpoints to + update or terminate ongoing requests, and supports the endpoint's + ability to limit the concurrency and frequency of requests. Request + IDs for one endpoint increment independently from those sent by the + peer endpoint. The client's Request ID starts at 0 and are even and + the server's Request ID starts at 1 and are odd. The Request ID + increments by 2 with each FETCH, SUBSCRIBE, REQUEST_UPDATE, + SUBSCRIBE_NAMESPACE, PUBLISH, PUBLISH_NAMESPACE or TRACK_STATUS + request. Other messages with a Request ID field reference the + Request ID of another message for correlation. If an endpoint + receives a Request ID that is not valid for the peer, or a new + request with a Request ID that is not the next in sequence or exceeds + the received MAX_REQUEST_ID, it MUST close the session with + INVALID_REQUEST_ID. + + + + +Nandakumar, et al. Expires 17 July 2026 [Page 41] + +Internet-Draft moq-transport January 2026 + + +9.2. Parameters + + Some messages include a Parameters field that encodes optional + message elements. Parameters in the CLIENT_SETUP and SERVER_SETUP + messages are called Setup Parameters. Parameters in other control + messages are Message Parameters. Receivers ignore unrecognized Setup + Parameters. All Message Parameters MUST be defined in the negotiated + version of MOQT or negotiated via Setup Parameters. An endpoint that + receives an unknown Message Parameter MUST close the session with + PROTOCOL_VIOLATION. + + Senders MUST NOT repeat the same parameter type in a message unless + the parameter definition explicitly allows multiple instances of that + type to be sent in a single message. Receivers SHOULD check that + there are no unexpected duplicate parameters and close the session as + a PROTOCOL_VIOLATION if found. Receivers MUST allow duplicates of + unknown Setup Parameters. + + The number of parameters in a message is not specifically limited, + but the total length of a control message is limited to 2^16-1 bytes. + + Parameters are serialized as Key-Value-Pairs Figure 2. + + Setup Parameters use a namespace that is constant across all MOQT + versions. All other messages use a version-specific namespace. For + example, the integer '1' can refer to different parameters for Setup + messages and for all other message types. SETUP message parameter + types are defined in Section 9.3.1. Version-specific parameter types + are defined in Section 9.2.2. + + Message Parameters in SUBSCRIBE, PUBLISH_OK and FETCH MUST NOT cause + the publisher to alter the payload of the objects it sends, as that + would violate the track uniqueness guarantee described in + Section 2.4.3. + +9.2.1. Parameter Scope + + Message Parameters are always intended for the peer endpoint only and + are not forwarded by Relays, though relays can consider received + parameter values when making a request. Any Track metadata sent by + the publisher that is forwarded to subscribers is sent as Track + Extension header. + + + + + + + + + +Nandakumar, et al. Expires 17 July 2026 [Page 42] + +Internet-Draft moq-transport January 2026 + + +9.2.2. Message Parameters + + Each message parameter definition indicates the message types in + which it can appear. If it appears in some other type of message, it + MUST be ignored. Note that since Setup parameters use a separate + namespace, it is impossible for these parameters to appear in Setup + messages. + +9.2.2.1. AUTHORIZATION TOKEN Parameter + + The AUTHORIZATION TOKEN parameter (Parameter Type 0x03) MAY appear in + a PUBLISH, SUBSCRIBE, REQUEST_UPDATE, SUBSCRIBE_NAMESPACE, + PUBLISH_NAMESPACE, TRACK_STATUS or FETCH message. This parameter + conveys information to authorize the sender to perform the operation + carrying the parameter. + + The parameter value is a Token structure containing an optional + Session-specific Alias. The Alias allows the sender to reference a + previously transmitted Token Type and Token Value in future messages. + The Token structure is serialized as follows: + + Token { + Alias Type (i), + [Token Alias (i),] + [Token Type (i),] + [Token Value (..)] + } + + Figure 4: Token structure + + * Alias Type - an integer defining both the serialization and the + processing behavior of the receiver. This Alias type has the + following code points: + + DELETE (0x0): There is an Alias but no Type or Value. This Alias + and the Token Value it was previously associated with| MUST be + retired. Retiring removes them from the pool of actively + registered tokens. + + REGISTER (0x1): There is an Alias, a Type and a Value. This Alias + MUST be associated with the Token Value for the duration of the + Session or it is deleted. This action is termed "registering" the + Token. + + USE_ALIAS (0x2): There is an Alias but no Type or Value. Use the + Token Type and Value previously registered with this Alias. + + USE_VALUE (0x3): There is no Alias and there is a Type and Value. + + + +Nandakumar, et al. Expires 17 July 2026 [Page 43] + +Internet-Draft moq-transport January 2026 + + + Use the Token Value as provided. The Token Value may be discarded + after processing. + + If a server receives Alias Type DELETE (0x0) or USE_ALIAS (0x2) in a + CLIENT_SETUP message, it MUST close the session with a + PROTOCOL_VIOLATION. + + * Token Alias - a Session-specific integer identifier that + references a Token Value. There are separate Alias spaces for the + client and server (e.g.: they can each register Alias=1). Once a + Token Alias has been registered, it cannot be re-registered by the + same endpoint in the Session without first being deleted. Use of + the Token Alias is optional. + + * Token Type - a numeric identifier for the type of Token payload + being transmitted. This type is defined by the IANA table "MOQT + Auth Token Type" (see Section 13). Type 0 is reserved to indicate + that the type is not defined in the table and is negotiated out- + of-band between client and receiver. + + * Token Value - the payload of the Token. The contents and + serialization of this payload are defined by the Token Type. + + If the Token structure cannot be decoded, the receiver MUST close the + Session with KEY_VALUE_FORMATTING_ERROR. The receiver of a message + attempting to register an Alias which is already registered MUST + close the Session with DUPLICATE_AUTH_TOKEN_ALIAS. The receiver of a + message referencing an Alias that is not currently registered MUST + reject the message with UNKNOWN_AUTH_TOKEN_ALIAS. + + The receiver of a message containing a well-formed Token structure + but otherwise invalid AUTHORIZATION TOKEN parameter MUST reject that + message with an MALFORMED_AUTH_TOKEN error. + + The receiver of a message carrying an AUTHORIZATION TOKEN with Alias + Type REGISTER that does not result in a Session error MUST register + the Token Alias, in the token cache, even if the message fails for + other reasons, including Unauthorized. This allows senders to + pipeline messages that refer to previously registered tokens without + potentially terminating the entire Session. A receiver MAY store an + error code (eg: UNAUTHORIZED or MALFORMED_AUTH_TOKEN) in place of the + Token Type and Token Alias if any future message referencing the + Token Alias will result in that error. However, it is important to + not store an error code for a token that might be valid in the future + or due to some other property becoming fulfilled which currently + isn't. The size of a registered cache entry includes the length of + the Token Value, regardless of whether it is stored. + + + + +Nandakumar, et al. Expires 17 July 2026 [Page 44] + +Internet-Draft moq-transport January 2026 + + + If a receiver detects that an authorization token has expired, it + MUST retain the registered Alias until it is deleted by the sender, + though it MAY discard other state associated with the token that is + no longer needed. Expiration does not affect the size occupied by a + token in the token cache. Any message that references the token with + Alias Type USE_ALIAS fails with EXPIRED_AUTH_TOKEN. + + Using an Alias to refer to a previously registered Token Type and + Value is for efficiency only and has the same effect as if the Token + Type and Value was included directly. Retiring an Alias that was + previously used to authorize a message has no retroactive effect on + the original authorization, nor does it prevent that same Token Type + and Value from being re-registered. + + Senders of tokens SHOULD only register tokens which they intend to + re-use during the Session and SHOULD retire previously registered + tokens once their utility has passed. + + By registering a Token, the sender is requiring the receiver to store + the Token Alias and Token Value until they are deleted, or the + Session ends. The receiver can protect its resources by sending a + SETUP parameter defining the MAX_AUTH_TOKEN_CACHE_SIZE limit (see + Section 9.3.1.4) it is willing to accept. If a registration is + attempted which would cause this limit to be exceeded, the receiver + MUST termiate the Session with a AUTH_TOKEN_CACHE_OVERFLOW error. + + The AUTHORIZATION TOKEN parameter MAY be repeated within a message as + long as the combination of Token Type and Token Value are unique + after resolving any aliases. + +9.2.2.2. DELIVERY TIMEOUT Parameter + + The DELIVERY TIMEOUT parameter (Parameter Type 0x02) MAY appear in a + PUBLISH_OK, SUBSCRIBE, or REQUEST_UPDATE message. + + It is the duration in milliseconds the relay SHOULD continue to + attempt forwarding Objects after they have been received. The start + time for the timeout is based on when the Object Headers are + received, and does not depend upon the forwarding preference. + Objects with forwarding preference 'Datagram' are not retransmitted + when lost, so the Delivery Timeout only limits the amount of time + they can be queued before being sent. There is no explicit signal + that an Object was not sent because the delivery timeout was + exceeded. + + DELIVERY_TIMEOUT, if present, MUST contain a value greater than 0. + If an endpoint receives a DELIVERY_TIMEOUT equal to 0 it MUST close + the session with PROTOCOL_VIOLATION. + + + +Nandakumar, et al. Expires 17 July 2026 [Page 45] + +Internet-Draft moq-transport January 2026 + + + If both the subscriber specifies this parameter and the Track has a + DELIVERY_TIMEOUT extension, the endpoints use the min of the two + values for the subscription. + + Publishers can, at their discretion, discontinue forwarding Objects + earlier than the negotiated DELIVERY TIMEOUT, subject to stream + closure and ordering constraints described in Section 10.4.3. + However, if neither the subscriber nor publisher specifies DELIVERY + TIMEOUT, all Objects in the track matching the subscription filter + are delivered as indicated by their Group Order and Priority. If a + subscriber fails to consume Objects at a sufficient rate, causing the + publisher to exceed its resource limits, the publisher MAY terminate + the subscription with error TOO_FAR_BEHIND. + + If an object in a subgroup exceeds the delivery timeout, the + publisher MUST reset the underlying transport stream (see + Section 10.4.3) and SHOULD NOT attempt to open a new stream to + deliver additional Objects in that Subgroup. + + This parameter is intended to be specific to a subscription, so it + SHOULD NOT be forwarded upstream by a relay that intends to serve + multiple subscriptions for the same track. + + Publishers SHOULD consider whether the entire Object can likely be + successfully delivered within the timeout period before sending any + data for that Object, taking into account priorities, congestion + control, and any other relevant information. + +9.2.2.3. SUBSCRIBER PRIORITY Parameter + + The SUBSCRIBER_PRIORITY parameter (Parameter Type 0x20) MAY appear in + a SUBSCRIBE, FETCH, REQUEST_UPDATE (for a subscription or FETCH), + PUBLISH_OK message. It is an integer expressing the priority of a + subscription relative to other subscriptions and fetch responses in + the same session. Lower numbers get higher priority. See Section 7. + The range is restricted to 0-255. If a publisher receives a value + outside this range, it MUST close the session with + PROTOCOL_VIOLATION. + + If omitted from SUBSCRIBE, PUBLISH_OK or FETCH, the publisher uses + the value 128. + +9.2.2.4. GROUP ORDER Parameter + + The GROUP_ORDER parameter (Parameter Type 0x22) MAY appear in a + SUBSCRIBE, PUBLISH_OK, or FETCH. + + + + + +Nandakumar, et al. Expires 17 July 2026 [Page 46] + +Internet-Draft moq-transport January 2026 + + + It is an enum indicating how to prioritize Objects from different + groups within the same subscription (see Section 7), or how to order + Groups in a Fetch response (see Section 9.16.3). The allowed values + are Ascending (0x1) or Descending (0x2). If an endpoint receives a + value outside this range, it MUST close the session with + PROTOCOL_VIOLATION. + + If omitted from SUBSCRIBE, the publisher's preference from the Track + is used. If omitted from FETCH, the receiver uses Ascending (0x1). + +9.2.2.5. SUBSCRIPTION FILTER Parameter + + The SUBSCRIPTION_FILTER parameter (Parameter Type 0x21) MAY appear in + a SUBSCRIBE, PUBLISH_OK or REQUEST_UPDATE (for a subscription) + message. It is a length-prefixed Subscription Filter (see + Section 5.1.2). If the length of the Subscription Filter does not + match the parameter length, the publisher MUST close the session with + PROTOCOL_VIOLATION. + + If omitted from SUBSCRIBE or PUBLISH_OK, the subscription is + unfiltered. If omitted from REQUEST_UPDATE, the value is unchanged. + +9.2.2.6. EXPIRES Parameter + + The EXPIRES parameter (Parameter Type 0x8) MAY appear in + SUBSCRIBE_OK, PUBLISH or PUBLISH_OK (TODO: or REQUEST_OK). It is a + variable length integer encoding the time in milliseconds after which + the sender of the parameter will terminate the subscription. The + sender will terminate the subscription using PUBLISH_DONE or + UNSUBSCRIBE, depending on its role. This value is advisory and the + sender can terminate the subscription prior to or after the expiry + time. + + The receiver of the parameter can extend the subscription by sending + a REQUEST_UPDATE. If the receiver of the parameter has one or more + updated AUTHORIZATION_TOKENs, it SHOULD include those in the + REQUEST_UPDATE. Relays that send this parameter and applications + that receive it MAY introduce jitter to prevent many endpoints from + updating simultaneously. + + If the EXPIRES parameter is 0 or is not present in a message, the + subscription does not expire or expires at an unknown time. + + + + + + + + + +Nandakumar, et al. Expires 17 July 2026 [Page 47] + +Internet-Draft moq-transport January 2026 + + +9.2.2.7. LARGEST OBJECT Parameter + + The LARGEST_OBJECT parameter (Parameter Type 0x9) MAY appear in + SUBSCRIBE_OK, PUBLISH or in REQUEST_OK (in response to REQUEST_UPDATE + or TRACK_STATUS). It is a length-prefixed Location structure (see + Section 1.4.1) containing the largest Location in the Track observed + by the sending endpoint (see Section 5.1.2. If Objects have been + published on this Track the Publisher MUST include this parameter. + + If omitted from a message, the sending endpoint has not published or + received any Objects in the Track. + +9.2.2.8. FORWARD Parameter + + The FORWARD parameter (Parameter Type 0x10) MAY appear in SUBSCRIBE, + REQUEST_UPDATE (for a subscription), PUBLISH, PUBLISH_OK and + SUBSCRIBE_NAMESPACE. It is a variable length integer specifying the + Forwarding State on affected subscriptions (see Section 5.1). The + allowed values are 0 (don't forward) or 1 (forward). If an endpoint + receives a value outside this range, it MUST close the session with + PROTOCOL_VIOLATION. + + If the parameter is omitted from REQUEST_UPDATE, the value for the + subscription remains unchanged. If the parameter is omitted from any + other message, the default value is 1. + +9.2.2.9. NEW GROUP REQUEST Parameter + + The NEW_GROUP_REQUEST parameter (parameter type 0x32) MAY appear in + PUBLISH_OK, SUBSCRIBE or REQUEST_UPDATE for a subscription. It is an + integer representing the largest Group ID in the Track known by the + subscriber, plus 1. A value of 0 indicates that the subscriber has + no Group information for the Track. A subscriber MUST NOT send this + parameter in PUBLISH_OK or REQUEST_UPDATE if the Track did not + include the DYNAMIC_GROUPS Extension with value 1. A subscriber MAY + include this parameter in SUBSCRIBE without foreknowledge of support. + If the original publisher does not support dynamic Groups, it ignores + the parameter in that case. + + When an Original Publisher that supports dynamic Groups receives a + NEW_GROUP_REQUEST with a value of 0 or a value larger than the + current Group, it SHOULD end the current Group and begin a new Group + as soon as practical. The Original Publisher MAY delay the + NEW_GROUP_REQUEST subject to implementation specific concerns, for + example, acheiving a minimum duration for each Group. The Original + Publisher chooses the next Group ID; there are no requirements that + it be equal to the NEW_GROUP_REQUEST parameter value. + + + + +Nandakumar, et al. Expires 17 July 2026 [Page 48] + +Internet-Draft moq-transport January 2026 + + + Relay Handling: + + A relay that receives a NEW_GROUP_REQUEST for a Track without an + Established subscription MUST include the NEW_GROUP_REQUEST when + subscribing upstream. + + A relay that receives a NEW_GROUP_REQUEST for an Established + subscription with a value of 0 or a value larger than the Largest + Group MUST send a REQUEST_UPDATE including the NEW_GROUP_REQUEST to + the publisher unless: + + 1. The Track does not support dynamic Groups + + 2. There is already an outstanding NEW_GROUP_REQUEST from this Relay + with a greater or equal value + + If a relay receives a NEW_GROUP_REQUEST with a non-zero value less + than or equal to the Largest Group, it does not send a + NEW_GROUP_REQUEST upstream. + + After sending a NEW_GROUP_REQUEST upstream, the request is considered + outstanding until the Largest Group increases. + +9.3. CLIENT_SETUP and SERVER_SETUP + + The CLIENT_SETUP and SERVER_SETUP messages are the first messages + exchanged by the client and the server; they allow the endpoints to + agree on the initial configuration before any control messsages are + exchanged. The messages contain a sequence of key-value pairs called + Setup parameters; the semantics and format of which can vary based on + whether the client or server is sending. To ensure future + extensibility of MOQT, endpoints MUST ignore unknown setup + parameters. TODO: describe GREASE for Setup Parameters. + + The wire format of the Setup messages are as follows: + + + + + + + + + + + + + + + + +Nandakumar, et al. Expires 17 July 2026 [Page 49] + +Internet-Draft moq-transport January 2026 + + + CLIENT_SETUP Message { + Type (i) = 0x20, + Length (16), + Number of Parameters (i), + Setup Parameters (..) ..., + } + + SERVER_SETUP Message { + Type (i) = 0x21, + Length (16), + Number of Parameters (i), + Setup Parameters (..) ..., + } + + Figure 5: MOQT Setup Messages + + The available Setup parameters are detailed in the next sections. + +9.3.1. Setup Parameters + +9.3.1.1. AUTHORITY + + The AUTHORITY parameter (Parameter Type 0x05) allows the client to + specify the authority component of the MoQ URI when using native QUIC + ([QUIC]). It MUST NOT be used by the server, or when WebTransport is + used. When an AUTHORITY parameter is received from a server, or when + an AUTHORITY parameter is received while WebTransport is used, or + when an AUTHORITY parameter is received by a server but the server + does not support the specified authority, the session MUST be closed + with INVALID_AUTHORITY. + + The AUTHORITY parameter follows the URI formatting rules [RFC3986]. + When connecting to a server using a URI with the "moqt" scheme, the + client MUST set the AUTHORITY parameter to the authority portion of + the URI. If an AUTHORITY parameter does not conform to these rules, + the session MUST be closed with MALFORMED_AUTHORITY. + +9.3.1.2. PATH + + The PATH parameter (Parameter Type 0x01) allows the client to specify + the path of the MoQ URI when using native QUIC ([QUIC]). It MUST NOT + be used by the server, or when WebTransport is used. When a PATH + parameter is received from a server, or when a PATH parameter is + received while WebTransport is used, or when a PATH parameter is + received by a server but the server does not support the specified + path, the session MUST be closed with INVALID_PATH. + + + + + +Nandakumar, et al. Expires 17 July 2026 [Page 50] + +Internet-Draft moq-transport January 2026 + + + The PATH parameter follows the URI formatting rules [RFC3986]. When + connecting to a server using a URI with the "moqt" scheme, the client + MUST set the PATH parameter to the path-abempty portion of the URI; + if query is present, the client MUST concatenate ?, followed by the + query portion of the URI to the parameter. If a PATH does not + conform to these rules, the session MUST be closed with + MALFORMED_PATH. + +9.3.1.3. MAX_REQUEST_ID + + The MAX_REQUEST_ID parameter (Parameter Type 0x02) communicates an + initial value for the Maximum Request ID to the receiving endpoint. + The default value is 0, so if not specified, the peer MUST NOT send + requests. + +9.3.1.4. MAX_AUTH_TOKEN_CACHE_SIZE + + The MAX_AUTH_TOKEN_CACHE_SIZE parameter (Parameter Type 0x04) + communicates the maximum size in bytes of all actively registered + Authorization tokens that the endpoint is willing to store per + Session. This parameter is optional. The default value is 0 which + prohibits the use of token Aliases. + + The token size is calculated as 16 bytes + the size of the Token + Value field (see Figure 4). The total size as restricted by the + MAX_AUTH_TOKEN_CACHE_SIZE parameter is calculated as the sum of the + token sizes for all registered tokens (Alias Type value of 0x01) + minus the sum of the token sizes for all deregistered tokens (Alias + Type value of 0x00), since Session initiation. + +9.3.1.5. AUTHORIZATION TOKEN + + The AUTHORIZATION TOKEN setup parameter (Parameter Type 0x03)) is + funcionally equivalient to the AUTHORIZATION TOKEN message parameter, + see Section 9.2.2.1. The endpoint can specify one or more tokens in + CLIENT_SETUP or SERVER_SETUP that the peer can use to authorize MOQT + session establishment. + + If a server receives an AUTHORIZATION TOKEN parameter in CLIENT_SETUP + with Alias Type REGISTER that exceeds its MAX_AUTH_TOKEN_CACHE_SIZE, + it MUST NOT fail the session with AUTH_TOKEN_CACHE_OVERFLOW. + Instead, it MUST treat the parameter as Alias Type USE_VALUE. A + client MUST handle registration failures of this kind by purging any + Token Aliases that failed to register based on the + MAX_AUTH_TOKEN_CACHE_SIZE parameter in SERVER_SETUP (or the default + value of 0). + + + + + +Nandakumar, et al. Expires 17 July 2026 [Page 51] + +Internet-Draft moq-transport January 2026 + + +9.3.1.6. MOQT IMPLEMENTATION + + The MOQT_IMPLEMENTATION parameter (Parameter Type 0x07) identifies + the name and version of the sender's MOQT implementation. This + SHOULD be a UTF-8 encoded string [RFC3629], though the message does + not carry information, such as language tags, that would aid + comprehension by any entity other than the one that created the text. + +9.4. GOAWAY + + An endpoint sends a GOAWAY message to inform the peer it intends to + close the session soon. Servers can use GOAWAY to initiate session + migration (Section 3.5) with an optional URI. + + The GOAWAY message does not impact subscription state. A subscriber + SHOULD individually UNSUBSCRIBE for each existing subscription, while + a publisher MAY reject new requests after sending a GOAWAY. + + Upon receiving a GOAWAY, an endpoint SHOULD NOT initiate new requests + to the peer including SUBSCRIBE, PUBLISH, FETCH, PUBLISH_NAMESPACE, + SUBSCRIBE_NAMESPACE and TRACK_STATUS. + + Sending a GOAWAY does not prevent the sender from initiating new + requests, though the sender SHOULD avoid initiating requests unless + required by migration (see (Section 8.4.1 and Section 8.5.1). An + endpoint that receives a GOAWAY MAY reject new requests with an + appropriate error code (e.g., SUBSCRIBE_ERROR with error code + GOING_AWAY). + + The endpoint MUST close the session with a PROTOCOL_VIOLATION + (Section 3.4) if it receives multiple GOAWAY messages. + + GOAWAY Message { + Type (i) = 0x10, + Length (16), + New Session URI Length (i), + New Session URI (..), + } + + Figure 6: MOQT GOAWAY Message + + + + + + + + + + + +Nandakumar, et al. Expires 17 July 2026 [Page 52] + +Internet-Draft moq-transport January 2026 + + + * New Session URI: When received by a client, indicates where the + client can connect to continue this session. The client MUST use + this URI for the new session if provided. If the URI is zero + bytes long, the current URI is reused instead. The new session + URI SHOULD use the same scheme as the current URI to ensure + compatibility. The maxmimum length of the New Session URI is + 8,192 bytes. If an endpoint receives a length exceeding the + maximum, it MUST close the session with a PROTOCOL_VIOLATION. + + If a server receives a GOAWAY with a non-zero New Session URI + Length it MUST close the session with a PROTOCOL_VIOLATION. + +9.5. MAX_REQUEST_ID + + An endpoint sends a MAX_REQUEST_ID message to increase the number of + requests the peer can send within a session. + + The Maximum Request ID MUST only increase within a session. If an + endpoint receives MAX_REQUEST_ID message with an equal or smaller + Request ID it MUST close the session with a PROTOCOL_VIOLATION. + + MAX_REQUEST_ID Message { + Type (i) = 0x15, + Length (16), + Max Request ID (i), + } + + Figure 7: MOQT MAX_REQUEST_ID Message + + * Max Request ID: The new Maximum Request ID for the session plus 1. + If a Request ID equal to or larger than this is received by the + endpoint that sent the MAX_REQUEST_ID in any request message + (PUBLISH_NAMESPACE, FETCH, SUBSCRIBE, SUBSCRIBE_NAMESPACE, + REQUEST_UPDATE or TRACK_STATUS), the endpoint MUST close the + session with an error of TOO_MANY_REQUESTS. + + MAX_REQUEST_ID is similar to MAX_STREAMS in ([RFC9000], Section 4.6), + and similar considerations apply when deciding how often to send + MAX_REQUEST_ID. For example, implementations might choose to + increase MAX_REQUEST_ID as subscriptions are closed to keep the + number of available subscriptions roughly consistent. + +9.6. REQUESTS_BLOCKED + + The REQUESTS_BLOCKED message is sent when an endpoint would like to + send a new request, but cannot because the Request ID would exceed + the Maximum Request ID value sent by the peer. The endpoint SHOULD + send only one REQUESTS_BLOCKED for a given Maximum Request ID. + + + +Nandakumar, et al. Expires 17 July 2026 [Page 53] + +Internet-Draft moq-transport January 2026 + + + An endpoint MAY send a MAX_REQUEST_ID upon receipt of + REQUESTS_BLOCKED, but it MUST NOT rely on REQUESTS_BLOCKED to trigger + sending a MAX_REQUEST_ID, because sending REQUESTS_BLOCKED is not + required. + + REQUESTS_BLOCKED Message { + Type (i) = 0x1A, + Length (16), + Maximum Request ID (i), + } + + Figure 8: MOQT REQUESTS_BLOCKED Message + + * Maximum Request ID: The Maximum Request ID for the session on + which the endpoint is blocked. More on Request ID in Section 9.1. + +9.7. REQUEST_OK + + The REQUEST_OK message is sent to a response to REQUEST_UPDATE, + TRACK_STATUS, SUBSCRIBE_NAMESPACE and PUBLISH_NAMESPACE requests. + The unique request ID in the REQUEST_OK is used to associate it with + the correct type of request. + + REQUEST_OK Message { + Type (i) = 0x7, + Length (16), + Request ID (i), + Number of Parameters (i), + Parameters (..) ... + } + + Figure 9: MOQT REQUEST_OK Message + + * Request ID: The Request ID to which this message is replying. + + * Parameters: The parameters are defined in Section 9.2.2. + +9.8. REQUEST_ERROR + + The REQUEST_ERROR message is sent to a response to any request + (SUBSCRIBE, FETCH, PUBLISH, SUBSCRIBE_NAMESPACE, PUBLISH_NAMESPACE, + TRACK_STATUS). The unique request ID in the REQUEST_ERROR is used to + associate it with the correct type of request. + + + + + + + + +Nandakumar, et al. Expires 17 July 2026 [Page 54] + +Internet-Draft moq-transport January 2026 + + + REQUEST_ERROR Message { + Type (i) = 0x5, + Length (16), + Request ID (i), + Error Code (i), + Retry Interval (i), + Error Reason (Reason Phrase), + } + + Figure 10: MOQT REQUEST_ERROR Message + + * Request ID: The Request ID to which this message is replying. + + * Error Code: Identifies an integer error code for request failure. + + * Retry Interval: The minimum time (in milliseconds) before the + request SHOULD be sent again, plus one. If the value is 0, the + request SHOULD NOT be retried. + + * Error Reason: Provides a text description of the request error. + See Section 1.4.3. + + The application SHOULD use a relevant error code in REQUEST_ERROR, as + defined below and assigned in Section 13.4.2. Most codepoints have + identical meanings for various request types, but some have request- + specific meanings. + + If a request is retryable with the same parameters at a later time, + the sender of REQUEST_ERROR includes a non-zero Retry Interval in the + message. To minimize the risk of synchronized retry storms, the + sender can apply randomization to each retry interval so that retries + are spread out over time. A Retry Interval value of 1 indicates the + request can be retried immediately. + + INTERNAL_ERROR: An implementation specific or generic error + occurred. + + UNAUTHORIZED: The subscriber is not authorized to perform the + requested action on the given track. This might be retryable if + the authorization token is not yet valid. + + TIMEOUT: The subscription could not be completed before an + implementation specific timeout. For example, a relay could not + establish an upstream subscription within the timeout. + + NOT_SUPPORTED: The endpoint does not support the type of request. + + MALFORMED_AUTH_TOKEN: Invalid Auth Token serialization during + + + +Nandakumar, et al. Expires 17 July 2026 [Page 55] + +Internet-Draft moq-transport January 2026 + + + registration (see Section 9.2.2.1). + + EXPIRED_AUTH_TOKEN: Authorization token has expired + (Section 9.2.2.1). + + DUPLICATE_SUBSCRIPTION (0x19): The PUBLISH or SUBSCRIBE request + attempted to create a subscription to a Track with the same role + as an existing subscription. + + Below are errors for use by the publisher. They can appear in + response to SUBSCRIBE, FETCH, TRACK_STATUS, and SUBSCRIBE_NAMESPACE, + unless otherwise noted. + + DOES_NOT_EXIST: The track or namespace is not available at the + publisher. + + INVALID_RANGE: In response to SUBSCRIBE or FETCH, specified Filter + or range of Locations cannot be satisfied. + + MALFORMED_TRACK: In response to a FETCH, a relay publisher detected + the track was malformed (see Section 2.4.2). + + The following are errors for use by the subscriber. They can appear + in response to PUBLISH or PUBLISH_NAMESPACE, unless otherwise noted. + + UNINTERESTED: The subscriber is not interested in the track or + namespace. + + Errors below can only be used in response to one message type. + + PREFIX_OVERLAP: In response to SUBSCRIBE_NAMESPACE, the namespace + prefix overlaps with another SUBSCRIBE_NAMESPACE in the same + session. + + INVALID_JOINING_REQUEST_ID: In response to a Joining FETCH, the + referenced Request ID is not an Established Subscription. + +9.9. SUBSCRIBE + + A subscription causes the publisher to send newly published objects + for a track. + + Subscribe only requests newly published or received Objects. Objects + from the past are retrieved using FETCH (Section 9.16). + + The format of SUBSCRIBE is as follows: + + + + + +Nandakumar, et al. Expires 17 July 2026 [Page 56] + +Internet-Draft moq-transport January 2026 + + + SUBSCRIBE Message { + Type (i) = 0x3, + Length (16), + Request ID (i), + Track Namespace (..), + Track Name Length (i), + Track Name (..), + Number of Parameters (i), + Parameters (..) ... + } + + Figure 11: MOQT SUBSCRIBE Message + + * Request ID: See Section 9.1. + + * Track Namespace: Identifies the namespace of the track as defined + in (Section 2.4.1). + + * Track Name: Identifies the track name as defined in + (Section 2.4.1). + + * Parameters: The parameters are defined in Section 9.2.2. + + On successful subscription, the publisher MUST reply with a + SUBSCRIBE_OK, allowing the subscriber to determine the start group/ + object when not explicitly specified, and start sending objects. + + If the publisher cannot satisfy the requested Subscription Filter + (see Section 9.2.2.5) or if the entire End Group has already been + published it SHOULD send a REQUEST_ERROR with code INVALID_RANGE. A + publisher MUST NOT send objects from outside the requested range. + + Subscribing with the FORWARD parameter (Section 9.2.2.8) equal to 0 + allows publisher or relay to prepare to serve the subscription in + advance, reducing the time to receive objects in the future. + +9.10. SUBSCRIBE_OK + + A publisher sends a SUBSCRIBE_OK control message for successful + subscriptions. + + + + + + + + + + + +Nandakumar, et al. Expires 17 July 2026 [Page 57] + +Internet-Draft moq-transport January 2026 + + + SUBSCRIBE_OK Message { + Type (i) = 0x4, + Length (16), + Request ID (i), + Track Alias (i), + Number of Parameters (i), + Parameters (..) ..., + Track Extensions (..), + } + + Figure 12: MOQT SUBSCRIBE_OK Message + + * Request ID: The Request ID of the SUBSCRIBE this message is + replying to Section 9.9. + + * Track Alias: The identifer used for this track in Subgroups or + Datagrams (see Section 10.1). The same Track Alias MUST NOT be + used to refer to two different Tracks simultaneously. If a + subscriber receives a SUBSCRIBE_OK that uses the same Track Alias + as a different track with an Established subscription, it MUST + close the session with error DUPLICATE_TRACK_ALIAS. + + * Parameters: The parameters are defined in Section 9.2.2. + + * Track Extensions : A sequence of Extension Headers. See + Section 2.5. + +9.11. REQUEST_UPDATE + + The sender of a request (SUBSCRIBE, PUBLISH, FETCH, TRACK_STATUS, + PUBLISH_NAMESPACE, SUBSCRIBE_NAMESPACE) can later send a + REQUEST_UPDATE to modify it. A subscriber can also send + REQUEST_UPDATE to modify parameters of a subscription established + with PUBLISH. + + The receiver of a REQUEST_UPDATE MUST respond with exactly one + REQUEST_OK or REQUEST_ERROR message indicating if the update was + successful. + + If a parameter previously set on the request is not present in + REQUEST_UPDATE, its value remains unchanged. + + There is no mechanism to remove a parameter from a request. + + The format of REQUEST_UPDATE is as follows: + + + + + + +Nandakumar, et al. Expires 17 July 2026 [Page 58] + +Internet-Draft moq-transport January 2026 + + + REQUEST_UPDATE Message { + Type (i) = 0x2, + Length (16), + Request ID (i), + Existing Request ID (i), + Number of Parameters (i), + Parameters (..) ... + } + + Figure 13: MOQT REQUEST_UPDATE Message + + * Request ID: See Section 9.1. + + * Existing Request ID: The Request ID of the request this message is + updating. This MUST match the Request ID of an existing request. + The receiver MUST close the session with PROTOCOL_VIOLATION if the + sender specifies an invalid Existing Request ID, or if the + parameters included in the REQUEST_UPDATE are invalid for the type + of request being modified. + + * Parameters: The parameters are defined in Section 9.2.2. + +9.11.1. Updating Subscriptions + + When a subscriber decreases the Start Location of the Subscription + Filter (see Section 5.1.2), the Start Location can be smaller than + the Track's Largest Location, similar to a new Subscription. FETCH + can be used to retrieve any necessary Objects smaller than the + current Largest Location. + + When a subscriber increases the End Location, the Largest Object at + the publisher might already be larger than the previous End Location. + This will create a gap in the subscription. The REQUEST_OK in + response to the REQUEST_UPDATE will include the LARGEST_OBJECT + parameter, and the subscriber can issue a FETCH to retrieve the + omitted Objects, if any. + + When a subscriber narrows their subscription (increase the Start + Location and/or decrease the End Group), it might still receive + Objects outside the new range if the publisher sent them before the + update was processed. + + When a subscription update is unsuccessful, the publisher MUST also + terminate the subscription with PUBLISH_DONE with error code + UPDATE_FAILED. + + + + + + +Nandakumar, et al. Expires 17 July 2026 [Page 59] + +Internet-Draft moq-transport January 2026 + + +9.12. UNSUBSCRIBE + + A Subscriber issues an UNSUBSCRIBE message to a Publisher indicating + it is no longer interested in receiving the specified Track, + indicating that the Publisher stop sending Objects as soon as + possible. + + The format of UNSUBSCRIBE is as follows: + + UNSUBSCRIBE Message { + Type (i) = 0xA, + Length (16), + Request ID (i) + } + + Figure 14: MOQT UNSUBSCRIBE Message + + * Request ID: The Request ID of the subscription that is being + terminated. See Section 9.9. + +9.13. PUBLISH + + The publisher sends the PUBLISH control message to initiate a + subscription to a track. The receiver verifies the publisher is + authorized to publish this track. + + PUBLISH Message { + Type (i) = 0x1D, + Length (16), + Request ID (i), + Track Namespace (..), + Track Name Length (i), + Track Name (..), + Track Alias (i), + Number of Parameters (i), + Parameters (..) ..., + Track Extensions (..), + } + + Figure 15: MOQT PUBLISH Message + + * Request ID: See Section 9.1. + + * Track Namespace: Identifies a track's namespace as defined in + (Section 2.4.1) + + * Track Name: Identifies the track name as defined in + (Section 2.4.1). + + + +Nandakumar, et al. Expires 17 July 2026 [Page 60] + +Internet-Draft moq-transport January 2026 + + + * Track Alias: The identifer used for this track in Subgroups or + Datagrams (see Section 10.1). The same Track Alias MUST NOT be + used to refer to two different Tracks simultaneously. If a + subscriber receives a PUBLISH that uses the same Track Alias as a + different track with an Established subscription, it MUST close + the session with error DUPLICATE_TRACK_ALIAS. + + * Parameters: The parameters are defined in Section 9.2.2. + + * Track Extensions : A sequence of Extension Headers. See + Section 2.5. + + A subscriber receiving a PUBLISH for a Track it does not wish to + receive SHOULD send REQUEST_ERROR with error code UNINTERESTED, and + abandon reading any publisher initiated streams associated with that + subscription using a STOP_SENDING frame. + + A publisher that sends the FORWARD parameter (Section 9.2.2.8) equal + to 0 indicates that it will not transmit any objects until the + subscriber sets the Forward State to 1. If the FORWARD parameter is + omitted or equal to 1, the publisher will start transmitting objects + immediately, possibly before PUBLISH_OK. + +9.14. PUBLISH_OK + + The subscriber sends a PUBLISH_OK control message to acknowledge the + successful authorization and acceptance of a PUBLISH message, and + establish a subscription. + + PUBLISH_OK Message { + Type (i) = 0x1E, + Length (16), + Request ID (i), + Number of Parameters (i), + Parameters (..) ..., + } + + Figure 16: MOQT PUBLISH_OK Message + + * Request ID: The Request ID of the PUBLISH this message is replying + to Section 9.13. + + * Parameters: The parameters are defined in Section 9.2.2. + + TODO: A similar section to SUBSCRIBE about how the publisher handles + a filter that is entirely behind Largest Object or is otherwise + invalid. + + + + +Nandakumar, et al. Expires 17 July 2026 [Page 61] + +Internet-Draft moq-transport January 2026 + + +9.15. PUBLISH_DONE + + A publisher sends a PUBLISH_DONE message to indicate it is done + publishing Objects for that subscription. The Status Code indicates + why the subscription ended, and whether it was an error. Because + PUBLISH_DONE is sent on the control stream, it is likely to arrive at + the receiver before late-arriving objects, and often even late- + opening streams. However, the receiver uses it as an indication that + it should receive any late-opening streams in a relatively short + time. + + Note that some objects in the subscribed track might never be + delivered, because a stream was reset, or never opened in the first + place, due to the delivery timeout. + + A sender MUST NOT send PUBLISH_DONE until it has closed all streams + it will ever open, and has no further datagrams to send, for a + subscription. After sending PUBLISH_DONE, the sender can immediately + destroy subscription state, although stream state can persist until + delivery completes. The sender might persist subscription state to + enforce the delivery timeout by resetting streams on which it has + already sent FIN, only deleting it when all such streams have + received ACK of the FIN. + + A sender MUST NOT destroy subscription state until it sends + PUBLISH_DONE, though it can choose to stop sending objects (and thus + send PUBLISH_DONE) for any reason. + + A subscriber that receives PUBLISH_DONE SHOULD set a timer of at + least its delivery timeout in case some objects are still inbound due + to prioritization or packet loss. The subscriber MAY dispense with a + timer if it sent UNSUBSCRIBE or is otherwise no longer interested in + objects from the track. Once the timer has expired, the receiver + destroys subscription state once all open streams for the + subscription have closed. A subscriber MAY discard subscription + state earlier, at the cost of potentially not delivering some late + objects to the application. The subscriber SHOULD send STOP_SENDING + on all streams related to the subscription when it deletes + subscription state. + + The format of PUBLISH_DONE is as follows: + + + + + + + + + + +Nandakumar, et al. Expires 17 July 2026 [Page 62] + +Internet-Draft moq-transport January 2026 + + + PUBLISH_DONE Message { + Type (i) = 0xB, + Length (16), + Request ID (i), + Status Code (i), + Stream Count (i), + Error Reason (Reason Phrase) + } + + Figure 17: MOQT PUBLISH_DONE Message + + * Request ID: The Request ID of the subscription that is being + terminated. See Section 9.9. + + * Status Code: An integer status code indicating why the + subscription ended. + + * Stream Count: An integer indicating the number of data streams the + publisher opened for this subscription. This helps the subscriber + know if it has received all of the data published in this + subscription by comparing the number of streams received. The + subscriber can immediately remove all subscription state once the + same number of streams have been processed. If the track had only + Objects with Forwarding Preference = Datagram, the publisher MUST + set Stream Count to 0. If the publisher is unable to set Stream + Count to the exact number of streams opened for the subscription, + it MUST set Stream Count to 2^62 - 1. Subscribers SHOULD use a + timeout or other mechanism to remove subscription state in case + the publisher set an incorrect value, reset a stream before the + SUBGROUP_HEADER, or set the maximum value. If a subscriber + receives more streams for a subscription than specified in Stream + Count, it MAY close the session with a PROTOCOL_VIOLATION. + + * Error Reason: Provides the reason for subscription error. See + Section 1.4.3. + + The application SHOULD use a relevant status code in PUBLISH_DONE, as + defined below: + + INTERNAL_ERROR (0x0): An implementation specific or generic error + occurred. + + UNAUTHORIZED (0x1): The subscriber is no longer authorized to + subscribe to the given track. + + TRACK_ENDED (0x2): The track is no longer being published. + + SUBSCRIPTION_ENDED (0x3): The publisher reached the end of an + + + +Nandakumar, et al. Expires 17 July 2026 [Page 63] + +Internet-Draft moq-transport January 2026 + + + associated subscription filter. + + GOING_AWAY (0x4): The subscriber or publisher issued a GOAWAY + message. + + EXPIRED (0x5): The publisher reached the timeout specified in + SUBSCRIBE_OK. + + TOO_FAR_BEHIND (0x6): The publisher's queue of objects to be sent to + the given subscriber exceeds its implementation defined limit. + + MALFORMED_TRACK (0x12): A relay publisher detected that the track + was malformed (see Section 2.4.2). + + UPDATE_FAILED (0x8): REQUEST_UPDATE failed on this subscription (see + Section 9.11). + +9.16. FETCH + + A subscriber issues a FETCH to a publisher to request a range of + already published objects within a track. + + There are three types of Fetch messages. + + +======+========================+ + | Code | Fetch Type | + +======+========================+ + | 0x1 | Standalone Fetch | + +------+------------------------+ + | 0x2 | Relative Joining Fetch | + +------+------------------------+ + | 0x3 | Absolute Joining Fetch | + +------+------------------------+ + + Table 2 + + An endpoint that receives a Fetch Type other than 0x1, 0x2 or 0x3 + MUST close the session with a PROTOCOL_VIOLATION. + +9.16.1. Standalone Fetch + + A Fetch of Objects performed independently of any Subscribe. + + A Standalone Fetch includes this structure: + + + + + + + +Nandakumar, et al. Expires 17 July 2026 [Page 64] + +Internet-Draft moq-transport January 2026 + + + Standalone Fetch { + Track Namespace (..), + Track Name Length (i), + Track Name (..), + Start Location (Location), + End Location (Location) + } + + * Track Namespace: Identifies the namespace of the track as defined + in (Section 2.4.1). + + * Track Name: Identifies the track name as defined in + (Section 2.4.1). + + * Start Location: The start Location. + + * End Location: The end Location, plus 1. A Location.Object value + of 0 means the entire group is requested. + +9.16.2. Joining Fetches + + A Joining Fetch is associated with a Subscribe request by specifying + the Request ID of a subscription in the Established or Pending + (subscriber) state. A publisher receiving a Joining Fetch uses + properties of the associated Subscribe to determine the Track + Namespace, Track Name and End Location such that it is contiguous + with the associated Subscribe. The subscriber can set the Start + Location to an absolute Location or a Location relative to the + current group. + + A Subscriber can use a Joining Fetch to, for example, fill a playback + buffer with a certain number of groups prior to the live edge of a + track. + + A Joining Fetch is only permitted when the associated Subscribe has + the Filter Type Largest Object; any other value results in closing + the session with a PROTOCOL_VIOLATION. + + If no Objects have been published for the track, and the SUBSCRIBE_OK + did not include a LARGEST_OBJECT parameter (Section 9.2.2.7), the + publisher MUST respond with a REQUEST_ERROR with error code + INVALID_RANGE. + + A Joining Fetch includes this structure: + + + + + + + +Nandakumar, et al. Expires 17 July 2026 [Page 65] + +Internet-Draft moq-transport January 2026 + + + Joining Fetch { + Joining Request ID (i), + Joining Start (i) + } + + * Joining Request ID: The Request ID of the subscription to be + joined. If a publisher receives a Joining Fetch with a Request ID + that does not correspond to a subscription in the same session in + the Established or Pending (subscriber) states, it MUST return a + REQUEST_ERROR with error code INVALID_JOINING_REQUEST_ID. + + * Joining Start : A relative or absolute value used to determing the + Start Location, described below. + +9.16.2.1. Joining Fetch Range Calculation + + The Largest Location value from the corresponding subscription is + used to calculate the end of a Joining Fetch, so the Objects + retrieved by the FETCH and SUBSCRIBE are contiguous and non- + overlapping. + + The publisher receiving a Joining Fetch sets the End Location to + {Subscribe Largest Location.Object + 1}. Here Subscribe Largest + Location is the saved value from when the subscription started (see + Section 5.1). + + Note: the last Object included in the Joining FETCH response is + Subscribe Largest Location. The + 1 above indicates the equivalent + Standalone Fetch encoding. + + For a Relative Joining Fetch, the publisher sets the Start Location + to {Subscribe Largest Location.Group - Joining Start, 0}. + + For an Absolute Joining Fetch, the publisher sets the Start Location + to {Joining Start, 0}. + +9.16.3. Fetch Handling + + The format of FETCH is as follows: + + + + + + + + + + + + +Nandakumar, et al. Expires 17 July 2026 [Page 66] + +Internet-Draft moq-transport January 2026 + + + FETCH Message { + Type (i) = 0x16, + Length (16), + Request ID (i), + Fetch Type (i), + [Standalone (Standalone Fetch),] + [Joining (Joining Fetch),] + Number of Parameters (i), + Parameters (..) ... + } + + Figure 18: MOQT FETCH Message + + * Request ID: See Section 9.1. + + * Fetch Type: Identifies the type of Fetch, whether Standalone, + Relative Joining or Absolute Joining. + + * Standalone: Standalone Fetch structure included when Fetch Type is + 0x1 + + * Joining: Joining Fetch structure included when Fetch Type is 0x2 + or 0x3. + + * Parameters: The parameters are defined in Section 9.2.2. + + A publisher responds to a FETCH request with either a FETCH_OK or a + REQUEST_ERROR message. The publisher creates a new unidirectional + stream that is used to send the Objects. The FETCH_OK or + REQUEST_ERROR can come at any time relative to object delivery. + + The publisher responding to a FETCH is responsible for delivering all + available Objects in the requested range in the requested order (see + Section 9.2.2.4). The Objects in the response are delivered on a + single unidirectional stream. Any gaps in the Group and Object IDs + in the response stream indicate objects that do not exist. For + Ascending Group Order this includes ranges between the first + requested object and the first object in the stream; between objects + in the stream; and between the last object in the stream and the + Largest Group/Object indicated in FETCH_OK, so long as the fetch + stream is terminated by a FIN. If no Objects exist in the requested + range, the publisher opens the unidirectional stream, sends the + FETCH_HEADER (see Section 10.4.4) and closes the stream with a FIN. + + A relay that has cached objects from the beginning of the range MAY + start sending objects immediately in response to a FETCH. If it + encounters an object in the requested range that is not cached and + has unknown status, the relay MUST pause subsequent delivery until it + + + +Nandakumar, et al. Expires 17 July 2026 [Page 67] + +Internet-Draft moq-transport January 2026 + + + has confirmed the object's status upstream. If the upstream FETCH + fails, the relay sends a REQUEST_ERROR and can reset the + unidirectional stream. It can choose to do so immediately or wait + until the cached objects have been delivered before resetting the + stream. + + The Object Forwarding Preference does not apply to fetches. + + Fetch specifies an inclusive range of Objects starting at Start + Location and ending at End Location. End Location MUST specify the + same or a larger Location than Start Location for Standalone and + Absolute Joining Fetches. + + Objects that are not yet published will not be retrieved by a FETCH. + The Largest available Object in the requested range is indicated in + the FETCH_OK, and is the last Object a fetch will return if the End + Location have not yet been published. + + If Start Location is greater than the Largest Object (Section 9.9) + the publisher MUST return REQUEST_ERROR with error code + INVALID_RANGE. + + A publisher MUST send fetched groups in the requested group order, + either ascending or descending. Within each group, objects are sent + in Object ID order; subgroup ID is not used for ordering. + + If a Publisher receives a FETCH with a range that includes one or + more Objects with unknown status (e.g. a Relay has temporarily lost + contact with the Original Publisher and does not have the Object in + cache), it can choose to reset the FETCH data stream with + UNKNOWN_OBJECT_STATUS, or indicate the range of unknown Objects and + continue serving other known Objects. + +9.17. FETCH_OK + + A publisher sends a FETCH_OK control message in response to + successful fetches. A publisher MAY send Objects in response to a + FETCH before the FETCH_OK message is sent, but the FETCH_OK MUST NOT + be sent until the End Location is known. + + + + + + + + + + + + +Nandakumar, et al. Expires 17 July 2026 [Page 68] + +Internet-Draft moq-transport January 2026 + + + FETCH_OK Message { + Type (i) = 0x18, + Length (16), + Request ID (i), + End Of Track (8), + End Location (Location), + Number of Parameters (i), + Parameters (..) ... + Track Extensions (..), + } + + Figure 19: MOQT FETCH_OK Message + + * Request ID: The Request ID of the FETCH this message is replying + to Section 9.9. + + * End Of Track: 1 if all Objects have been published on this Track, + and the End Location is the final Object in the Track, 0 if not. + + * End Location: The largest object covered by the FETCH response. + The End Location is determined as follows: + + - If the requested FETCH End Location was beyond the Largest + known (possibly final) Object, End Location is {Largest.Group, + Largest.Object + 1} + + - If End Location.Object in the FETCH request was 0 and the + response covers the last Object in the Group, End Location is + {Fetch.End Location.Group, 0} + + - Otherwise, End Location is Fetch.End Location Where Fetch.End + Location is either Fetch.Standalone.End Location or the + computed End Location described in Section 9.16.2.1. + + If the relay is subscribed to the track, it uses its knowledge of + the largest {Group, Object} to set End Location. If it is not + subscribed and the requested End Location exceeds its cached data, + the relay makes an upstream request to complete the FETCH, and + uses the upstream response to set End Location. + + If End Location is smaller than the Start Location in the + corresponding FETCH the receiver MUST close the session with a + PROTOCOL_VIOLATION. + + * Parameters: The parameters are defined in Section 9.2.2. + + * Track Extensions : A sequence of Extension Headers. See + Section 2.5. + + + +Nandakumar, et al. Expires 17 July 2026 [Page 69] + +Internet-Draft moq-transport January 2026 + + +9.18. FETCH_CANCEL + + A subscriber sends a FETCH_CANCEL message to a publisher to indicate + it is no longer interested in receiving objects for the fetch + identified by the 'Request ID'. The publisher SHOULD promptly close + the unidirectional stream, even if it is in the middle of delivering + an object. + + The format of FETCH_CANCEL is as follows: + + FETCH_CANCEL Message { + Type (i) = 0x17, + Length (16), + Request ID (i) + } + + Figure 20: MOQT FETCH_CANCEL Message + + * Request ID: The Request ID of the FETCH (Section 9.16) this + message is cancelling. + +9.19. TRACK_STATUS + + A potential subscriber sends a TRACK_STATUS message on the control + stream to obtain information about the current status of a given + track. + + The TRACK_STATUS message format is identical to the SUBSCRIBE message + (Section 9.9), but subscriber parameters related to Track delivery + (e.g. SUBSCRIBER_PRIORITY) are not included. + + The receiver of a TRACK_STATUS message treats it identically as if it + had received a SUBSCRIBE message, except it does not create + downstream subscription state or send any Objects. If successful, + the publisher responds with a REQUEST_OK message with the same + parameters it would have set in a SUBSCRIBE_OK. Track Alias is not + used. A publisher responds to a failed TRACK_STATUS with an + appropriate REQUEST_ERROR message. + + Relays without an Established subscription MAY forward TRACK_STATUS + to one or more publishers, or MAY initiate a subscription (subject to + authorization) as described in Section 8.5 to determine the response. + The publisher does not send PUBLISH_DONE for this request, and the + subscriber cannot send REQUEST_UPDATE or UNSUBSCRIBE. + + + + + + + +Nandakumar, et al. Expires 17 July 2026 [Page 70] + +Internet-Draft moq-transport January 2026 + + +9.20. PUBLISH_NAMESPACE + + The publisher sends the PUBLISH_NAMESPACE control message to + advertise that it has tracks available within a Track Namespace. The + receiver verifies the publisher is authorized to publish tracks under + this namespace. + + PUBLISH_NAMESPACE Message { + Type (i) = 0x6, + Length (16), + Request ID (i), + Track Namespace (..), + Number of Parameters (i), + Parameters (..) ... + } + + Figure 21: MOQT PUBLISH_NAMESPACE Message + + * Request ID: See Section 9.1. + + * Track Namespace: Identifies a track's namespace as defined in + Section 2.4.1. + + * Parameters: The parameters are defined in Section 9.2.2. + +9.21. NAMESPACE + + The NAMESPACE message is similar to the PUBLISH_NAMESPACE message, + except it is sent on the response stream of a SUBSCRIBE_NAMESPACE + request. All NAMESPACE messages are in response to a + SUBSCRIBE_NAMESPACE, so only the namespace tuples after the 'Track + Namespace Prefix' are included in the 'Track Namespace Suffix'. + + NAMESPACE Message { + Type (i) = 0x8, + Length (16), + Track Namespace Suffix (..), + } + + Figure 22: MOQT NAMESPACE Message + + * Track Namespace Suffix: Specifies the final portion of a track's + namespace as defined in Section 2.4.1 after removing namespace + tuples included in 'Track Namespace Prefix' {message-subscribe- + ns}. + + + + + + +Nandakumar, et al. Expires 17 July 2026 [Page 71] + +Internet-Draft moq-transport January 2026 + + +9.22. PUBLISH_NAMESPACE_DONE + + The publisher sends the PUBLISH_NAMESPACE_DONE control message to + indicate its intent to stop serving new subscriptions for tracks + within the provided Track Namespace. + + PUBLISH_NAMESPACE_DONE Message { + Type (i) = 0x9, + Length (16), + Request ID (i) + } + + Figure 23: MOQT PUBLISH_NAMESPACE_DONE Message + + * Request ID: The Request ID of the PUBLISH_NAMESPACE that is being + terminated. See Section 9.9. + +9.23. NAMESPACE_DONE + + The publisher sends the NAMESPACE_DONE control message to indicate + its intent to stop serving new subscriptions for tracks within the + provided Track Namespace. All NAMESPACE_DONE messages are in + response to a SUBSCRIBE_NAMESPACE, so only the namespace tuples after + the 'Track Namespace Prefix' are included in the 'Track Namespace + Suffix'. + + NAMESPACE_DONE Message { + Type (i) = 0xE, + Length (16), + Track Namespace Suffix (..) + } + + Figure 24: MOQT NAMESPACE_DONE Message + + * Track Namespace Suffix: Specifies the final portion of a track's + namespace as defined in Section 2.4.1. The namespace begins with + the 'Track Namespace Prefix' specified in {message-subscribe-ns}. + +9.24. PUBLISH_NAMESPACE_CANCEL + + The subscriber sends an PUBLISH_NAMESPACE_CANCEL control message to + indicate it will stop sending new subscriptions for tracks within the + provided Track Namespace. + + + + + + + + +Nandakumar, et al. Expires 17 July 2026 [Page 72] + +Internet-Draft moq-transport January 2026 + + + PUBLISH_NAMESPACE_CANCEL Message { + Type (i) = 0xC, + Length (16), + Request ID (i), + Error Code (i), + Error Reason (Reason Phrase) + } + + Figure 25: MOQT PUBLISH_NAMESPACE_CANCEL Message + + * Request ID: The Request ID of the PUBLISH_NAMESPACE that is being + terminated. See Section 9.9. + + * Error Code: Identifies an integer error code for canceling the + publish. PUBLISH_NAMESPACE_CANCEL uses the same error codes as + REQUEST_ERROR (Section 9.8) that responds to PUBLISH_NAMESPACE. + + * Error Reason: Provides the reason for publish cancelation. See + Section 1.4.3. + +9.25. SUBSCRIBE_NAMESPACE + + The subscriber sends a SUBSCRIBE_NAMESPACE control message on a new + bidirectional stream to a publisher to request the current set of + matching published namespaces and/or Established subscriptions, as + well as future updates to the set. + + SUBSCRIBE_NAMESPACE Message { + Type (i) = 0x11, + Length (16), + Request ID (i), + Track Namespace Prefix (..), + Subscribe Options (i), + Number of Parameters (i), + Parameters (..) ... + } + + Figure 26: MOQT SUBSCRIBE_NAMESPACE Message + + * Request ID: See Section 9.1. + + * Track Namespace Prefix: A Track Namespace structure as described + in Section 2.4.1 with between 0 and 32 Track Namespace Fields. + This prefix is matched against track namespaces known to the + publisher. For example, if the publisher is a relay that has + received PUBLISH_NAMESPACE messages for namespaces ("example.com", + "meeting=123", "participant=100") and ("example.com", + "meeting=123", "participant=200"), a SUBSCRIBE_NAMESPACE for + + + +Nandakumar, et al. Expires 17 July 2026 [Page 73] + +Internet-Draft moq-transport January 2026 + + + ("example.com", "meeting=123") would match both. If an endpoint + receives a Track Namespace Prefix consisting of greater than than + 32 Track Namespace Fields, it MUST close the session with a + PROTOCOL_VIOLATION. + + * Subscribe Options: Allows subscribers to request PUBLISH (0x00), + NAMESPACE (0x01), or both (0x02) for a given SUBSCRIBE_NAMESPACE + request. + + * Parameters: The parameters are defined in Section 9.2.2. + + The publisher will respond with REQUEST_OK or REQUEST_ERROR on the + response half of the stream. If the SUBSCRIBE_NAMESPACE is + successful, the publisher will send matching NAMESPACE messages on + the response stream if they are requested. If it is an error, the + stream will be immediately closed via FIN. Also, any matching + PUBLISH messages without an Established Subscription will be sent on + the control stream. When there are changes to the namespaces or + subscriptions being published and the subscriber is subscribed to + them, the publisher sends the corresponding NAMESPACE, + NAMESPACE_DONE, or PUBLISH messages. + + A subscriber cannot make overlapping namespace subscriptions on a + single session. Within a session, if a publisher receives a + SUBSCRIBE_NAMESPACE with a Track Namespace Prefix that shares a + common prefix with an established namespace subscription, it MUST + respond with REQUEST_ERROR with error code PREFIX_OVERLAP. + + The publisher MUST ensure the subscriber is authorized to perform + this namespace subscription. + + SUBSCRIBE_NAMESPACE is not required for a publisher to send + PUBLISH_NAMESPACE, PUBLISH_NAMESPACE_DONE or PUBLISH messages to a + subscriber. It is useful in applications or relays where subscribers + are only interested in or authorized to access a subset of available + namespaces and tracks. + + If the FORWARD parameter (Section 9.2.2.8) is present in this message + and equal to 0, PUBLISH messages resulting from this + SUBSCRIBE_NAMESPACE will set the FORWARD parameter to 0. If the + FORWARD parameter is equal to 1 or omitted from this message, PUBLISH + messages resulting from this SUBSCRIBE_NAMESPACE will set the FORWARD + parameter to 1, or indicate that value by omitting the parameter (see + Section 5.1). + + + + + + + +Nandakumar, et al. Expires 17 July 2026 [Page 74] + +Internet-Draft moq-transport January 2026 + + + The publisher MUST NOT send NAMESPACE_DONE for a namespace suffix + before the corresponding NAMESPACE. If a subscriber receives a + NAMESPACE_DONE before the corresponding NAMESPACE, it MUST close the + session with a 'PROTOCOL_VIOLATION'. + +10. Data Streams and Datagrams + + A publisher sends Objects matching a subscription on Data Streams or + Datagrams and sends Objects matching a FETCH request on one Data + Stream. + + All unidirectional MOQT streams start with a variable-length integer + indicating the type of the stream in question. + + +===========+==================================+ + | ID | Type | + +===========+==================================+ + | 0x10-0x1D | SUBGROUP_HEADER (Section 10.4.2) | + +-----------+----------------------------------+ + | 0x05 | FETCH_HEADER (Section 10.4.4) | + +-----------+----------------------------------+ + + Table 3 + + All MOQT datagrams start with a variable-length integer indicating + the type of the datagram. See Section 10.3.1. + + An endpoint that receives an unknown stream or datagram type MUST + close the session. + + Every Object has a 'Object Forwarding Preference' and the Original + Publisher MAY use both Subgroups and Datagrams within a Group or + Track. + +10.1. Track Alias + + To optimize wire efficiency, Subgroups and Datagrams refer to a track + by a numeric identifier, rather than the Full Track Name. Track + Alias is chosen by the publisher and included in SUBSCRIBE_OK + (Section 9.10) or PUBLISH (Section 9.13). + + Objects can arrive after a subscription has been cancelled. + Subscribers SHOULD retain sufficient state to quickly discard these + unwanted Objects, rather than treating them as belonging to an + unknown Track Alias. + + + + + + +Nandakumar, et al. Expires 17 July 2026 [Page 75] + +Internet-Draft moq-transport January 2026 + + +10.2. Objects + + An Object contains a range of contiguous bytes from the specified + track, as well as associated metadata required to deliver, cache, and + forward it. Objects are sent by publishers. + +10.2.1. Canonical Object Properties + + A canonical MoQ Object has the following information: + + * Track Namespace and Track Name: The track this object belongs to. + + * Group ID: The identifier of the Object's Group (see Section 2.3) + within the Track. + + * Object ID: The order of the object within the group. + + * Publisher Priority: An 8 bit integer indicating the publisher's + priority for the Object (Section 7). + + * Object Forwarding Preference: An enumeration indicating how a + publisher sends an object. The preferences are Subgroup and + Datagram. Object Forwarding Preference is a property of an + individual Object and can vary among Objects in the same Track. + In a subscription, an Object MUST be sent according to its Object + Forwarding Preference. + + * Subgroup ID: The identifier of the Object's Subgroup (see + Section 2.2) within the Group. This field is omitted if the + Object Forwarding Preference is Datagram. + + * Object Status: An enumeration used to indicate whether the Object + is a normal Object or mark the end of a group or track. See + Section 10.2.1.1 below. + + * Object Extensions : A sequence of Extensions associated with the + object. See Section 10.2.1.2. + + * Object Payload: An opaque payload intended for an End Subscriber + and SHOULD NOT be processed by a relay. Only present when 'Object + Status' is Normal (0x0). + +10.2.1.1. Object Status + + The Object Status is a field that is only present in objects that are + delivered via a SUBSCRIPTION, and is absent in Objects delivered via + a FETCH. It allows the publisher to explicitly communicate that a + specific range of objects does not exist. + + + +Nandakumar, et al. Expires 17 July 2026 [Page 76] + +Internet-Draft moq-transport January 2026 + + + Status can have following values: + + * 0x0 := Normal object. This status is implicit for any non-zero + length object. Zero-length objects explicitly encode the Normal + status. + + * 0x3 := Indicates End of Group. Indicates that no objects with the + specified Group ID and the Object ID that is greater than or equal + to the one specified exist in the group identified by the Group + ID. + + * 0x4 := Indicates End of Track. Indicates that no objects with the + location that is equal to or greater than the one specified exist. + + All of those SHOULD be cached. + + Any other value SHOULD be treated as a protocol error and the session + SHOULD be closed with a PROTOCOL_VIOLATION (Section 3.4). Any object + with a status code other than zero MUST have an empty payload. + +10.2.1.2. Object Extension Headers + + Any Object with status Normal can have extension headers + (Section 2.5). If an endpoint receives extension headers on Objects + with status that is not Normal, it MUST close the session with a + PROTOCOL_VIOLATION. + + Object Extension Headers are visible to relays and are intended to be + relevant to MOQT Object distribution. Any Object metadata never + intended to be accessed by the transport or Relays SHOULD be + serialized as part of the Object payload and not as an extension + header. + + Object Extension Headers are serialized as a length in bytes followed + by Key-Value-Pairs (see Figure 2). + + Extensions { + Extension Headers Length (i), + Extension Headers (..), + } + + Object Extension Header types are registered in the IANA table 'MOQ + Extension Headers'. See Section 13. + + + + + + + + +Nandakumar, et al. Expires 17 July 2026 [Page 77] + +Internet-Draft moq-transport January 2026 + + +10.3. Datagrams + + A single object can be conveyed in a datagram. The Track Alias field + (Section 10.1) indicates the track this Datagram belongs to. If an + endpoint receives a datagram with an unknown Track Alias, it MAY drop + the datagram or choose to buffer it for a brief period to handle + reordering with the control message that establishes the Track Alias. + + An Object received in an OBJECT_DATAGRAM message has an Object + Forwarding Preference = Datagram. + + To send an Object with Object Forwarding Preference = Datagram, + determine the length of the header and payload and send the Object as + datagram. When the total size is larger than the maximum datagram + size for the session, the Object will be dropped without any explicit + notification. + + Each session along the path between the Original Publisher and End + Subscriber might have different maximum datagram sizes. + Additionally, Object Extension Headers (Section 10.2.1.2) can be + added to Objects as they pass through the MOQT network, increasing + the size of the Object and the chances it will exceed the maximum + datagram size of a downstream session and be dropped. + +10.3.1. Object Datagram + + An OBJECT_DATAGRAM carries a single object in a datagram. + + OBJECT_DATAGRAM { + Type (i) = 0x00..0x0F / 0x20..0x21 / 0x24..0x25 / + 0x28..0x29 / 0x2C..0x2D, + Track Alias (i), + Group ID (i), + [Object ID (i),] + [Publisher Priority (8),] + [Extensions (..),] + [Object Status (i),] + [Object Payload (..),] + } + + Figure 27: MOQT OBJECT_DATAGRAM + + The Type field in the OBJECT_DATAGRAM takes the form 0b00X0XXXX (or + the set of values from 0x00 to 0x0F, 0x20 to 0x2F). However, not all + Type values in this range are valid. The four low-order bits and bit + 5 of the Type field determine which fields are present in the + datagram: + + + + +Nandakumar, et al. Expires 17 July 2026 [Page 78] + +Internet-Draft moq-transport January 2026 + + + * The *EXTENSIONS* bit (0x01) indicates when the Extensions field is + present. When set to 1, the Extensions structure defined in + Section 10.2.1.2 is present. When set to 0, the Extensions field + is absent. If an endpoint receives a datagram with the EXTENSIONS + bit set and an Extension Headers Length of 0, it MUST close the + session with a PROTOCOL_VIOLATION. + + * The *END_OF_GROUP* bit (0x02) indicates End of Group. When set to + 1, this indicates that no Object with the same Group ID and an + Object ID greater than the Object ID in this datagram exists. + + * The *ZERO_OBJECT_ID* bit (0x04) indicates when the Object ID field + is present. When set to 1, the Object ID field is omitted and the + Object ID is + + 1. When set to 0, the Object ID field is present. + + * The *DEFAULT_PRIORITY* bit (0x08) indicates when the Priority + field is present. When set to 1, the Priority field is omitted + and this Object inherits the Publisher Priority specified in the + control message that established the subscription. When set to 0, + the Priority field is present. + + * The *STATUS* bit (0x20) indicates whether the datagram contains an + Object Status or Object Payload. When set to 1, the Object Status + field is present and there is no Object Payload. When set to 0, + the Object Payload is present and the Object Status field is + omitted. There is no explicit length field for the Object + Payload; the entirety of the transport datagram following the + Object header fields contains the payload. + + The following Type values are invalid. If an endpoint receives a + datagram with any of these Type values, it MUST close the session + with a PROTOCOL_VIOLATION: + + * Type values with both the STATUS bit (0x20) and END_OF_GROUP bit + (0x02) set: 0x22, 0x23, 0x26, 0x27, 0x2A, 0x2B, 0x2E, 0x2F. An + object status message cannot signal end of group. + + * Type values that do not match the form 0b00X0XXXX (i.e., Type + values outside the ranges 0x00..0x0F and 0x20..0x2F). + + + + + + + + + + +Nandakumar, et al. Expires 17 July 2026 [Page 79] + +Internet-Draft moq-transport January 2026 + + +10.4. Streams + + When Objects are sent on streams, the stream begins with a Subgroup + or Fetch Header and is followed by one or more sets of serialized + Object fields. If a stream ends gracefully (i.e., the stream + terminates with a FIN) in the middle of a serialized Object, the + session SHOULD be closed with a PROTOCOL_VIOLATION. + + A publisher SHOULD NOT open more than one stream at a time with the + same Subgroup Header field values. + +10.4.1. Stream Cancellation + + Streams aside from the control stream MAY be canceled due to + congestion or other reasons by either the publisher or subscriber. + Early termination of a stream does not affect the MoQ application + state, and therefore has no effect on outstanding subscriptions. + +10.4.2. Subgroup Header + + All Objects on a Subgroup stream belong to the track identified by + Track Alias (see Section 10.1) and the Subgroup indicated by 'Group + ID' and Subgroup ID indicated by the SUBGROUP_HEADER. + + If an endpoint receives a subgroup with an unknown Track Alias, it + MAY abandon the stream, or choose to buffer it for a brief period to + handle reordering with the control message that establishes the Track + Alias. The endpoint MAY withhold stream flow control beyond the + SUBGROUP_HEADER until the Track Alias has been established. To + prevent deadlocks, the publisher MUST allocate connection flow + control to the control stream before allocating it any data streams. + Otherwise, a receiver might wait for a control message containing a + Track Alias to release flow control, while the sender waits for flow + control to send the message. + + SUBGROUP_HEADER { + Type (i) = 0x10..0x15 / 0x18..0x1D / 0x30..0x35 / 0x38..0x3D, + Track Alias (i), + Group ID (i), + [Subgroup ID (i),] + [Publisher Priority (8),] + } + + Figure 28: MOQT SUBGROUP_HEADER + + All Objects received on a stream opened with SUBGROUP_HEADER have an + Object Forwarding Preference = Subgroup. + + + + +Nandakumar, et al. Expires 17 July 2026 [Page 80] + +Internet-Draft moq-transport January 2026 + + + The Type field in the SUBGROUP_HEADER takes the form 0b00X1XXXX (or + the set of values from 0x10 to 0x1F, 0x30 to 0x3F), where bit 4 is + always set to 1. However, not all Type values in this range are + valid. The four low-order bits and bit 5 determine which fields are + present in the header: + + * The *EXTENSIONS* bit (0x01) indicates when the Extensions field is + present in all Objects in this Subgroup. When set to 1, the + Extensions structure defined in Section 10.2.1.2 is present in all + Objects. When set to 0, the Extensions field is never present. + Objects with no extensions set Extension Headers Length to 0. + + * The *SUBGROUP_ID_MODE* field (bits 1-2, mask 0x06) is a two-bit + field that determines the encoding of the Subgroup ID. To extract + this value, perform a bitwise AND with mask 0x06 and right-shift + by 1 bit: + + - 0b00: The Subgroup ID field is absent and the Subgroup ID is 0. + + - 0b01: The Subgroup ID field is absent and the Subgroup ID is + the Object ID of the first Object transmitted in this Subgroup. + + - 0b10: The Subgroup ID field is present in the header. + + - 0b11: Reserved for future use. + + * The *END_OF_GROUP* bit (0x08) indicates that this subgroup + contains the largest Object in the Group. When set to 1, the + subscriber can infer the final Object in the Group when the data + stream is terminated by a FIN. In this case, Objects that have + the same Group ID and an Object ID larger than the last Object + received on the stream do not exist. This does not apply when the + data stream is terminated with a RESET_STREAM or RESET_STREAM_AT. + + * The *DEFAULT_PRIORITY* bit (0x20) indicates when the Priority + field is present. When set to 1, the Priority field is omitted + and this Subgroup inherits the Publisher Priority specified in the + control message that established the subscription. When set to 0, + the Priority field is present in the Subgroup header. + + The following Type values are invalid. If an endpoint receives a + stream header with any of these Type values, it MUST close the + session with a PROTOCOL_VIOLATION: + + * Type values with SUBGROUP_ID_MODE set to 0b11: 0x16, 0x17, 0x1E, + 0x1F, 0x36, 0x37, 0x3E, 0x3F. This mode is reserved for future + use. + + + + +Nandakumar, et al. Expires 17 July 2026 [Page 81] + +Internet-Draft moq-transport January 2026 + + + * Type values that do not match the form 0b00X1XXXX (i.e., Type + values outside the ranges 0x10..0x1F and 0x30..0x3F, or values + where bit 4 is not set). + + To send an Object with Object Forwarding Preference = Subgroup, find + the open stream that is associated with the subscription, Group ID + and Subgroup ID, or open a new one and send the SUBGROUP_HEADER. + Then serialize the following fields. + + The Object Status field is only sent if the Object Payload Length is + zero. + + The Object ID Delta + 1 is added to the previous Object ID in the + Subgroup stream if there was one. The Object ID is the Object ID + Delta if it's the first Object in the Subgroup stream. For example, + a Subgroup of sequential Object IDs starting at 0 will have 0 for all + Object ID Delta values. A consumer cannot infer information about + the existence of Objects between the current and previous Object ID + in the Subgroup (e.g. when Object ID Delta is non-zero) unless there + is an Prior Object ID Gap extesnion header (see Section 11.4). + + { + Object ID Delta (i), + [Extensions (..),] + Object Payload Length (i), + [Object Status (i),] + [Object Payload (..),] + } + + Figure 29: MOQT Subgroup Object Fields + +10.4.3. Closing Subgroup Streams + + Subscribers will often need to know if they have received all objects + in a Subgroup, particularly if they serve as a relay or cache. QUIC + and Webtransport streams provide signals that can be used for this + purpose. Closing Subgroups promptly frees system resources and often + unlocks flow control credit to open more streams. + + If a sender has delivered all objects in a Subgroup to the QUIC + stream, except any Objects with Locations smaller than the + subscription's Start Location, it MUST close the stream with a FIN. + + If a sender closes the stream before delivering all such objects to + the QUIC stream, it MUST use a RESET_STREAM or RESET_STREAM_AT + [I-D.draft-ietf-quic-reliable-stream-reset] frame. This includes, + but is not limited to: + + + + +Nandakumar, et al. Expires 17 July 2026 [Page 82] + +Internet-Draft moq-transport January 2026 + + + * An Object in an open Subgroup exceeding its Delivery Timeout + + * Early termination of subscription due to an UNSUBSCRIBE message + + * A publisher's decision to end the subscription early + + * A REQUEST_UPDATE moving the subscription's End Group to a smaller + Group or the Start Location to a larger Location + + * Omitting a Subgroup Object due to the subcriber's Forward State + + When RESET_STREAM_AT is used, the reliable_size SHOULD include the + stream header so the receiver can identify the corresponding + subscription and accurately account for reset data streams when + handling PUBLISH_DONE (see Section 9.15). Publishers that reset data + streams without using RESET_STREAM_AT with an appropriate + reliable_size can cause subscribers to hold on to subscription state + until a timeout expires. + + A sender might send all objects in a Subgroup and the FIN on a QUIC + stream, and then reset the stream. In this case, the receiving + application would receive the FIN if and only if all objects were + received. If the application receives all data on the stream and the + FIN, it can ignore any RESET_STREAM it receives. + + If a sender will not deliver any objects from a Subgroup, it MAY send + a SUBGROUP_HEADER on a new stream, with no objects, and then send + RESET_STREAM_AT with a reliable_size equal to the length of the + stream header. This explicitly tells the receiver there is an unsent + Subgroup. + + A relay MUST NOT forward an Object on an existing Subgroup stream + unless it is the next Object in that Subgroup. A relay determines + that an Object is the next Object in the Subgroup if at least one of + the following is true: + + * The Object ID is one greater than the previous Object sent on this + Subgroup stream. + + * The Object was received on the same upstream Subgroup stream as + the previously sent Object on the downstream Subgroup stream, with + no other Objects in between. + + * It determined all Object IDs between the current and previous + Object IDs on the Subgroup stream belong to different Subgroups or + do not exist. + + + + + +Nandakumar, et al. Expires 17 July 2026 [Page 83] + +Internet-Draft moq-transport January 2026 + + + If the relay does not know if an Object is the next Object, it MUST + reset the Subgroup stream and open a new one to forward it. + + Since SUBSCRIBEs always end on a group boundary, an ending + subscription can always cleanly close all its subgroups. A sender + that terminates a stream early for any other reason (e.g., to handoff + to a different sender) MUST use RESET_STREAM or RESET_STREAM_AT. + Senders SHOULD terminate a stream on Group boundaries to avoid doing + so. + + An MOQT implementation that processes a stream FIN is assured it has + received all objects in a subgroup from the start of the + subscription. If a relay, it can forward stream FINs to its own + subscribers once those objects have been sent. A relay MAY treat + receipt of EndOfGroup or EndOfTrack objects as a signal to close + corresponding streams even if the FIN has not arrived, as further + objects on the stream would be a protocol violation. + + Similarly, an EndOfGroup message indicates the maximum Object ID in + the Group, so if all Objects in the Group have been received, a FIN + can be sent on any stream where the entire subgroup has been sent. + This might be complex to implement. + + Processing a RESET_STREAM or RESET_STREAM_AT means that there might + be other objects in the Subgroup beyond the last one received. A + relay might immediately reset the corresponding downstream stream, or + it might attempt to recover the missing Objects in an effort to send + all the Objects in the subgroups and the FIN. It also might send + RESET_STREAM_AT with reliable_size set to the last Object it has, so + as to reliably deliver the Objects it has while signaling that other + Objects might exist. + + A subscriber MAY send a QUIC STOP_SENDING frame for a subgroup stream + if the Group or Subgroup is no longer of interest to it. The + publisher SHOULD respond with RESET_STREAM or RESET_STREAM_AT. If + RESET_STREAM_AT is sent, note that the receiver has indicated no + interest in the objects, so setting a reliable_size beyond the stream + header is of questionable utility. + + RESET_STREAM and STOP_SENDING on SUBSCRIBE data streams have no + impact on other Subgroups in the Group or the subscription, although + applications might cancel all Subgroups in a Group at once. + + A publisher that receives a STOP_SENDING on a Subgroup stream SHOULD + NOT attempt to open a new stream to deliver additional Objects in + that Subgroup. + + + + + +Nandakumar, et al. Expires 17 July 2026 [Page 84] + +Internet-Draft moq-transport January 2026 + + + The application SHOULD use a relevant error code in RESET_STREAM or + RESET_STREAM_AT, as defined below: + + INTERNAL_ERROR (0x0): An implementation specific error. + + CANCELLED (0x1): The subscriber requested cancellation via + UNSUBSCRIBE, FETCH_CANCEL or STOP_SENDING, or the publisher ended + the subscription, in which case PUBLISH_DONE (Section 9.15) will + have a more detailed status code. + + DELIVERY_TIMEOUT (0x2): The DELIVERY TIMEOUT Section 9.2.2.2 was + exceeded for this stream. + + SESSION_CLOSED (0x3): The publisher session is being closed. + + UNKNOWN_OBJECT_STATUS (0x4): In response to a FETCH, the publisher + is unable to determine the Status of the next Object in the + requested range. + + MALFORMED_TRACK (0x12): A relay publisher detected that the track + was malformed (see Section 2.4.2). + +10.4.4. Fetch Header + + When a stream begins with FETCH_HEADER, all objects on the stream + belong to the track requested in the Fetch message identified by + Request ID. + + FETCH_HEADER { + Type (i) = 0x5, + Request ID (i), + } + + Figure 30: MOQT FETCH_HEADER + + Each Object sent on a FETCH stream after the FETCH_HEADER has the + following format: + + { + Serialization Flags (i), + [Group ID (i),] + [Subgroup ID (i),] + [Object ID (i),] + [Publisher Priority (8),] + [Extensions (..),] + Object Payload Length (i), + [Object Payload (..),] + } + + + +Nandakumar, et al. Expires 17 July 2026 [Page 85] + +Internet-Draft moq-transport January 2026 + + + Figure 31: MOQT Fetch Object Fields + + The Serialization Flags field defines the serialization of the + Object. It is a variable-length integer. When less than 128, the + bits represent flags described below. The following additional + values are defined: + + +=======+===========================+ + | Value | Meaning | + +=======+===========================+ + | 0x8C | End of Non-Existent Range | + +-------+---------------------------+ + | 0x10C | End of Unknown Range | + +-------+---------------------------+ + + Table 4 + + Any other value is a PROTOCOL_VIOLATION. + +10.4.4.1. Flags + + The two least significant bits (LSBs) of the Serialization Flags form + a two-bit field that defines the encoding of the Subgroup. To + extract this value, the Subscriber performs a bitwise AND operation + with the mask 0x03. + + +==============================+===============================+ + | Bitmask Result | Meaning | + | (Serialization Flags & 0x03) | | + +==============================+===============================+ + | 0x00 | Subgroup ID is zero | + +------------------------------+-------------------------------+ + | 0x01 | Subgroup ID is the prior | + | | Object's Subgroup ID | + +------------------------------+-------------------------------+ + | 0x02 | Subgroup ID is the prior | + | | Object's Subgroup ID plus one | + +------------------------------+-------------------------------+ + | 0x03 | The Subgroup ID field is | + | | present | + +------------------------------+-------------------------------+ + + Table 5 + + The following table defines additional flags within the Serialization + Flags field. Each flag is an independent boolean value, where a set + bit (1) indicates the corresponding condition is true. + + + + +Nandakumar, et al. Expires 17 July 2026 [Page 86] + +Internet-Draft moq-transport January 2026 + + + +=========+============================+============================+ + | Bitmask | Condition if set | Condition if not set (0) | + +=========+============================+============================+ + | 0x04 | Object ID field is | Object ID is the prior | + | | present | Object's ID plus one | + +---------+----------------------------+----------------------------+ + | 0x08 | Group ID field is | Group ID is the prior | + | | present | Object's Group ID | + +---------+----------------------------+----------------------------+ + | 0x10 | Priority field is | Priority is the prior | + | | present | Object's Priority | + +---------+----------------------------+----------------------------+ + | 0x20 | Extensions field is | Extensions field is not | + | | present | present | + +---------+----------------------------+----------------------------+ + | 0x40 | Datagram: ignore the two | Use the subgroup ID in the | + | | least significant bits | two least significant bits | + +---------+----------------------------+----------------------------+ + + Table 6 + + If the first Object in the FETCH response uses a flag that references + fields in the prior Object, the Subscriber MUST close the session + with a PROTOCOL_VIOLATION. + + The Extensions structure is defined in Section 10.2.1.2. + + When encoding an Object with a Forwarding Preference of "Datagram" + (see Section 10.2.1), the object has no Subgroup ID. The publisher + MUST SET bit 0x40 to '1'. When 0x40 is set, it SHOULD set the two + least significant bits to zero and the subscriber MUST ignore the + bits. + +10.4.4.2. End of Range + + When Serialization Flags indicates an End of Range (e.g. values 0x8C + or 0x10C), the Group ID and Object ID fields are present. Subgroup + ID, Priority and Extensions are not present. All Objects with + Locations between the last serialized Object, if any, and this + Location, inclusive, either do not exist (when Serialization Flags is + 0x8C) or are unknown (0x10C). A publisher SHOULD NOT use End of Non- + Existent Range in a FETCH response except to split a range of Objects + that will not be serialized into those that are known not to exist + and those with unknown status. + + + + + + + +Nandakumar, et al. Expires 17 July 2026 [Page 87] + +Internet-Draft moq-transport January 2026 + + +10.5. Examples + + Sending a subgroup on one stream: + + Stream = 2 + + SUBGROUP_HEADER { + Type = 0x14 + Track Alias = 2 + Group ID = 0 + Subgroup ID = 0 + Priority = 0 + } + { + Object ID = 0 + Object Payload Length = 4 + Payload = "abcd" + } + { + Object ID = 1 + Object Payload Length = 4 + Payload = "efgh" + } + + Sending a group on one stream, with the first object containing two + Extension Headers. + + + + + + + + + + + + + + + + + + + + + + + + + +Nandakumar, et al. Expires 17 July 2026 [Page 88] + +Internet-Draft moq-transport January 2026 + + + Stream = 2 + + SUBGROUP_HEADER { + Type = 0x35 + Track Alias = 2 + Group ID = 0 + Subgroup ID = 0 + } + { + Object ID Delta = 0 (Object ID is 0) + Extension Headers Length = 33 + { Type = 4 + Value = 2186796243 + }, + { Type = 77 + Length = 21 + Value = "traceID:123456" + } + Object Payload Length = 4 + Payload = "abcd" + } + { + Object ID Delta = 0 (Object ID is 1) + Extension Headers Length = 0 + Object Payload Length = 4 + Payload = "efgh" + } + +11. Extension Headers + + The following Extension Headers are defined in MOQT. Each Extension + Header specifies whether it can be used with Tracks, Objects, or + both. + +11.1. DELIVERY TIMEOUT + + The DELIVERY TIMEOUT extension (Extension Header Type 0x02) is a + Track Extension. It expresses the publisher's DELIVERY_TIMEOUT for a + Track (see Section 9.2.2.2). + + DELIVERY_TIMEOUT, if present, MUST contain a value greater than 0. + If an endpoint receives a DELIVERY_TIMEOUT equal to 0 it MUST close + the session with PROTOCOL_VIOLATION. + + If unspecified, the subscriber's DELIVERY_TIMEOUT is used. If + neither endpoint specified a timeout, Objects do not time out. + + + + + +Nandakumar, et al. Expires 17 July 2026 [Page 89] + +Internet-Draft moq-transport January 2026 + + +11.1.1. MAX CACHE DURATION + + The MAX_CACHE_DURATION extension (Extension Header Type 0x04) is a + Track Extension. + + It is an integer expressing the number of milliseconds an Object can + be served from a cache. If present, the relay MUST NOT start + forwarding any individual Object received through this subscription + or fetch after the specified number of milliseconds has elapsed since + the beginning of the Object was received. This means Objects earlier + in a multi-object stream will expire earlier than Objects later in + the stream. Once Objects have expired from cache, their state + becomes unknown, and a relay that handles a downstream request that + includes those Objects re-requests them. + + If the MAX_CACHE_DURATION extension is not sent by the publisher, the + Objects can be cached until implementation constraints cause them to + be evicted. + +11.1.1.1. DEFAULT PUBLISHER PRIORITY + + The DEFAULT PUBLISHER PRIORITY extension (Extension Header Type 0x0E) + is a Track Extension that specifies the priority of a subscription + relative to other subscriptions in the same session. The value is + from 0 to 255 and lower numbers get higher priority. See Section 7. + Priorities above 255 are invalid. Subgroups and Datagrams for this + subscription inherit this priority, unless they specifically override + it. + + A subscription has Publisher Priorty 128 if this extension is + omitted. + +11.1.1.2. DEFAULT PUBLISHER GROUP ORDER + + The DEFAULT_PUBLISHER_GROUP_ORDER extension (Extension Header Type + 0x22) is a Track Extension. + + It is an enum indicating the publisher's preference for prioritizing + Objects from different groups within the same subscription (see + Section 7). The allowed values are Ascending (0x1) or Descending + (0x2). If an endpoint receives a value outside this range, it MUST + close the session with PROTOCOL_VIOLATION. + + If omitted, the publisher's preference is Ascending (0x1). + + + + + + + +Nandakumar, et al. Expires 17 July 2026 [Page 90] + +Internet-Draft moq-transport January 2026 + + +11.1.1.3. DYNAMIC GROUPS + + The DYNAMIC_GROUPS Extension (Extension Header Type 0x30) is a Track + Extension. The allowed values are 0 or 1. When the value is 1, it + indicates that the subscriber can request the Original Publisher to + start a new Group by including the NEW_GROUP_REQUEST parameter in + PUBLISH_OK or REQUEST_UPDATE for this Track. If an endpoint receives + a value larger than 1, it MUST close the session with + PROTOCOL_VIOLATION. + + If omitted, the value is 0. + +11.2. Immutable Extensions + + The Immutable Extensions (Extension Header Type 0xB) contains a + sequence of Key-Value-Pairs (see Figure 2) which are also Track or + Object Extension Headers. + + Immutable Extensions { + Type (0xB), + Length (i), + Key-Value-Pair (..) ... + } + + This extension can be added by the Original Publisher, but MUST NOT + be added by Relays. This extension MUST NOT be modified or removed. + Relays MUST cache this extension if the Object is cached and MUST + forward this extension if the enclosing Object is forwarded. Relays + MAY decode and view these extensions. + + A Track is considered malformed (see Section 2.4.2) if any of the + following conditions are detected: + + * An Object contains an Immutable Extensions header that contains + another Immutable Extensions key. + + * A Key-Value-Pair cannot be parsed. + + The following figure shows an example Object structure with a + combination of mutable and immutable extensions and end to end + encrypted metadata in the Object payload. + + + + + + + + + + +Nandakumar, et al. Expires 17 July 2026 [Page 91] + +Internet-Draft moq-transport January 2026 + + + Object Header Object Payload +<------------------------------------------------> <-------------------> ++--------+-------+------------+-------+-----------+--------------------+ +| Object | Ext 1 | Immutable | Ext N | [Payload] | Private Extensions | +| Fields | | Extensions | | [Length] | App Payload | ++--------+-------+------------+-------+-----------+--------------------+ + xxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxx + yyyyyyyyyyyyyyyyyyyy +x = e2e Authenticated Data +y = e2e Encrypted Data +EXT 1 and EXT N can be modified or removed by Relays + + An Object MUST NOT contain more than one instance of this extension + header. + +11.3. Prior Group ID Gap + + Prior Group ID Gap only applies to Objects, not Tracks. + + Prior Group ID Gap (Extension Header Type 0x3C) is a variable length + integer containing the number of Groups prior to the current Group + that do not and will never exist. For example, if the Original + Publisher is publishing an Object in Group 7 and knows it will never + publish any Objects in Group 8 or Group 9, it can include Prior Group + ID Gap = 2 in any number of Objects in Group 10, as it sees fit. A + Track is considered malformed (see Section 2.4.2) if any of the + following conditions are detected: + + * An Object contains more than one instance of Prior Group ID Gap. + + * A Group contains more than one Object with different values for + Prior Group ID Gap. + + * An Object has a Prior Group ID Gap larger than the Group ID. + + * An endpoint receives an Object with a Prior Group ID Gap covering + an Object it previously received. + + * An endpoint receives an Object with a Group ID within a previously + communicated gap. + + This extension is optional, as publishers might not know the prior + gap gize, or there may not be a gap. If Prior Group ID Gap is not + present, the receiver cannot infer any information about the + existence of prior groups (see Section 2.3.1). + + + + + + +Nandakumar, et al. Expires 17 July 2026 [Page 92] + +Internet-Draft moq-transport January 2026 + + + This extension can be added by the Original Publisher, but MUST NOT + be added by relays. This extension MAY be removed by relay when the + object in question is served via FETCH, and the gap that the + extension communicates is already communicated implicitly in the + FETCH response; it MUST NOT be modified or removed otherwise. + + An Object MUST NOT contain more than one instance of this extension + header. + +11.4. Prior Object ID Gap + + Prior Object ID Gap only applies to Objects, not Tracks. + + Prior Object ID Gap (Extension Header Type 0x3E) is a variable length + integer containing the number of Objects prior to the current Object + that do not and will never exist. For example, if the Original + Publisher is publishing Object 10 in Group 3 and knows it will never + publish Objects 8 or 9 in this Group, it can include Prior Object ID + Gap = 2. A Track is considered malformed (see Section 2.4.2) if any + of the following conditions are detected: + + * An Object contains more than one instance of Prior Object ID Gap. + + * An Object has a Prior Object ID Gap larger than the Object ID. + + * An endpoint receives an Object with a Prior Object ID Gap covering + an Object it previously received. + + * An endpoint receives an Object with an Object ID within a + previously communicated gap. + + This extension is optional, as publishers might not know the prior + gap gize, or there may not be a gap. If Prior Object ID Gap is not + present, the receiver cannot infer any information about the + existence of prior objects (see Section 2.1). + + This extension can be added by the Original Publisher, but MUST NOT + be added by relays. This extension MAY be removed by relay when the + object in question is served via FETCH, and the gap that the + extension communicates is already communicated implicitly in the + FETCH response; it MUST NOT be modified or removed otherwise. + + An Object MUST NOT contain more than one instance of this extension + header. + + + + + + + +Nandakumar, et al. Expires 17 July 2026 [Page 93] + +Internet-Draft moq-transport January 2026 + + +12. Security Considerations + + TODO: Expand this section, including subscriptions. + + TODO: Describe Cache Poisoning attacks + +12.1. Resource Exhaustion + + Live content requires significant bandwidth and resources. Failure + to set limits will quickly cause resource exhaustion. + + MOQT uses stream limits and flow control to impose resource limits at + the network layer. Endpoints SHOULD set flow control limits based on + the anticipated bitrate. + + Endpoints MAY impose a MAX STREAM count limit which would restrict + the number of concurrent streams which an application could have in + flight. + + The publisher prioritizes and transmits streams out of order. + Streams might be starved indefinitely during congestion. The + publisher and subscriber MUST cancel a stream, preferably the one + with the lowest priority, after reaching a resource limit. + +12.2. Timeouts + + Implementations are advised to use timeouts to prevent resource + exhaustion attacks by a peer that does not send expected data within + an expected time. Each implementation is expected to set its own + timeouts. + +12.3. Relay security considerations + +12.3.1. State maintenance + + A Relay SHOULD have mechanisms to prevent malicious endpoints from + flooding it with PUBLISH_NAMESPACE or SUBSCRIBE_NAMESPACE requests + that could bloat data structures. It could use the advertised + MAX_REQUEST_ID to limit the number of such requests, or could have + application-specific policies that can reject incoming + PUBLISH_NAMESPACE or SUBSCRIBE_NAMESPACE requests that cause the + state maintenance for the session to be excessive. + + + + + + + + + +Nandakumar, et al. Expires 17 July 2026 [Page 94] + +Internet-Draft moq-transport January 2026 + + +12.3.2. SUBSCRIBE_NAMESPACE with short prefixes + + A Relay can use authorization rules in order to prevent subscriptions + closer to the root of a large prefix tree. Otherwise, if an entity + sends a relay a SUBSCRIBE_NAMESPACE message with a short prefix, it + can cause the relay to send a large volume of PUBLISH_NAMESPACE + messages. As churn continues in the tree of prefixes, the relay + would have to continue to send PUBLISH_NAMESPACE/ + PUBLISH_NAMESPACE_DONE messages to the entity that had sent the + SUBSCRIBE_NAMESPACE. + + TODO: Security/Privacy Considerations of MOQT_IMPLEMENTATION + parameter + +13. IANA Considerations + + TODO: fill out currently missing registries: + + * MOQT ALPN values + + * Setup parameters + + * Non-setup Parameters - List which params can be repeated in the + table. + + * Message types + + * MOQ Extension headers - we wish to reserve extension types 0-63 + for standards utilization where space is a premium, 64 - 16383 for + standards utilization where space is less of a concern, and 16384 + and above for first-come-first-served non-standardization usage. + List which headers can be repeated in the table. + + * MOQT Auth Token Type + + TODO: register the URI scheme and the ALPN and grease the Extension + types + +13.1. Authorization Token Alias Type + + +======+===========+=================+ + | Code | Name | Specification | + +======+===========+=================+ + | 0x0 | DELETE | Section 9.2.2.1 | + +------+-----------+-----------------+ + | 0x1 | REGISTER | Section 9.2.2.1 | + +------+-----------+-----------------+ + | 0x2 | USE_ALIAS | Section 9.2.2.1 | + + + +Nandakumar, et al. Expires 17 July 2026 [Page 95] + +Internet-Draft moq-transport January 2026 + + + +------+-----------+-----------------+ + | 0x3 | USE_VALUE | Section 9.2.2.1 | + +------+-----------+-----------------+ + + Table 7 + +13.2. Message Parameters + + +================+=====================+=================+ + | Parameter Type | Parameter Name | Specification | + +================+=====================+=================+ + | 0x02 | DELIVERY_TIMEOUT | Section 9.2.2.2 | + +----------------+---------------------+-----------------+ + | 0x03 | AUTHORIZATION_TOKEN | Section 9.2.2.1 | + +----------------+---------------------+-----------------+ + | 0x08 | EXPIRES | Section 9.2.2.6 | + +----------------+---------------------+-----------------+ + | 0x09 | LARGEST_OBJECT | Section 9.2.2.7 | + +----------------+---------------------+-----------------+ + | 0x10 | FORWARD | Section 9.2.2.8 | + +----------------+---------------------+-----------------+ + | 0x20 | SUBSCRIBER_PRIORITY | Section 9.2.2.3 | + +----------------+---------------------+-----------------+ + | 0x21 | SUBSCRIPTION_FILTER | Section 9.2.2.5 | + +----------------+---------------------+-----------------+ + | 0x22 | GROUP_ORDER | Section 9.2.2.4 | + +----------------+---------------------+-----------------+ + | 0x32 | NEW_GROUP_REQUEST | Section 9.2.2.9 | + +----------------+---------------------+-----------------+ + + Table 8 + +13.3. Extension Headers + + +======+===============================+========+================+ + | Type | Name | Scope | Specification | + +======+===============================+========+================+ + | 0x02 | DELIVERY_TIMEOUT | Track | Section 11.1 | + +------+-------------------------------+--------+----------------+ + | 0x04 | MAX_CACHE_DURATION | Track | Section 11.1.1 | + +------+-------------------------------+--------+----------------+ + | 0x0B | IMMUTABLE_EXTENSIONS | Track, | Section 11.2 | + | | | Object | | + +------+-------------------------------+--------+----------------+ + | 0x0E | DEFAULT_PUBLISHER_PRIORITY | Track | Section | + | | | | 11.1.1.1 | + +------+-------------------------------+--------+----------------+ + | 0x22 | DEFAULT_PUBLISHER_GROUP_ORDER | Track | Section | + + + +Nandakumar, et al. Expires 17 July 2026 [Page 96] + +Internet-Draft moq-transport January 2026 + + + | | | | 11.1.1.2 | + +------+-------------------------------+--------+----------------+ + | 0x30 | DYNAMIC_GROUPS | Track | Section | + | | | | 11.1.1.3 | + +------+-------------------------------+--------+----------------+ + | 0x3C | PRIOR_GROUP_ID_GAP | Object | Section 11.3 | + +------+-------------------------------+--------+----------------+ + | 0x3E | PRIOR_OBJECT_ID_GAP | Object | Section 11.4 | + +------+-------------------------------+--------+----------------+ + + Table 9 + +13.4. Error Codes + +13.4.1. Session Termination Error Codes + + +============================+======+===============+ + | Name | Code | Specification | + +============================+======+===============+ + | NO_ERROR | 0x0 | Section 3.4 | + +----------------------------+------+---------------+ + | INTERNAL_ERROR | 0x1 | Section 3.4 | + +----------------------------+------+---------------+ + | UNAUTHORIZED | 0x2 | Section 3.4 | + +----------------------------+------+---------------+ + | PROTOCOL_VIOLATION | 0x3 | Section 3.4 | + +----------------------------+------+---------------+ + | INVALID_REQUEST_ID | 0x4 | Section 3.4 | + +----------------------------+------+---------------+ + | DUPLICATE_TRACK_ALIAS | 0x5 | Section 3.4 | + +----------------------------+------+---------------+ + | KEY_VALUE_FORMATTING_ERROR | 0x6 | Section 3.4 | + +----------------------------+------+---------------+ + | TOO_MANY_REQUESTS | 0x7 | Section 3.4 | + +----------------------------+------+---------------+ + | INVALID_PATH | 0x8 | Section 3.4 | + +----------------------------+------+---------------+ + | MALFORMED_PATH | 0x9 | Section 3.4 | + +----------------------------+------+---------------+ + | GOAWAY_TIMEOUT | 0x10 | Section 3.4 | + +----------------------------+------+---------------+ + | CONTROL_MESSAGE_TIMEOUT | 0x11 | Section 3.4 | + +----------------------------+------+---------------+ + | DATA_STREAM_TIMEOUT | 0x12 | Section 3.4 | + +----------------------------+------+---------------+ + | AUTH_TOKEN_CACHE_OVERFLOW | 0x13 | Section 3.4 | + +----------------------------+------+---------------+ + | DUPLICATE_AUTH_TOKEN_ALIAS | 0x14 | Section 3.4 | + + + +Nandakumar, et al. Expires 17 July 2026 [Page 97] + +Internet-Draft moq-transport January 2026 + + + +----------------------------+------+---------------+ + | VERSION_NEGOTIATION_FAILED | 0x15 | Section 3.4 | + +----------------------------+------+---------------+ + | MALFORMED_AUTH_TOKEN | 0x16 | Section 3.4 | + +----------------------------+------+---------------+ + | UNKNOWN_AUTH_TOKEN_ALIAS | 0x17 | Section 3.4 | + +----------------------------+------+---------------+ + | EXPIRED_AUTH_TOKEN | 0x18 | Section 3.4 | + +----------------------------+------+---------------+ + | INVALID_AUTHORITY | 0x19 | Section 3.4 | + +----------------------------+------+---------------+ + | MALFORMED_AUTHORITY | 0x1A | Section 3.4 | + +----------------------------+------+---------------+ + + Table 10 + +13.4.2. REQUEST_ERROR Codes + + +============================+======+===============+ + | Name | Code | Specification | + +============================+======+===============+ + | INTERNAL_ERROR | 0x0 | Section 9.8 | + +----------------------------+------+---------------+ + | UNAUTHORIZED | 0x1 | Section 9.8 | + +----------------------------+------+---------------+ + | TIMEOUT | 0x2 | Section 9.8 | + +----------------------------+------+---------------+ + | NOT_SUPPORTED | 0x3 | Section 9.8 | + +----------------------------+------+---------------+ + | MALFORMED_AUTH_TOKEN | 0x4 | Section 9.8 | + +----------------------------+------+---------------+ + | EXPIRED_AUTH_TOKEN | 0x5 | Section 9.8 | + +----------------------------+------+---------------+ + | DOES_NOT_EXIST | 0x10 | Section 9.8 | + +----------------------------+------+---------------+ + | INVALID_RANGE | 0x11 | Section 9.8 | + +----------------------------+------+---------------+ + | MALFORMED_TRACK | 0x12 | Section 9.8 | + +----------------------------+------+---------------+ + | DUPLICATE_SUBSCRIPTION | 0x19 | Section 9.8 | + +----------------------------+------+---------------+ + | UNINTERESTED | 0x20 | Section 9.8 | + +----------------------------+------+---------------+ + | PREFIX_OVERLAP | 0x30 | Section 9.8 | + +----------------------------+------+---------------+ + | INVALID_JOINING_REQUEST_ID | 0x32 | Section 9.8 | + +----------------------------+------+---------------+ + + + + +Nandakumar, et al. Expires 17 July 2026 [Page 98] + +Internet-Draft moq-transport January 2026 + + + Table 11 + +13.4.3. PUBLISH_DONE Codes + + +====================+======+===============+ + | Name | Code | Specification | + +====================+======+===============+ + | INTERNAL_ERROR | 0x0 | Section 9.15 | + +--------------------+------+---------------+ + | UNAUTHORIZED | 0x1 | Section 9.15 | + +--------------------+------+---------------+ + | TRACK_ENDED | 0x2 | Section 9.15 | + +--------------------+------+---------------+ + | SUBSCRIPTION_ENDED | 0x3 | Section 9.15 | + +--------------------+------+---------------+ + | GOING_AWAY | 0x4 | Section 9.15 | + +--------------------+------+---------------+ + | EXPIRED | 0x5 | Section 9.15 | + +--------------------+------+---------------+ + | TOO_FAR_BEHIND | 0x6 | Section 9.15 | + +--------------------+------+---------------+ + | UPDATE_FAILED | 0x8 | Section 9.15 | + +--------------------+------+---------------+ + | MALFORMED_TRACK | 0x12 | Section 9.15 | + +--------------------+------+---------------+ + + Table 12 + +13.4.4. Data Stream Reset Error Codes + + +=======================+======+================+ + | Name | Code | Specification | + +=======================+======+================+ + | INTERNAL_ERROR | 0x0 | Section 10.4.3 | + +-----------------------+------+----------------+ + | CANCELLED | 0x1 | Section 10.4.3 | + +-----------------------+------+----------------+ + | DELIVERY_TIMEOUT | 0x2 | Section 10.4.3 | + +-----------------------+------+----------------+ + | SESSION_CLOSED | 0x3 | Section 10.4.3 | + +-----------------------+------+----------------+ + | UNKNOWN_OBJECT_STATUS | 0x4 | Section 10.4.3 | + +-----------------------+------+----------------+ + | MALFORMED_TRACK | 0x12 | Section 10.4.3 | + +-----------------------+------+----------------+ + + Table 13 + + + + +Nandakumar, et al. Expires 17 July 2026 [Page 99] + +Internet-Draft moq-transport January 2026 + + +Contributors + + The original design behind this protocol was inspired by three + independent proposals: WARP [I-D.draft-lcurley-warp] by Luke Curley, + RUSH [I-D.draft-kpugin-rush] by Kirill Pugin, Nitin Garg, Alan + Frindell, Jordi Cenzano and Jake Weissman, and QUICR + [I-D.draft-jennings-moq-quicr-proto] by Cullen Jennings, Suhas + Nandakumar and Christian Huitema. The authors of those documents + merged their proposals to create the first draft of moq-transport. + The IETF MoQ Working Group received an enormous amount of support + from many people. The following people provided substantive + contributions to this document: + + * Ali Begen + + * Charles Krasic + + * Christian Huitema + + * Cullen Jennings + + * James Hurley + + * Jordi Cenzano + + * Kirill Pugin + + * Luke Curley + + * Martin Duke + + * Mike English + + * Mo Zanaty + + * Will Law + +References + +Normative References + + [I-D.draft-ietf-quic-reliable-stream-reset] + Seemann, M. and K. Oku, "QUIC Stream Resets with Partial + Delivery", Work in Progress, Internet-Draft, draft-ietf- + quic-reliable-stream-reset-07, 14 June 2025, + . + + + + +Nandakumar, et al. Expires 17 July 2026 [Page 100] + +Internet-Draft moq-transport January 2026 + + + [QUIC] Iyengar, J., Ed. and M. Thomson, Ed., "QUIC: A UDP-Based + Multiplexed and Secure Transport", RFC 9000, + DOI 10.17487/RFC9000, May 2021, + . + + [RFC2119] Bradner, S., "Key words for use in RFCs to Indicate + Requirement Levels", BCP 14, RFC 2119, + DOI 10.17487/RFC2119, March 1997, + . + + [RFC3629] Yergeau, F., "UTF-8, a transformation format of ISO + 10646", STD 63, RFC 3629, DOI 10.17487/RFC3629, November + 2003, . + + [RFC3986] Berners-Lee, T., Fielding, R., and L. Masinter, "Uniform + Resource Identifier (URI): Generic Syntax", STD 66, + RFC 3986, DOI 10.17487/RFC3986, January 2005, + . + + [RFC7301] Friedl, S., Popov, A., Langley, A., and E. Stephan, + "Transport Layer Security (TLS) Application-Layer Protocol + Negotiation Extension", RFC 7301, DOI 10.17487/RFC7301, + July 2014, . + + [RFC8174] Leiba, B., "Ambiguity of Uppercase vs Lowercase in RFC + 2119 Key Words", BCP 14, RFC 8174, DOI 10.17487/RFC8174, + May 2017, . + + [RFC8615] Nottingham, M., "Well-Known Uniform Resource Identifiers + (URIs)", RFC 8615, DOI 10.17487/RFC8615, May 2019, + . + + [RFC9110] Fielding, R., Ed., Nottingham, M., Ed., and J. Reschke, + Ed., "HTTP Semantics", STD 97, RFC 9110, + DOI 10.17487/RFC9110, June 2022, + . + + [RFC9221] Pauly, T., Kinnear, E., and D. Schinazi, "An Unreliable + Datagram Extension to QUIC", RFC 9221, + DOI 10.17487/RFC9221, March 2022, + . + + [WebTransport] + Frindell, A., Kinnear, E., and V. Vasiliev, "WebTransport + over HTTP/3", Work in Progress, Internet-Draft, draft- + ietf-webtrans-http3-14, 20 October 2025, + . + + + +Nandakumar, et al. Expires 17 July 2026 [Page 101] + +Internet-Draft moq-transport January 2026 + + +Informative References + + [I-D.draft-jennings-moq-quicr-proto] + Jennings, C. F., Nandakumar, S., and C. Huitema, "QuicR - + Media Delivery Protocol over QUIC", Work in Progress, + Internet-Draft, draft-jennings-moq-quicr-proto-01, 11 July + 2022, . + + [I-D.draft-kpugin-rush] + Pugin, K., Garg, N., Frindell, A., Ferret, J. C., and J. + Weissman, "RUSH - Reliable (unreliable) streaming + protocol", Work in Progress, Internet-Draft, draft-kpugin- + rush-03, 21 April 2025, + . + + [I-D.draft-lcurley-warp] + Curley, L., Pugin, K., Nandakumar, S., and V. Vasiliev, + "Warp - Live Media Transport over QUIC", Work in Progress, + Internet-Draft, draft-lcurley-warp-04, 13 March 2023, + . + + [I-D.ietf-webtrans-overview] + Kinnear, E. and V. Vasiliev, "The WebTransport Protocol + Framework", Work in Progress, Internet-Draft, draft-ietf- + webtrans-overview-11, 20 October 2025, + . + + [RFC6582] Henderson, T., Floyd, S., Gurtov, A., and Y. Nishida, "The + NewReno Modification to TCP's Fast Recovery Algorithm", + RFC 6582, DOI 10.17487/RFC6582, April 2012, + . + + [RFC9000] Iyengar, J., Ed. and M. Thomson, Ed., "QUIC: A UDP-Based + Multiplexed and Secure Transport", RFC 9000, + DOI 10.17487/RFC9000, May 2021, + . + + [RFC9438] Xu, L., Ha, S., Rhee, I., Goel, V., and L. Eggert, Ed., + "CUBIC for Fast and Long-Distance Networks", RFC 9438, + DOI 10.17487/RFC9438, August 2023, + . + + + + + + +Nandakumar, et al. Expires 17 July 2026 [Page 102] + +Internet-Draft moq-transport January 2026 + + +Appendix A. Change Log + + RFC Editor's Note: Please remove this section prior to publication of + a final version of this document. + + Issue and pull request numbers are listed with a leading octothorp. + +A.1. Since draft-ietf-moq-transport-15 + + *Setup and Control Plane* + + * Delta encode Key-Value-Pairs for Parameters and Headers (#1315) + + * Use Request ID in PUBLISH_NAMESPACE_{DONE/CANCEL} (#1329) + + * Remove delivery related params from TRACK_STATUS for Subscribers + (#1325) + + * PUBLISH does not imply PUBLISH_NAMESPACE (#1364) + + * Allow Start Location to decrease in SUBSCRIBE_UPDATE (#1323) + + * Change SUBSCRIBE_UPDATE to REQUEST_UPDATE and expand ability to + update (#1332) + + * Put SUBSCRIBE_NAMESPACE on a stream, make Namespaces and PUBLISH + independent (#1344) + + * Require NAMESPACE before NAMESPACE_DONE (#1392) + + * Allow the '*' or the empty namespace in SUBSCRIBE_NAMESPACE + (#1393) + + * Relays match SUBSCRIBE to both Tracks and Namespaces (#1397) + + * Clarify sending requests after sending GOAWAY (#1398) + + * Add Retry Interval to REQUEST_ERROR (#1339) + + * Add Extension Headers to PUBLISH, SUBSCRIBE_OK, and FETCH_OK + (#1374) + + * Move track properties to extensions, scope parameters (#1390) + + * Add LARGEST_OBJECT parameter to TRACK_STATUS (#1367) + + * Duplicate subscription processing (#1341) + + + + +Nandakumar, et al. Expires 17 July 2026 [Page 103] + +Internet-Draft moq-transport January 2026 + + + * Address Track Name/Namespace edge cases (#1399) + + *Data Plane Wire Format and Handling* + + * Enable mixing datagrams with streams in one track (#1350) + + * Clarify datagrams and subgroups (#1382) + + * Redo the way we deal with missing Objects and Object Status + (#1342) + + * Allow unknown ranges in a FETCH response (#1331) + + * Do not reopen subgroups after delivery timeout or STOP_SENDING + (#1396) + + * Clarify handling of unknown extensions (#1395) + + * Clarify Delivery Timeout for datagrams (#1406) + + * Disallow DELIVERY_TIMEOUT=0 (#1330) + + * Malformed track due to multiple priorities for one subgroup + (#1317) + + *Notable Editorial Changes* + + * Subscribers can migrate networks too (#1410) + + * Rename Version Specific Parameters to Message Parameters (#1411) + + * Clarify valid joining fetch subscription states (#1363) + + * Formatting names for logs (#1355) + + * A Publisher might not use the congestion window (#1408) + +A.2. Since draft-ietf-moq-transport-14 + + *Setup and Control Plane* + + * Always use ALPN for version negotiation (#499) + + * Consolidate all the Error Message types (#1159) + + * Change MOQT IMPLEMENTATION code point to 0x7 (#1191) + + * Add Forward to SUBSCRIBE_NAMESPACE (#1220) + + + +Nandakumar, et al. Expires 17 July 2026 [Page 104] + +Internet-Draft moq-transport January 2026 + + + * Parameters for Group Order, Subscribe Priority and Subscription + Filter (redo) (#1273) + + * REQUEST_OK message (#1274) + + * Subscribe Update Acknowledgements (#1275) + + * Disallow DELETE and USE_ALIAS in CLIENT_SETUP (#1277) + + * Remove Expires field from SUBSCRIBE_OK (#1282) + + * Make Forward a Parameter (#1283) + + * Allow SUBSCRIBE_UPDATE to increase the end location (#1288) + + * Add default port for raw QUIC (#1289) + + * Unsubscribe Namespace should be linked to Subscribe Namespace + (#1292) + + *Data Plane Wire Format and Handling* + + * Fetch Object serialization optimization (#949) + + * Make default PUBLISHER PRIORITY a parameter, optional in Subgroup/ + Datagram (#1056) + + * Allow datagram status with object ID=0 (#1197) + + * Disallow object extension headers in all non-Normal status objects + (#1266) + + * Objects for malformed track must not be cached (#1290) + + * Remove NO_OBJECTS fetch error code (#1303) + + * Clarify what happens when max_cache_duration parameter is omitted + (#1287) + + *Notable Editorial Changes* + + * Rename Request ID field in MAX_REQUEST_ID (#1250) + + * Define and draw subscription state machine (#1296) + + * Omitting a subgroup object necessitates reset (#1295) + + * Define duplication rules for header extensions (#1293) + + + +Nandakumar, et al. Expires 17 July 2026 [Page 105] + +Internet-Draft moq-transport January 2026 + + + * Clarify joining fetch end location (#1286) + +A.3. Since draft-ietf-moq-transport-13 + + *Setup and Control Plane* + + * Add an AUTHORITY parameter (#1058) + + * Add a free-form SETUP parameter identifying the implementation + (#1114) + + * Add a Request ID to SUBSCRIBE_UDPATE (#1106) + + * Indicate which params can appear PUBLISH* messages (#1071) + + * Add TRACK_STATUS to the list of request types affected by GOAWAY + (#1105) + + *Data Plane Wire Format and Handling* + + * Delta encode Object IDs within Subgroups (#1042) + + * Use a bit in Datagram Type to convey Object ID = 0 (#1055) + + * Corrected missed code point updates to Object Datagram Status + (#1082) + + * Merge OBJECT_DATAGRAM and OBJECT_DATAGRAM_STATUS description + (#1179) + + * Objects are not schedulable if flow-control blocked (#1054) + + * Clarify DELIVERY_TIMEOUT reordering computation (#1120) + + * Receiving unrequested Objects (#1112) + + * Clarify End of Track (#1111) + + * Malformed tracks apply to FETCH (#1083) + + * Remove early FIN from the definition of malformed tracks (#1096) + + * Prior Object ID Gap Extension header (#939) + + * Add Extension containing immutable extensions (#1025) + + *Relay Handling* + + + + +Nandakumar, et al. Expires 17 July 2026 [Page 106] + +Internet-Draft moq-transport January 2026 + + + * Explain FETCH routing for relays (#1165) + + * MUST for multi-publisher relay handling (#1115) + + * Filters don't (usually) determine the end of subscription (#1113) + + * Allow self-subscriptions (#1110) + + * Explain Namespace Prefix Matching in more detail (#1116) + + *Explanatory* + + * Explain Modularity of MOQT (#1107) + + * Explain how to resume publishing after losing state (#1087) + + *Major Editorial Changes* + + * Rename ANNOUNCE to PUBLISH_NAMESPACE (#1104) + + * Rename SUBSCRIBE_DONE to PUBLISH_DONE (#1108) + + * Major FETCH Reorganization (#1173) + + * Reformat Error Codes (#1091) + +A.4. Since draft-ietf-moq-transport-12 + + * TRACK_STATUS_REQUEST and TRACK_STATUS have changed to directly + mirror SUBSCRIBE/OK/ERROR (#1015) + + * SUBSCRIBE_ANNOUNCES was renamed back to SUBSCRIBE_NAMESPACE + (#1049) + +A.5. Since draft-ietf-moq-transport-11 + + * Move Track Alias from SUBSCRIBE to SUBSCRIBE_OK (#977) + + * Expand cases FETCH_OK returns Invalid Range (#946) and clarify + fields (#936) + + * Add an error code to FETCH_ERROR when an Object status is unknown + (#825) + + * Rename Latest Object to Largest Object (#1024) and clarify what to + do when it's incomplete (#937) + + * Explain Malformed Tracks and what to do with them (#938) + + + +Nandakumar, et al. Expires 17 July 2026 [Page 107] + +Internet-Draft moq-transport January 2026 + + + * Allow End of Group to be indicated in a normal Object (#1011) + + * Relays MUST have an upstream subscription to send SUBSCRIBE_OK + (#1017) + + * Allow AUTHORIZATION TOKEN in CLIENT_SETUP, SERVER_SETUP and other + fixes (#1013) + + * Add PUBLISH for publisher initiated subscriptions (#995) and fix + the PUBLISH codepoints (#1048, #1051) + +A.6. Since draft-ietf-moq-transport-10 + + * Added Common Structure definitions - Location, Key-Value-Pair and + Reason Phrase + + * Limit lengths of all variable length fields, including Track + Namespace and Name + + * Control Message length is now 16 bits instead of variable length + + * Subscribe ID became Request ID, and was added to most control + messages. Request ID is used to correlate OK/ERROR responses for + ANNOUNCE, SUBSCRIBE_NAMESPACE, and TRACK_STATUS. Like Subscribe + ID, Request IDs are flow controlled. + + * Explain rules for caching in more detail + + * Changed the SETUP parameter format for even number parameters to + match the Object Header Extension format + + * Rotated SETUP code points + + * Added Parameters to TRACK_STATUS and TRACK_STATUS_REQUEST + + * Clarified how subscribe filters work + + * Added Next Group Filter to SUBSCRIBE + + * Added Forward flag to SUBSCRIBE + + * Renamed FETCH_OK field to End and clarified how to set it + + * Added Absolute Joining Fetch + + * Clarified No Error vs Invalid Range FETCH_ERROR cases + + + + + +Nandakumar, et al. Expires 17 July 2026 [Page 108] + +Internet-Draft moq-transport January 2026 + + + * Use bits in SUBGROUP_HEADER and DATAGRAM* types to compress + subgroup ID and extensions + + * Coalesced END_OF_GROUP and END_OF_TRACK_AND_GROUP status + + * Objects that Do Not Exist cannot have extensions when sent on the + wire + + * Specified error codes for resetting data streams + + * Defined an Object Header Extension for communicating a known Group + ID gap + + * Replaced AUTHORIZATION_INFO with AUTHORIZATION_TOKEN, which has + more structure, compression, and additional Auth related error + codes (#760) + +Authors' Addresses + + Suhas Nandakumar + Cisco + Email: snandaku@cisco.com + + + Victor Vasiliev + Google + Email: vasilvv@google.com + + + Ian Swett (editor) + Google + Email: ianswett@google.com + + + Alan Frindell (editor) + Meta + Email: afrind@meta.com + + + + + + + + + + + + + + +Nandakumar, et al. Expires 17 July 2026 [Page 109] diff --git a/publisher/contribute/broadcast.ts b/publisher/contribute/broadcast.ts index cd784a2..0fe3c5e 100644 --- a/publisher/contribute/broadcast.ts +++ b/publisher/contribute/broadcast.ts @@ -149,7 +149,17 @@ export class Broadcast { * supplied here to override the broadcast-wide one, or omitted to fall * back to whatever was passed to the `Broadcast` constructor. */ - addTrack(media: MediaStreamTrack, config: VideoEncoderConfig | AudioEncoderConfig): void { + /** + * Add a media track to a running broadcast and append a matching entry + * to `this.catalog.tracks`. Returns the generated track name (e.g. + * `"video-a3f7b2c1"`) so callers can later pass it to `removeTrack`. + * + * Each call produces a unique name (via the random suffix in Track), + * so there is no hard limit on how many times the same kind can be + * cycled through add/remove. The caller is responsible for ensuring at + * most one active track of each kind at a time. + */ + addTrack(media: MediaStreamTrack, config: VideoEncoderConfig | AudioEncoderConfig): string { // Choose audio vs video config slot based on the media kind. We use // the per-call config when provided, otherwise fall back to the // broadcast-wide one set in the constructor. @@ -158,15 +168,15 @@ export class Broadcast { video: media.kind === "video" ? (config as VideoEncoderConfig) : this.config.video, } - // Track name is currently derived from media.kind (see Track ctor), so - // duplicate detection happens on that name. - if (this.#tracks.has(media.kind)) { - throw new Error(`track with name '${media.kind}' already exists`) - } - const { track, entry } = this.#buildTrack(media, trackConfig) this.#tracks.set(track.name, track) this.catalog.tracks.push(entry) + log.debug("[catalog] addTrack: catalog after add", { + addedTrackName: track.name, + trackCount: this.catalog.tracks.length, + tracks: this.catalog.tracks.map((t) => ({ name: t.name, initTrack: (t as { initTrack?: string }).initTrack })), + }) + return track.name } /** @@ -183,6 +193,11 @@ export class Broadcast { */ async removeTrack(name: string): Promise { const track = this.#tracks.get(name) + log.debug("[catalog] removeTrack called", { + name, + trackFound: !!track, + currentTracks: this.catalog.tracks.map((t) => ({ name: t.name })), + }) if (!track) return await track.close() @@ -191,9 +206,14 @@ export class Broadcast { // Catalog entries use `${trackName}.m4s` as their name. const entryName = `${name}.m4s` const idx = this.catalog.tracks.findIndex((t) => t.name === entryName) + log.debug("[catalog] removeTrack: catalog entry search", { entryName, foundAtIndex: idx }) if (idx >= 0) { this.catalog.tracks.splice(idx, 1) } + log.debug("[catalog] removeTrack: catalog after remove", { + trackCount: this.catalog.tracks.length, + tracks: this.catalog.tracks.map((t) => ({ name: t.name })), + }) } async #run() { @@ -249,6 +269,11 @@ export class Broadcast { const bytes = Catalog.encode(this.catalog) + log.debug("[catalog] serving catalog to subscriber", { + trackCount: this.catalog.tracks.length, + tracks: this.catalog.tracks.map((t) => ({ name: t.name, initTrack: (t as { initTrack?: string }).initTrack })), + }) + await subscriber.ack() await sleep(500) diff --git a/publisher/contribute/track.ts b/publisher/contribute/track.ts index 31b8cab..aca0033 100644 --- a/publisher/contribute/track.ts +++ b/publisher/contribute/track.ts @@ -20,7 +20,14 @@ export class Track { #notify = new Notify() constructor(media: MediaStreamTrack, config: BroadcastConfig) { - this.name = media.kind + // Use a random 8-byte hex suffix so each track instance gets a unique + // name. Without this, a second video track added after removeTrack + // reuses the same key ("video"), causing the worker's init-track lookup + // to hit a stale or absent entry and hang forever on `await init.promise`. + const id = Array.from(crypto.getRandomValues(new Uint8Array(4))) + .map((b) => b.toString(16).padStart(2, "0")) + .join("") + this.name = `${media.kind}-${id}` // We need to split based on type because Typescript is hard if (isAudioTrack(media)) { diff --git a/publisher/publish/index.ts b/publisher/publish/index.ts index de8742f..7b8f18d 100644 --- a/publisher/publish/index.ts +++ b/publisher/publish/index.ts @@ -61,12 +61,15 @@ export class PublisherApi { * Add a media track to the running broadcast. Delegates to * `Broadcast.addTrack`. Throws if `publish()` has not been called yet — * there is no broadcast to mutate until that point. + * + * Returns the generated track name (e.g. `"video-a3f7b2c1"`). Pass this + * to `removeTrack` when you want to remove the track later. */ - addTrack(media: MediaStreamTrack, config: VideoEncoderConfig | AudioEncoderConfig): void { + addTrack(media: MediaStreamTrack, config: VideoEncoderConfig | AudioEncoderConfig): string { if (!this.broadcast) { throw new Error("PublisherApi.addTrack: publish() has not been called; no broadcast to mutate") } - this.broadcast.addTrack(media, config) + return this.broadcast.addTrack(media, config) } /**