Overview
MOQTransport carries a session over Media over QUIC, moving audio and RTVI messages over QUIC instead of a WebRTC stack. It runs in two modes:
- Serve mode (the default) — the bot binds its own UDP socket and the browser dials it. No relay process to run, and a self-signed certificate is minted for local development.
- Client mode — the bot and the browser both dial a relay and rendezvous there. Neither side needs a reachable address, so this works when the bot is behind NAT. A dropped relay session is redialed rather than treated as the end of the call (see Reconnect and errors).
Example Implementation
Runnable bot covering both modes
Media over QUIC
The protocol and its reference relay
Installation
Serve mode vs client mode
Serve mode is the shorter path for local development: nothing to run besides the bot.[::]:4080, mints a self-signed certificate for localhost, and publishes its SHA-256 fingerprint so the browser can pin it.
Client mode is selected by naming a relay:
Broadcast paths
Each side publishes on one path and subscribes to the other’s. By default they’re composed from the namespace and the two participant ids, which are named by direction:response_path and request_path to bypass the namespace entirely and give the paths directly. That suits a deployment where the paths are assigned externally — a host running one bot per caller, naming both paths after an id the caller minted, with no namespace for the two sides to agree on beforehand. Either can be set alone; the other still derives from the namespace.
Configuration
MOQTransport
str
default:"localhost"
Host used to compose the relay URL when
params.relay_url is unset.int
default:"4080"
Port used to compose the relay URL, and the serve-mode listen port when
params.bind is unset.str
default:"/moq"
Path used to compose the relay URL.
str | None
default:"None"
Optional name for the input transport processor.
str | None
default:"None"
Optional name for the output transport processor.
MOQParams
ExtendsTransportParams, so the standard audio, VAD, and turn-analyzer options apply too.
Connection
Paths
Client-side TLS (client mode only)
client_tls_roots and client_tls_fingerprints are alternatives to turning
verify_ssl off, not companions to it. Reach for them when a relay uses a
private CA or a self-signed certificate, and leave verification on.
Audio
Reconnect and errors
In client mode a dropped relay session is redialed rather than reported as the peer leaving. Each dial announces the bot’s broadcast afresh on the same path, with the transcript so far replayed into it, so the peer sees the same session return, and the transport retries with a delay that doubles from 0.5 s up to 2 s.connection_timeout bounds the whole outage: it counts from the session dropping until the peer’s data flows again, and a redial that succeeds does not restart it, because a relay keeps announcing a path whose route has died, so an announcement alone is not the peer being back. A relay that vanishes without closing the session is noticed by a traffic-stall watchdog, well before QUIC’s idle timeout would report it. Only when the time is up does the transport fire on_client_disconnected for a peer it had seen, so the usual handler ends the call.
Each relay session is a connect and a disconnect of its own: on_connected fires for the first session and every redialed one, and on_disconnected when a session ends. A redial therefore shows up as a disconnect followed by a connect, so a bot ends the call from on_client_disconnected, which fires only once the peer is gone. Set connection_timeout longer than a load balancer in front of the relays takes to fail a dead one out, since until then redials can be pinned to the dead target.
Each side appends a session-ending marker to its transcript stream before it leaves. The peer’s tracks ending after that marker is a hangup, and the transport reports the client gone at once. Ending without it is also what a failed relay between the peers looks like, so the transport redials and gives the peer connection_timeout to appear on the new session, reporting it gone only if it does not. A client that closes without sending the marker, a tab killed outright for instance, is reported gone within connection_timeout of its broadcast disappearing.
A relay that refuses the token is not retried. The relay accepts the QUIC connection first and then closes the session as unauthorized, so a forged token fails on the first session and an expired one ends the call at its expiry, both without redials.
Failures reach the pipeline as an ErrorFrame from the input transport, in addition to the on_error event: a refused token carries an authentication or authorization category, and a relay that could not be reached within connection_timeout carries a connectivity category marked permanent. Either leaves the transport unable to do its job, and the pipeline worker’s processor_unusable_policy decides whether the pipeline ends. The default, CONTINUE, only logs, so a bot that should exit on a dead relay sets the policy to END or CANCEL, or acts on on_error itself.
Transcript records
Each transcript track is a single group that a subscriber reads from its first record, so a reconnect on either side replays the whole log. Every record the transport writes is the RTVI message plus two fields:seq, the record’s position in the log, counting across reconnects, and epoch, an opaque string identifying this transport instance. On subscribe the transport drops records at or below the last seq it accepted for the current epoch, treats a different epoch as a new peer whose count starts over, and strips both fields before the message enters the pipeline. A record without seq is delivered unchanged, so a client that predates the fields keeps working.
@pipecat-ai/moq-transport implements the same format. Another client on this stream should too; otherwise a reconnect redelivers client-ready and the bot greets again.
Properties
cert_fingerprints
Usage
create_transport builds this for you:
MOQRunnerArguments takes either a host and port to compose the relay URL from, or a full relay_url, query string included, which create_transport passes through unchanged. A host that hands the bot a relay URL with a token in it uses the second form:
Runner options
The
/start response carries a moq block — relayUrl, certHash, serve, namespace, clientId, botId, and transcriptTrack — which is what the browser needs to join. The prebuilt client UI shipped with the runner extra speaks MoQ, so http://localhost:7860 can connect without any client code of your own.
Event Handlers
Notes
- RTVI over MoQ: the
transcript_trackis a lossless, ordered JSON stream carrying RTVI messages in both directions, so MoQ is a full RTVI transport on par with Daily or WebSocket rather than an audio-only path. - Audio: a single Opus track each way. The library resamples to the nearest Opus-supported rate before encoding, so
audio_out_sample_ratedoesn’t have to be one of them. - Latency:
audio_in_max_latency_mstrades interactivity against resilience — lower waits less for a late frame, at the cost of more drops on a poor network.