transport
MOQ (Media over QUIC) transport implementation for Pipecat.
Uses the upstream moq Python library
(moq-rs) for the QUIC connection,
MOQ session, announcement discovery, subscription routing, group/frame
framing, codec-specific catalog management, and Opus encode/decode +
resampling for raw audio tracks. This module just wires it into the
pipecat Frame pipeline.
Each participant publishes under a per-participant broadcast path
<namespace>/<participant_id> (e.g. pipecat/bot0); the bot
subscribes to the peer at <namespace>/<peer_id>. Audio rides on a
single Opus track; RTVI JSON rides on a fixed-name transcript.json.z
track carried by moq’s JSON stream helper (publish_json_stream /
subscribe_json_stream). The stream is an ordered, lossless append-log
of records — every message is delivered in order, unlike the JSON
snapshot helper (publish_json / subscribe_json) which collapses
to the latest value and would drop RTVI events a slow consumer fell
behind on. Compression is enabled (hence the .z suffix). The
transcript is a side-channel, like moq-boy’s status/command
tracks: it deliberately bypasses the catalog (which only describes media
renditions), so the browser reads it by the well-known name rather than
by catalog discovery.
Both directions carry the same shape: the bot publishes bot-side RTVI
events on its own transcript track and subscribes to the client’s
transcript track for client-side traffic (client-ready for
protocol negotiation, typed text input, function-call results, etc).
This makes MoQ a full bidirectional RTVI transport, on par with the
Daily and WebSocket transports.
Two modes:
Server mode (
serve=True, currently the only supported mode): the bot binds its own UDP socket viamoq.Serverand accepts the browser’s direct connection. Removes the need for a separatemoq-relayprocess for local dev. The self-signed cert fingerprints are exposed viaMOQTransport.cert_fingerprintsso a browser can pin them.Client mode (default): MoQ client mode is not yet supported. The bot would dial a relay at
relay_url(or the constructor’shost/port/path); transport-level wiring exists but the runner blocks this mode at arg-parse time until the cert-fingerprint plumbing to the browser is finished and we’ve validated the flow against an external relay. Seepipecat.runner.moq._validate_moq_args()for the guard.
- class pipecat.transports.moq.transport.MOQTrackType(*values)[source]
Bases:
IntEnumTypes of media tracks the transport surfaces in event callbacks.
- AUDIO = 1
- VIDEO = 2
- DATA = 3
- class pipecat.transports.moq.transport.MOQTrack(broadcast_path: str, name: str, track_type: MOQTrackType = MOQTrackType.DATA)[source]
Bases:
objectIdentifies a MOQ track for event callbacks.
- Parameters:
broadcast_path – The full broadcast path (e.g.
pipecat/bot0).name – The track name (e.g.
bot-audio).track_type – The track media type.
- broadcast_path: str
- name: str
- track_type: MOQTrackType = 3
- property full_name: str
Get the full track identifier.
- class pipecat.transports.moq.transport.MOQParams(*, audio_out_enabled: bool = False, audio_out_sample_rate: int = 24000, audio_out_channels: int = 1, audio_out_bitrate: int = 96000, audio_out_10ms_chunks: int = 4, audio_out_mixer: Mapping[str | None, ~pipecat.audio.mixers.base_audio_mixer.BaseAudioMixer] | None=None, audio_out_destinations: list[str] = <factory>, audio_out_end_silence_secs: int = 2, audio_out_auto_silence: bool = True, audio_in_enabled: bool = False, audio_in_sample_rate: int = 16000, audio_in_channels: int = 1, audio_in_filter: BaseAudioFilter | None = None, audio_in_stream_on_start: bool = True, audio_in_passthrough: bool = True, video_in_enabled: bool = False, video_out_enabled: bool = False, video_out_is_live: bool = False, video_out_width: int = 1024, video_out_height: int = 768, video_out_bitrate: int | None = None, video_out_framerate: int = 30, video_out_color_format: str = 'RGB', video_out_codec: str | None = None, video_out_destinations: list[str] = <factory>, relay_url: str | None = None, namespace: str = 'pipecat', participant_id: str = 'bot0', peer_id: str = 'client0', audio_out_track: str = 'bot-audio', transcript_track: str = 'transcript.json.z', verify_ssl: bool = True, connection_timeout: float = 30.0, serve: bool = False, serve_bind: str | None = None, serve_tls_host: str = 'localhost', serve_tls_cert: str | None = None, serve_tls_key: str | None = None, audio_in_max_latency_ms: int = 500, audio_out_frame_ms: int = 20, audio_out_max_buffer_ms: int = 25000)[source]
Bases:
TransportParamsConfiguration parameters for MOQ transport.
- Parameters:
relay_url – Full relay URL (e.g.
https://relay.example.com:4080/moq). If unset, the transport composes one from the constructor’shost/port/path. Ignored in serve mode.namespace – Top-level namespace shared by all participants.
participant_id – This bot’s id; the bot publishes under
<namespace>/<participant_id>.peer_id – The id of the peer (browser/client) the bot subscribes to:
<namespace>/<peer_id>.audio_out_track – Name of the bot’s outgoing audio track.
transcript_track – Name of the bot’s outgoing transcript track. A fixed-name JSON stream track carrying RTVI messages as a lossless, ordered append-log (moq’s
publish_json_stream/subscribe_json_stream, compression on), discovered by convention rather than via the catalog.verify_ssl – Verify the relay’s TLS certificate. Client mode only.
connection_timeout – Seconds to wait for the peer broadcast to be announced before giving up.
serve – When
True, the bot binds its own UDP socket and accepts incoming MOQ sessions instead of dialing a relay.serve_bind – Address to bind in serve mode (e.g.
"[::]:4080"). Defaults to"[::]:<port>"based on the constructor’s port.serve_tls_host – Hostname to use in the generated self-signed certificate when
serve_tls_cert/serve_tls_keyaren’t provided. The browser pins this cert via its SHA-256 fingerprint, so the value just needs to match what the browser sees in the URL (typically"localhost").serve_tls_cert – Path to a PEM-encoded TLS certificate chain. If unset alongside
serve_tls_key, a self-signed cert is generated on startup.serve_tls_key – Path to the PEM-encoded private key matching
serve_tls_cert.audio_out_sample_rate – Sample rate the bot publishes its audio at (Hz). Pipecat hands us audio at whatever rate the TTS produces; the library resamples to the nearest Opus-supported rate before encoding.
audio_in_sample_rate – Sample rate the pipeline expects to receive user audio at (Hz). Decoded Opus is resampled to this rate before being pushed downstream.
audio_in_max_latency_ms – How long
subscribe_audio()will wait for a late frame before skipping ahead. Lower = more interactive (fewer fills, more drops on bad networks); higher = smoother audio with more glass-to-glass delay.audio_out_frame_ms – Opus frame duration for the bot’s audio output. Must be 2, 5, 10, 20, 40, or 60. 20 ms is the real-time default.
audio_out_max_buffer_ms – How far ahead of real-time the bot is allowed to write audio. The bot writes TTS faster than real-time with future-dated timestamps so the browser player (
@moq/watch) can buffer and play at the encoded pace; this pacespublish_audioso the in-flight buffer never grows past this many milliseconds. Keep it a little under the player’s buffer ceiling (MoqTransportOptions.audioBufferMaxMs, 30s) so the producer self-limits below the player’s drop ceiling and the player never has to drop. On interruption the pacing clock is re-anchored (seereset_audio_pacing()) so the next utterance isn’t delayed by the previous buffer.
- relay_url: str | None
- namespace: str
- participant_id: str
- peer_id: str
- audio_out_track: str
- transcript_track: str
- verify_ssl: bool
- connection_timeout: float
- serve: bool
- serve_bind: str | None
- serve_tls_host: str
- serve_tls_cert: str | None
- serve_tls_key: str | None
- audio_out_sample_rate: int
- audio_in_sample_rate: int
- audio_in_max_latency_ms: int
- audio_out_frame_ms: int
- audio_out_max_buffer_ms: int
- class pipecat.transports.moq.transport.MOQCallbacks(*, on_connected: Callable[[], Awaitable[None]], on_disconnected: Callable[[], Awaitable[None]], on_client_connected: Callable[[], Awaitable[None]], on_client_disconnected: Callable[[], Awaitable[None]], on_track_subscribed: Callable[[MOQTrack], Awaitable[None]], on_error: Callable[[str, Exception], Awaitable[None]], on_audio_received: Callable[[bytes, int], Awaitable[None]], on_message_received: Callable[[dict], Awaitable[None]])[source]
Bases:
BaseModelCallback handlers bridging
MOQTransportClienttoMOQTransport.Decouples the moq-rs session/connection logic from pipecat’s Frame pipeline:
MOQTransportClientonly ever calls these, it never reaches intoMOQInputTransport/MOQOutputTransportor pipecat frame types directly.- Parameters:
on_connected – Called when the MOQ session (client or server) is established.
on_disconnected – Called when the session ends.
on_client_connected – Called when the peer’s broadcast is announced.
on_client_disconnected – Called when the peer’s broadcast goes away.
on_track_subscribed – Called when a remote track subscription succeeds.
on_error – Called when the underlying transport errors.
on_audio_received – Called with decoded mono PCM audio from the peer’s audio track.
on_message_received – Called with each RTVI message from the peer’s transcript stream.
- on_connected: Callable[[], Awaitable[None]]
- on_disconnected: Callable[[], Awaitable[None]]
- on_client_connected: Callable[[], Awaitable[None]]
- on_client_disconnected: Callable[[], Awaitable[None]]
- on_error: Callable[[str, Exception], Awaitable[None]]
- on_audio_received: Callable[[bytes, int], Awaitable[None]]
- on_message_received: Callable[[dict], Awaitable[None]]
- class pipecat.transports.moq.transport.MOQTransportClient(params: MOQParams, url: str, serve_bind: str, callbacks: MOQCallbacks)[source]
Bases:
objectOwns the moq-rs QUIC session/connection for a single MOQ transport.
Mirrors the
<Provider>TransportClientpattern used by the other transports (e.g.DailyTransportClient,SmallWebRTCClient): it owns dialing/serving, the publish broadcast, and forwarding the peer’s audio/transcript, and talks back toMOQTransportonly throughcallbacks— it has no knowledge of pipecat Frames or the input/output transports.- __init__(params: MOQParams, url: str, serve_bind: str, callbacks: MOQCallbacks)[source]
Initialize the MOQ transport client.
The publish broadcast and its transcript track are created here, synchronously, so the output processor can begin writing into them before the async session bring-up in
_run()finishes — otherwise the bot’s first ~hundreds of ms of audio gets silently dropped while we’re still dialing the relay.- Parameters:
params – MOQ configuration parameters.
url – Full relay URL to dial in client mode.
serve_bind – Address to bind in serve mode.
callbacks – Event/data callbacks back to the owning transport.
- async setup(setup: FrameProcessorSetup)[source]
Capture the task manager from the input/output processors.
Both processors forward their setup here; we only need to store the reference once since they share the same task manager.
- connect()[source]
Register a holder and start the MOQ session on the first call.
Called by both
MOQInputTransport.start()andMOQOutputTransport.start(), aftersetup()has wired in the task manager. Only the first call actually dials/serves; later calls just record another holder, balanced againststop()/cancel()releasing it.
- open_audio_track(sample_rate: int)[source]
Open the bot’s audio track via
publish_audio.Called by
MOQOutputTransport.startonce it knows the pipeline’s output sample rate. No-op if the track is already open or audio output is disabled.
- reset_audio_pacing()[source]
Re-anchor the publish_audio pacing clock to wall-clock now.
Called from
MOQOutputTransport.process_frameon InterruptionFrame so the next utterance plays immediately instead of waiting for the (now-cancelled) previous one to finish in pacing time.Part of the publish_audio pacing workaround; goes away once moq-rs exposes a flush primitive on
AudioProducer. Seepublish_audio().
- async publish_audio(audio: bytes)[source]
Push a PCM chunk to the bot’s audio track with real PTS, paced to a cap.
The library does the Opus encode + resample inside the FFI, so we just write S16 PCM bytes.
Each chunk is stamped with a monotonic presentation timestamp so the browser player (
@moq/watch) can buffer the future-dated frames and play them at the encoded pace.AudioProducer.write()is fire-and-forget, so to bound how far ahead the bot runs we pace the writes against a virtual clock: each call advances the clock by the chunk’s audio duration, and we sleep until wall-clock is withinaudio_out_max_buffer_ms(25s) of it. That keeps the in-flight buffer a little under the player’s drop ceiling (MoqTransportOptions.audioBufferMaxMs, 30s), so the producer self-limits below the consumer’s cap. Interruptions are flushed on the browser side (reset()onuser-started-speaking); the pacing clock is re-anchored here (seereset_audio_pacing()) so the next utterance isn’t delayed by the previous buffer.
- async wait_for_audio_drain(jitter_buffer_margin_s: float = 0.3) None[source]
Block until the pacing clock catches up to wall-clock, then a bit more.
publish_audiopaces each write against_publish_audio_clock, which tracks the presentation-time deadline of the last chunk written to the moq audio stream. When wall-clock reaches that value, the last audio chunk has (approximately) been consumed by the browser’s player. Add a small margin for the client-side jitter buffer to drain past the final sample before we signal session-ending and tear the transport down.No-op when no audio has been published yet or the clock has already caught up. Capped at
audio_out_max_buffer_msas a safety net so a bogus clock value can’t stall shutdown.
- publish_transcript(message)[source]
Append an RTVI message to the transcript JSON stream.
messageis a JSON-serializable value (the RTVI message dict); the stream helper serializes and frames it, appending one record to the ordered log so no message is dropped.
- property cert_fingerprints: list[str]
SHA-256 fingerprints (hex) of the serving cert (server mode only).
Populated once the bot has bound; empty in client mode or before
_run()has reached the listen step. Useful for telling a browser client which self-signed cert to pin.
- async disconnect()[source]
Disconnect from the MOQ relay.
Sends an intra-transport
session-endingnotification on the transcript stream before tearing anything down. This gives the browser-side MoQ transport a chance to disable auto-reconnect, drain its jitter buffer, and close the audio decoder cleanly — without that heads-up, WebTransport just vanishes underneath Chrome’s WebCodecs decoder and can crash the renderer (“Aw snap”).
- async cleanup()[source]
Unconditionally tear down the underlying MOQ connection.
Called as a last-resort safety net from
MOQInputTransport.cleanup/MOQOutputTransport.cleanup(the FrameProcessor lifecycle hook, always invoked regardless of whetherstop()/cancel()already released the session), so we guard against repeating the work. Preferstop()/cancel()to end the session during normal shutdown — those wait for every holder to release before tearing anything down.
- async stop()[source]
Release this holder’s claim on the session, draining buffered audio first.
Called by both
MOQInputTransport.stop()andMOQOutputTransport.stop(). Safe regardless of which one runs first: the session only actually disconnects once both have released their claim, and by the time the last one does, every audio frame written by the output side has already been drained. Draining twice (once per caller) is harmless — the second call just observes the clock has already caught up.
- async cancel()[source]
Release this holder’s claim on the session immediately, without draining.
Called by both
MOQInputTransport.cancel()andMOQOutputTransport.cancel(): cancellation is a hard stop, so there’s nothing to drain, and the session disconnects as soon as both have released their claim.
- class pipecat.transports.moq.transport.MOQInputTransport(client: MOQTransportClient, params: MOQParams, **kwargs)[source]
Bases:
BaseInputTransportMOQ input transport: subscribes to the peer’s audio track.
- __init__(client: MOQTransportClient, params: MOQParams, **kwargs)[source]
Initialize the MOQ input transport.
- Parameters:
client – MOQTransportClient instance managing the MoQ session.
params – MOQ transport configuration parameters.
**kwargs – Additional arguments passed to the parent class.
- async setup(setup: FrameProcessorSetup)[source]
Forward setup to the shared MOQTransportClient so it can create tasks.
- Parameters:
setup – Configuration object containing setup parameters.
- async start(frame: StartFrame)[source]
Auto-connect to the MOQ relay when the pipeline starts.
- Parameters:
frame – The start frame containing initialization parameters.
- async stop(frame: EndFrame)[source]
Stop the MOQ input transport.
Releases this transport’s claim on the shared session (see
MOQTransportClient.stop()).EndFramereaches this (upstream) input transport before it reachesMOQOutputTransportdownstream, so this call alone doesn’t end the session — the client waits for the output transport to release its own claim (after draining its buffered audio) before actually disconnecting.- Parameters:
frame – The end frame signaling transport shutdown.
- async cancel(frame: CancelFrame)[source]
Cancel the MOQ input transport.
Releases this transport’s claim on the shared session. Unlike
stop(), cancellation is immediate — there’s no audio to drain.- Parameters:
frame – The cancel frame signaling immediate cancellation.
- async push_received_audio(audio: bytes, sample_rate: int)[source]
Push a received audio frame downstream.
- Parameters:
audio – Raw mono 16-bit PCM audio bytes.
sample_rate – Sample rate of
audio, in Hz.
- async push_received_message(message: dict)[source]
Push a received RTVI message to
RTVIProcessorupstream.Called by
MOQTransportfor every record delivered on the peer’s transcript stream.RTVIProcessorsits upstream of this input transport (it’s prepended byPipelineWorkerwhenenable_rtvi=True), so pushing upstream is the direct route.- Parameters:
message – The parsed RTVI message.
- class pipecat.transports.moq.transport.MOQOutputTransport(client: MOQTransportClient, params: MOQParams, **kwargs)[source]
Bases:
BaseOutputTransportMOQ output transport: writes audio + transcript frames to MOQ tracks.
- __init__(client: MOQTransportClient, params: MOQParams, **kwargs)[source]
Initialize the MOQ output transport.
- Parameters:
client – MOQTransportClient instance managing the MoQ session.
params – MOQ transport configuration parameters.
**kwargs – Additional arguments passed to the parent class.
- async setup(setup: FrameProcessorSetup)[source]
Forward setup to the shared MOQTransportClient so it can create tasks.
- Parameters:
setup – Configuration object containing setup parameters.
- async start(frame: StartFrame)[source]
Start the MOQ output transport.
- Parameters:
frame – The start frame containing initialization parameters.
- async stop(frame: EndFrame)[source]
Stop the MOQ output transport.
On graceful end (an
EndFrame— pushed byEndWorkerFramefrom anend_calltool, for example) let the moq audio buffer drain fully before releasing this transport’s claim on the shared session (seeMOQTransportClient.stop()). Without this the bot’s final TTS utterance gets clipped: base_output’s stop() unblocks as soon as every audio frame has been written to moq’s pacing queue, but moq buffers up toaudio_out_max_buffer_ms(25s) of paced audio that still has to reach the browser and drain the client jitter buffer before it actually plays.- Parameters:
frame – The end frame signaling transport shutdown.
- async cancel(frame: CancelFrame)[source]
Cancel the MOQ output transport.
Releases this transport’s claim on the shared session. Unlike
stop(), cancellation is immediate — there’s no audio to drain.- Parameters:
frame – The cancel frame signaling immediate cancellation.
- async process_frame(frame: Frame, direction: FrameDirection)[source]
Reset the publish_audio pacing clock on interruption.
The pacing in
write_audio_framekeeps moq’s in-flight buffer bounded so InterruptionFrame can actually stop playback in the browser — but the pacing clock needs to be re-anchored tonowso the next utterance plays immediately instead of catching up.- Parameters:
frame – The frame to process.
direction – The direction of frame flow in the pipeline.
- async send_message(frame: OutputTransportMessageFrame | OutputTransportMessageUrgentFrame)[source]
Publish a transport message (RTVI JSON) on the transcript stream.
- Parameters:
frame – The transport message frame to send.
- async write_audio_frame(frame: OutputAudioRawFrame) bool[source]
Write an audio frame to the bot’s audio track.
- Parameters:
frame – The output audio frame to write.
- Returns:
True if the audio frame was written successfully, False otherwise.
- class pipecat.transports.moq.transport.MOQTransport(params: MOQParams, host: str = 'localhost', port: int = 4080, path: str = '/moq', input_name: str | None = None, output_name: str | None = None)[source]
Bases:
BaseTransportMOQ transport that connects to a MOQ relay via the
moqlibrary.Example:
transport = MOQTransport( params=MOQParams( audio_in_enabled=True, audio_out_enabled=True, namespace="my-room", ), host="localhost", port=4080, ) @transport.event_handler("on_connected") async def on_connected(transport): print("Connected to MOQ relay")
Event handlers available:
on_connected— connection to relay establishedon_disconnected— connection lost / closedon_client_connected— peer broadcast announced (client joined)on_client_disconnected— peer broadcast went awayon_track_subscribed— remote track subscription succeededon_error— error in the underlying transport
- __init__(params: MOQParams, host: str = 'localhost', port: int = 4080, path: str = '/moq', input_name: str | None = None, output_name: str | None = None)[source]
Initialize the MOQ transport.
- Parameters:
params – MOQ configuration parameters.
host – Relay host address.
port – Relay port number.
path – MOQ endpoint path on the relay.
input_name – Optional name for the input processor.
output_name – Optional name for the output processor.
- input() MOQInputTransport[source]
Get the input transport for receiving media.
- output() MOQOutputTransport[source]
Get the output transport for sending media.
- property cert_fingerprints: list[str]
SHA-256 fingerprints (hex) of the serving cert (server mode only).
Populated once the bot has bound; empty in client mode or before the session bring-up has reached the listen step. Useful for telling a browser client which self-signed cert to pin.
- async disconnect()[source]
Disconnect from the MOQ relay.
See
MOQTransportClient.disconnect()for details.