You already trust the browser’s WebRTC audio pipeline: echo cancellation, gain control, noise suppression, and a proven Opus encoder. The ClutchCall Voice SDK keeps all of that. It removes only the part you do not want in a real-time voice product: WebRTC’s own transport. One call to 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. getUserMedia still 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-:443 QUIC plane the rest of the stack uses.
The loopback 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.
That is the whole client. 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:
The worker-based transform is the only encoded-frame path. The legacy main-thread createEncodedStreams() branch was removed. Every frame goes through one worker, which lets the same plane carry frame-level end-to-end encryption later — then even a relay only ever sees ciphertext. If RTCRtpScriptTransform is absent, the SDK refuses to capture, and MoqtClient.connect refuses to connect when requireEncodedTransform is set. It does not silently fall back to an insecure path. See Browser Compatibility for the fallback status.

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’s OpusPlayer. The player decodes with WebCodecs and renders through an AudioWorklet ring buffer. See Browser Audio Capture for the full capture-and-playback loop.

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.