captureMicrophone runs a loopback RTCPeerConnection to drive the
encoder. It taps the encoded Opus frames with an RTCRtpScriptTransform
and publishes each frame as a MoQT object over QUIC/WebTransport. The path
uses no ICE, no DTLS, no SRTP, and no SFU.
This is the browser capture path shipped in the TypeScript SDK
(
@@clutchcall/sdk moqt/audio.ts). The same technique also underpins the
LiveKit migration shim. See
Migrating from WebRTC / LiveKit.Why divert instead of transport
A conventional WebRTC call couples three things into one bundle: the capture + encode graph (useful), the peer transport (ICE/DTLS/SRTP — you do not need it when you own a QUIC relay), and an SFU to fan media out (a whole server you would rather not run). The diversion unbundles them:- Keep the capture graph.
getUserMediastill runs AEC/AGC/noise suppression; the browser’s Opus encoder still produces standard 48 kHz Opus. - Drop the transport. The encoded frames never touch ICE or SRTP. They go directly onto your QUIC media plane.
- Replace the SFU with a MoQT relay: publish/subscribe fan-out on the same
single-
:443QUIC plane the rest of the stack uses.
RTCPeerConnection exists only to make the encoder run. Its far
end is a second in-page peer connection. The SDK discards that connection’s
decoded output; it is a sink that keeps the sender pipeline live.
Divert in one call
1
Connect a media session
Open a MoQT client against your relay and require the encoded-frame transform.
The session then refuses to start on a browser that cannot uphold the rule.
2
Publish an audio track
A publication is just a namespace + name on the MoQT plane. Use the caller’s
call_sid to scope the track so the rest of the stack can find it.3
Capture and divert
One call wires the loopback encoder to the publication. The SDK writes every
encoded Opus frame to the track as the encoder produces it.
captureMicrophone acquires the mic, starts the
loopback peer connections, installs the transform, and completes the loopback
SDP so the encoder starts. You never touch WebCodecs or the WebRTC API
directly.
What happens inside captureMicrophone
The interesting mechanics are the loopback graph and the transform worker. This
is the shipped implementation, trimmed to the load-bearing lines:
- Capture graph (main thread)
- Transform (worker)
The cost of diverting
Diversion of encoded frames onto QUIC instead of SRTP/UDP adds a JS-layer hop: frame → worker → main thread → MoQT write. What does that cost? A prototype that proved the technique measured the JS-layer round trip on localhost Chrome against a plain WebRTC echo:An internal prototype measured the +0.36 ms delta
(localhost Chrome, checked audibly end-to-end against a Node echo server). It
proved the diversion cost is negligible relative to a full voice turn. It is
a prototype figure, not a production SLA. The production version is the
shipped SDK path above.
Playing audio back
The reverse direction is symmetric. Subscribe to the downlink track and hand each Opus frame to the SDK’sOpusPlayer. The player decodes with WebCodecs
and renders through an AudioWorklet ring buffer. See
Browser Audio Capture
for the full capture-and-playback loop.
Related
RTCRtpScriptTransform
The encoded-frame tap in detail — the W3C Encoded Transform API and the worker.
Browser Compatibility
Where the transform ships, and why the SDK refuses to capture when it doesn’t.
WebTransport
The browser-native QUIC plane the diverted frames ride on.
Migrating from WebRTC
Move existing WebRTC and LiveKit clients onto the QUIC plane.

