MOQT output and relay
Publish normalized H.264 and AAC-LC media, or relay presentations using catalog discovery or static exact tracks.
Overview
output.moqt is an explicit opt-in network output. It can map a normalized source generation to an IETF MOQT publication with MSF-01 catalogs and CMSF-01 CMAF objects, or forward configured exact Full Track Names through the bounded opaque relay. The default configuration opens no MOQT socket.
Native QUIC and WebTransport entry points share the same request authorization, admission limits, route ownership, and identity-free observability. MOQT is separate from WHEP and does not provide a general browser playback API.
This module requires an offline full license. See licensing for setup and validity.
Publisher configuration
Place this moqt object under output. It publishes the normalized stream demo under the binary namespace tuple live, demo. The bearer token must resolve to 32 to 512 printable ASCII bytes. Keep it in deployment secret storage rather than committing it to the configuration file.
{
"moqt": {
"enabled": true,
"native": {"host": "0.0.0.0", "port": 4443},
"webTransport": {
"listener": {"host": "0.0.0.0", "port": 4444},
"path": "/moqt"
},
"tls": {
"certificateFile": "/run/secrets/moqt-cert.pem",
"privateKeyFile": "/run/secrets/moqt-key.pem"
},
"authorization": {
"mode": "bearer",
"bearerTokenType": 0,
"credentials": [{
"principal": "moqt-viewers",
"token": "${MOQT_TOKEN}",
"permissions": [
{"operation": "subscribe", "namespacePrefix": ["live"]},
{"operation": "fetch", "namespacePrefix": ["live"]}
]
}]
},
"routes": {
"localPeerId": "origin-london-1",
"publishers": [{
"streamId": "demo",
"namespace": ["live", "demo"],
"catalogNamespace": "live.demo",
"videoBitrate": 2000000,
"videoFrameRate": 25,
"audioBitrate": 128000
}]
},
"format": {"profile": "moqt-21"}
}
}
Set videoBitrate and audioBitrate in bits per second and videoFrameRate in frames per second to match the source. Video requires a positive bitrate and frame rate; AAC audio requires a positive bitrate. The example describes 2 Mbps H.264 video at 25 fps and 128 kbps AAC audio. These values supply catalog metadata, and the frame rate supplies an H.264 sample duration when the normalized source omits it. They do not configure an encoder or transcode the source. Omit the video values only for an audio-only source, or the audio value only for a video-only source. Missing required metadata rejects publication when the source declares its tracks.
The two listeners are independent UDP endpoints and must not overlap. One listener is sufficient. Add tls.clientCAFile to require and verify a client certificate on every enabled carrier.
Network and authorization boundary
output.moqt is valid only with enabled: true. It requires at least one native QUIC or WebTransport UDP listener plus certificate and private-key files. Native and WebTransport listener addresses must differ when both are enabled. The default format.profile is moqt-21, offering moqt-21 then the compatible moqt-20 identifier. Set format.profile to moqt-22 to offer moqt-22, moqt-21, then moqt-20 on both carriers and outbound relay connections. Other configured profiles fail startup. Each session binds its encoder and decoder to the selected native ALPN or WebTransport application protocol. Draft-22 uses an explicit Location Filter Type, including inside FILL_PARAMETERS; draft-20/21 retain their length-inferred encoding. Both carriers record the selected identifier, use TLS 1.3, require bidirectional QUIC DATAGRAM support for MOQT, and disable 0-RTT. The WebTransport CONNECT path must be absolute and contain no query or fragment. The transport library applies its same-origin check to browser requests.
authorization.mode is bearer or anonymous. Bearer mode requires exactly one authorization token of the configured bearerTokenType on each authorized initial request. Plaintext appears only while building the policy; the running policy retains a SHA-256 digest and detached permissions. Anonymous mode still requires explicit operation and namespace-prefix permissions. Permissions cover subscribe, publish, fetch, namespace, and route operations over exact binary namespace-component prefixes. route authorizes PUBLISH_NAMESPACE independently from track publication.
The session checks authorization before reserving a request ID, Track Alias, or application preparation. A missing, invalid, wrong-type, or ambiguous token consumes none of those resources. REQUEST_UPDATE without a token reuses the initial request credential; an explicitly supplied invalid update token rejects the update. Namespace-changing updates are authorized against their effective namespace.
The endpoint enforces global session, per-peer session, and token-bucket admission limits. A verified certificate contributes its unambiguous URI SAN, DNS SAN, or IP SAN to the opaque peer key. Without that identity, only a hash of the remote host is used. No peer identity, address, or hash enters a log or metric label. Shutdown first stops admission, then drains listeners and accepted sessions. Drained closes only after those resources are actually released.
Request admission maintains separate global and per-peer concurrent limits, token-bucket rates, and bursts. The peer registry is bounded too. Authorization succeeds first, then admission decides before request ID, Track Alias, route, publication, or application resource commit. An initial request lease remains owned for the request stream lifetime; REQUEST_UPDATE consumes only rate budget. During shutdown the limiter opens no new peer owner, and Drained closes only after every request lease is released.
The node configuration still requires output.http.listener, even when MOQT has no HTTP media output. It is not a MOQT carrier. A relay-only node with no local publisher can omit SRT input.
Route and cache configuration
output.moqt.routes is mandatory and contains at least one publisher or relay route. localPeerId is printable ASCII. A publisher route binds one validated streamId source generation to an exact binary namespace tuple and one catalog namespace. A local namespace can appear only once. The total configured publisher route count cannot exceed cache.maxPublications. A publication is not visible before its source exists. Source replacement creates a new publication generation while the old generation remains owned only until its already attached consumers actually drain.
Relay configuration requires a nonzero topologyVersion. Each peer binds an ID to a native QUIC address and TLS serverName; a separate CA and client certificate are optional. The outbound connection requires TLS 1.3, an ALPN from the configured profile window, bidirectional QUIC DATAGRAM support, and disabled 0-RTT. After dialing, the unambiguous verified peer identity from the upstream certificate must exactly equal upstreamPeerId. The relay manager does not fall back to another carrier or an unverified peer identity.
A relay route contains a route ID, required operator-controlled presentationId, required catalog or track role, owning peerId, declared upstreamPeerId, exact namespace and track name, and an optional upstream bearer token. Every presentation owned by one peer must contain exactly one catalog role. By default, that catalog determines the current required media and timeline routes. The catalog role requires the catalog track name; that track name cannot use the track role. Existing configurations must add both presentation fields to every relay route. The server does not derive either field from route ID, namespace, peer identity, track name, or catalog data. Only routes owned by the local peer start dial workers. Server configuration validation rejects missing or duplicate catalog roles. Topology validation rejects unknown peers, duplicate or ambiguous ownership, cycles, self-routes, and the configured route limit before dialing. The upstream token appears only in the outbound request; it is absent from formatted configuration, status, logs, and metrics.
A relay-only node needs a listener, TLS, authorization, and a catalog route for each presentation. This example follows the catalog and its advertised tracks from a native upstream:
{
"routes": {
"localPeerId": "edge.paris.internal",
"topologyVersion": 1,
"peers": [{
"id": "origin.internal",
"nativeAddress": "origin.internal:4443",
"serverName": "origin.internal",
"caFile": "/run/secrets/moqt-origin-ca.pem"
}],
"relays": [{
"id": "live-demo-catalog",
"presentationId": "demo",
"role": "catalog",
"peerId": "edge.paris.internal",
"upstreamPeerId": "origin.internal",
"namespace": ["live", "demo"],
"trackName": "catalog",
"token": "${MOQT_UPSTREAM_TOKEN}"
}],
"retry": {
"maxAttempts": 0,
"attemptTimeout": "10s",
"backoffBase": "250ms",
"backoffCap": "5s",
"multiplier": 2
}
}
}
The upstream certificate must identify the configured upstreamPeerId unambiguously after TLS verification. Add a catalog route for each additional presentation. Explicit media entries are optional overrides in automatic mode.
Catalog routes default to followCatalog: true. The relay validates MSF full catalogs and delta updates, subscribes to their media and timeline tracks, and removes obsolete names after codec or track generation changes. Configured track entries in the same presentation act as exact-name overrides for route ID and request parameters while advertised; obsolete entries stop being required. All entries in an automatically followed presentation must share the catalog's binary namespace and upstream peer. New tracks inherit the catalog's upstream token unless an exact configured entry supplies its own.
MSF tracks that omit namespace inherit the catalog route's binary namespace tuple. This is the format emitted by the repository publisher. To accept an explicit MSF namespace string, set catalogNamespace on the catalog route to that exact value. Other namespaces are rejected; catalog data never chooses a peer, address, credential, or binary namespace. Set followCatalog: false on the catalog route to preserve static exact-track routing, where every configured route remains required and catalog payloads stay opaque. Omit catalogNamespace in static mode; it is only valid with catalog following.
Discovery shares cache.maxRoutes with all other local routes, including catalogs. A presentation needs room for its catalog plus every advertised track. The existing MSF parser bounds each catalog object to 256 KiB and each snapshot to 256 tracks; its bounded metadata state is separate from retained media and subscriber queues. Invalid catalogs, unsupported namespace mappings, and route-capacity conflicts fail the catalog attempt and enter the configured retry policy. Readiness stays false until a validated catalog and every current required route are available.
The cache section may choose only deployment limits at or below repository hard ceilings. It separately bounds publications, relay routes, attachments, retained and queued objects and bytes, CMSF timeline history, codec state, and prepared batches. These are process-memory limits, not persistent cache or DVR contracts. Certificate, CA, route, topology, and token file or configuration changes require a process restart.
maxAttachments, maxRetainedObjects, maxRetainedBytes, maxQueuedObjects, and maxQueuedBytes apply to each origin publication as well as each relay route. Omitted or zero values select the hard ceilings. An origin attachment shares one queue budget between its initial retained snapshot and pending live objects. An initial snapshot that exceeds this budget is rejected with TOO_FAR_BEHIND; an oversized retained group is evicted as a whole. A detached SUBSCRIBE or FETCH worker continues to own its attachment slot until its sender callbacks return.
Media profile
The publisher accepts the repository normalized H.264 video and AAC-LC audio profile. It emits CMSF-01 CMAF initialization data and media fragments. Each media object contains one CMAF moof and mdat pair. A video group can begin only after parameter sets and a random-access sample. After a discontinuity, replacement, or codec change, video waits for the next usable random-access sample. AAC-LC uses the declared sample rate and frame duration. Unsupported or ambiguous codec state is rejected.
During multi-track startup or replacement, a sample that passes track, generation, duration, and payload validation is dropped without buffering until its codec state and every other declared codec state are ready, the full catalog commits, and a matching anchor covers the sample timestamp. A mismatched anchor epoch remains fail-closed. Each readiness drop increments the saturated CMSF drop counter. Invalid or stale samples, invalid durations, and malformed payloads remain fail-closed.
The current profile does not publish MPEG audio, media datagrams, adaptive bitrate ladders, DRM, arbitrary codecs, or arbitrary CMAF layouts. Catalogs use MSF-01, not LOC.
Namespace, catalog, and track contract
The exact MOQT namespace tuple and track name on the request stream select a publication. Namespace components are binary values, not flattened text. The publisher binds one normalized source generation to one immutable publication namespace and uses generation-specific track names. Source replacement therefore cannot silently bind a subscriber to stale media.
The catalog track is authoritative for available audio and video tracks. An MSF full catalog published before dependent media contains the CMAF initialization data. A topology or codec change publishes a new full catalog group. The bounded catalog delta parser and atomic apply path form a receive-side foundation, but this producer does not emit deltas. Track metadata preserves a declared valid BCP 47 language tag when present. Subscribers must treat a new catalog generation, initialization value, or discontinuity as a new decode boundary.
The publisher maps media PTS, MOQT object location, and UTC milliseconds on a standard MSF media timeline track. The timeline depends on the primary media track, and each group's first object contains bounded full timeline history. Provenance and discontinuity context remain bounded application metadata at the object boundary. They support in-process publisher and relay correlation. They are not routable client identity, credentials, stream ID labels, raw media copies, or a substitute for the MSF catalog.
Opaque relay
The relay aggregates one session-owned upstream subscription for each active exact-binary Full Track Name route. The route generation owns the upstream subscription, Current Group retention, downstream attachment reservations, subscriber workers, and capacity slot. Replacement fences callbacks from the old generation before the successor route becomes visible.
The relay forwards each upstream object as opaque data. Group ID, Subgroup ID, Object ID, priority, status, raw properties, and payload bytes remain unchanged. Only the downstream session-local subgroup stream header and Track Alias are re-encoded. The relay does not demux, normalize, transcode, repackage CMSF or MSF data, or construct LL-HLS, WHEP, recording, or SRT output resources.
A late join can start only from the in-memory Current Group retention. Retention evicts deterministically under hard object and byte ceilings, provides no unbounded DVR, and cannot recover after process restart. If the required retained boundary is unavailable, the request ends with bounded TOO_FAR_BEHIND status. Each downstream subscriber owns an isolated bounded queue and worker; ordinary media forwarding isolates a slow or failed subscriber. A catalog transition still waits for obsolete child owners to drain before it reuses their capacity.
The relay admits at most 256 route generations. Each route admits at most 1024 pending plus active attachments. Current Group retention is limited to 256 objects and 32 MiB per route; each attachment's combined prepared snapshot and live queue has the same 256-object and 32 MiB ceilings. One object remains subject to the session's 16 MiB payload ceiling. A replaced generation continues to occupy its route slot until Drained confirms that its upstream subscription and workers have exited.
Prepare creates the downstream attachment reservation before SUBSCRIBE_OK. Commit starts delivery only after the response is written, and Abort releases an uncommitted reservation exactly once. Close first removes route visibility and is idempotent and nonblocking. Drained signals only after the upstream subscription and subscriber workers exit and all queues, retained objects, and capacity ownership are released.
Multi-tier route ownership
The route manager consumes one immutable, versioned topology snapshot. Each route names its owning peer, authoritative upstream peer, exact binary Full Track Name, joining parameters, and the configured application profile. Validation rejects undeclared peers, duplicate route IDs, ambiguous peer and track ownership, self-routes, unsupported or conflicting profiles, cycles, route-count excess, and per-track fan-out excess before any dial or route allocation begins. A dialed moqt-20 identifier maps to the legacy application profile. A moqt-22 route permits legacy fallback, but each upstream and downstream session retains its negotiated codec. Selected identifiers remain visible as closed telemetry values. Only routes owned by the local peer start workers, in deterministic Full Track Name order.
One manager exclusively claims a relay registry, and one worker is the only reconnect owner for one local route. A successor manager can claim the registry only after the prior manager drains and only with a strictly newer topology version; duplicate and out-of-order snapshots fail before dialing. A dial and route setup attempt has a cancellation deadline and exponential backoff with a hard cap. The upstream Dial and Subscribe adapters must cooperate with context cancellation. A violating adapter remains owned and prevents Drained from reporting false completion.
Omitting routes.retry.maxAttempts, or setting it to 0, retries continuously until shutdown. The defaults are a 10-second attempt timeout, a 100-millisecond initial backoff, multiplier 2, and a 30-second backoff cap. Positive maxAttempts values from 1 through 64 opt into a finite limit for one unstable sequence, counting failed setup attempts and short-lived connections. Once a committed route has remained active for at least backoffCap, its next upstream loss starts a fresh sequence with the initial backoff. Time spent dialing, setting up, or draining does not count as stable uptime; rapid reconnect/disconnect cycles retain their accumulated backoff and optional finite limit. A finite sequence that exhausts its limit remains unavailable until the manager is replaced or the process restarts. Existing configurations with a positive limit must set it to 0 or omit it to recover automatically from an arbitrarily long outage.
Each managed outbound native relay connection configures QUIC keepalive and sends MOQT PADDING every max(1ns, min(idleTimeout / 2, 15s)). A PADDING failure closes the managed session. Managed upstream Drained waits for both session drain and keepalive worker exit. This protects an otherwise silent route under normal timer delivery; it is not an application-health signal and cannot extend a shorter remote timeout.
The manager reserves static root routes before their workers start. For a followed presentation, it reserves the current catalog track names before forwarding the catalog update downstream. A request for a known track that is still activating holds one bounded attachment reservation instead of receiving a premature track-missing response. Activation attaches the same request; cancellation, explicit finite retry exhaustion, or shutdown releases the reservation exactly once and returns a bounded protocol outcome. Pending activation never evicts an existing attachment, and it shares the configured per-route attachment ceiling.
An attempt becomes visible only after the upstream subscription and initial fill commit in the relay registry. Upstream loss removes that generation from downstream visibility. The manager starts both route and upstream connection Close before waiting, then waits for the route generation, downstream workers, subscription, and parent connection to drain. Only after that release can its bounded backoff schedule a successor. Stale callbacks remain fenced by the registry generation, so retained objects and queued delivery from different upstream generations cannot mix.
Each catalog connection attempt owns its discovered child workers. Before forwarding a replacement catalog, removed children are fenced and canceled, and their connections, route generations, downstream workers, and plan waiters must drain before capacity is reused. A slow owner delays that presentation's next update without creating an update-worker backlog. Catalog loss closes its children; reconnect reconstructs the desired set from the new catalog. Late callbacks from an older attempt cannot change its successor.
Joining and FILL/FETCH
Late join does not replay an unbounded live queue. The supported joining profile requires the Next Object location filter in the negotiated encoding and FILL_PARAMETERS with Current Group. The publisher sends retained objects captured during preparation on the request's FETCH stream, then continues live delivery on subgroup streams. Other filters, ordering, or fill forms fail closed.
Standalone catalog FETCH supports Current Group (LOCATION_FILTER with only StartGroup=1; parameter value bytes 01 01, with a legacy length prefix or a draft-22 type prefix). It returns the retained full catalog at Object zero and its contiguous deltas through Largest Object. The owner freezes that bounded snapshot during preparation, and FETCH_OK reports its exact End Location. Updates arriving before commit or during delivery do not enter the response. A relay request admitted before the route has observed any catalog object retains one bounded prepared attachment until the first complete snapshot arrives. Cancellation, route replacement, source loss, or shutdown releases that waiter. Once an object or boundary has been observed, a missing full-catalog anchor, unknown gap, incomplete retention, or snapshot above the queue limits rejects the range without an empty success or stale payload. See the pinned draft-21 location filter.
The existing exact initial {0,0} request remains supported, including its bounded wait before that first object commits. It never aliases the latest catalog: after {0,0} leaves retention, that exact request fails. Clients joining after generation changes must request Current Group, or use SUBSCRIBE with Current Group fill. Browser clients must implement the selected draft and these joining semantics; generic WebTransport support alone is insufficient. A client that runs catalog FETCH alongside its joining subscription must deduplicate overlapping objects and apply catalog updates in track order.
The request that opens a FETCH owns its identity. Commit activates delivery; cancellation, source loss, replacement, failure, and shutdown retain the attachment slot until the worker actually drains. A prepared snapshot can be aborted without delivery. A malformed, cancelled, timed-out, over-limit, or failed FETCH affects its request and data stream, without mutating retained history or stopping another track. The MOQT session layer owns the request and data-stream state machine.
Bounded operation and lifecycle
Successful outbound subgroup and FETCH streams refresh the MOQT session idle timeout after all encoded objects have been written and the transport stream has closed successfully. This includes batched subgroup sends. Active delivery can therefore keep a receive-only viewer connected without extending the configured idle duration (one minute by default). Failed, blocked, cancelled, timed-out, or capacity-rejected sends do not refresh that timer. Once valid session activity stops, the idle timeout still closes the connection; late stream completion cannot reactivate a closing or drained session.
The publisher explicitly caps publication work, prepared and active subscriptions, pending output streams, retained catalog data, queued media objects, object payload size, and Close wait time. Drained reports only actual resource release, so a contract-violating callback that blocks forever does not receive a false time-based completion. A backpressured, blocked, or failed subscriber is isolated. It cannot open an unbounded queue or stop the source, another output, or an independent MOQT publication.
One publication has hard ceilings of 256 tracks, 256 retained objects total, and 32 MiB of retained wire data. Each prepared or active subscriber gets the same combined 256-object and 32 MiB initial plus live backlog limits. One publication permits at most 1024 prepared and active attachments, and one session permits at most 1024 outbound data writers. One object payload is at most 16 MiB. The CMSF packager defaults to 256 timeline entries, 256 KiB of timeline data, 1 MiB of codec state, and 8 prepared batches. The cache configuration accepts smaller deployment values. These are defensive limits, not capacity or performance claims.
Attachment uses two phases. Preparation reserves the publication before the request response commits. Commit binds a sender only after a successful response. Abort releases an uncommitted reservation exactly once. Close rejects new work, stops publisher-owned writers, and waits for the bounded drain path. A Close timeout is not completion while a live writer owns resources; the eventual Drained signal remains authoritative.
The publisher retains only data required by the profile joining and catalog contract. It does not promise DVR, arbitrary historical seeking, lossless replay after eviction, or recovery after process restart. Capacity exhaustion, invalid codec state, namespace rejection, or a missing mandatory random-access boundary produces a bounded fail-closed protocol outcome for the affected request. Valid media that arrives before complete catalog readiness is dropped as described above instead of closing the publication.
Diagnostics and security boundary
MOQT diagnostics use fixed carrier, role, lifecycle, operation, outcome, and reason vocabularies plus process-aggregate counters, gauges, and durations. Publisher packaging failures also retain bounded source-event, pipeline-stage, and cause rows after publication drain. The event vocabulary is none, track_snapshot, codec_state, anchor, discontinuity, media_sample, or unknown. Stages and causes use closed publisher-owned vocabularies, and invalid values collapse to unknown. The detailed counter is exported as dispergo_moqt_publisher_packaging_failures_total; the existing aggregate packaging failure counter remains available.
Metrics and aggregate status diagnostics do not expose namespace bytes, track names, stream IDs, remote addresses, principals, tokens, request payloads, CMAF bytes, catalog JSON, or unbounded error strings. The presentations endpoint identifies a row only by its operator-controlled presentation ID. It does not expose namespaces, track names, route IDs, peer IDs, addresses, generations, errors, credentials, or payload data. Packaging detail has a hard 256-row process snapshot bound. Snapshot collection cannot wait for packager callbacks or delay Publish, Close, or Drained ownership.
Authorization covers request-level MOQT permissions; it is not a general tenant or DRM system. The presence of publisher, relay, topology, or route-manager packages opens no listener without explicit output.moqt configuration. Diagnostics expose neither route nor peer identity.
Operator certificate, client CA, and token configuration is read from files or process configuration, so changing it requires restart. Server does not watch these files or reload them during runtime.
With MOQT enabled, read-only GET /api/v1/moqt exposes identity-free starting, accepting, draining, or drained state, active and configured session limits, and aggregate admission and authorization counts. GET /api/v1/moqt/presentations returns schema version 1 with publisher and relay rows sorted by presentation ID. Publisher state is configured, active, or closing. Relay catalog state is missing, activating, available_empty, fetchable, retention_lost, draining, or unavailable. configuredRoutes counts the current required routes, including catalog-discovered tracks and excluding retired configured names in automatic mode. readyToPull is true only when every required current relay route is available and the current catalog has a complete retained snapshot within queue limits for Current Group catalog FETCH. The value is advisory and does not reserve authentication, session admission, attachment capacity, or later request admission. activeSubscriptions counts committed SUBSCRIBE delivery workers. activeFetches separately counts committed FETCH delivery workers. Both counts include a retiring worker until its sender call and worker ownership have completed. The response contains at most 256 publisher rows and 256 relay rows, for a 512-row global hard bound. GET /api/v1/features reports only redacted capability. /metrics exports fixed-label MOQT metric families, including session drain duration and count. dispergo_moqt_publisher_subscribers and dispergo_moqt_relay_subscribers report prepared and active SUBSCRIBE workers. The corresponding dispergo_moqt_publisher_fetches and dispergo_moqt_relay_fetches families report the same bounded phases for standalone FETCH work. No surface returns listener addresses, peers, routes, namespaces, or secrets.
Draft-22 acceptance
Draft-22 changes control-message Location Filter encoding without changing
MSF-01, CMSF-01, admission, retention, or shutdown ownership. The codec accepts
all six filter types. Application delivery still supports the bounded joining
and catalog FETCH profiles described above; arbitrary ranges remain unsupported.
An explicit draft-22 Absolute Start at {0,0} is distinct from Next Object.
FETCH End Location is inclusive: the frozen full catalog and deltas include
that last object, without including later arrivals.