getUserMedia runs echo
cancellation, gain control, and noise suppression, the browser’s Opus encoder
produces clean 48 kHz frames, and everything is glued together with an
RTCPeerConnection that negotiates ICE/DTLS/SRTP and pushes media to an SFU.
Migrating to TeleQuick Voice keeps the good half — capture and encode — and
throws away only the transport. One call to captureMicrophone runs a loopback
RTCPeerConnection to drive the same encoder, taps the encoded Opus frames
with an RTCRtpScriptTransform, and publishes each frame as a MoQT object over
QUIC/WebTransport. No signaling server, no ICE, no DTLS, no SRTP, no SFU.
This is the browser capture path shipped in the TypeScript SDK
(
@@telequick/sdk moqt/audio.ts). It runs today in Chromium, Edge, Safari,
and Firefox. Mobile (Swift / Kotlin) diversion is planned, not shipped — see
Mobile Apps.What migrates and what doesn’t
The unbundling is the whole point. A conventional WebRTC call ties three things together; the diversion keeps one and drops two.| Piece of your WebRTC stack | Migrates? | Becomes |
|---|---|---|
getUserMedia + AEC/AGC/noise suppression | Keep | Unchanged — the browser still runs it |
| Browser Opus encoder | Keep | Unchanged — standard 48 kHz Opus frames |
| ICE / DTLS / SRTP peer transport | Drop | Encoded frames ride your QUIC media plane instead |
| Signaling server (offer/answer exchange) | Drop | A MoQT publish is just a namespace + name |
| SFU (media fan-out) | Replace | A MoQT relay on the same single-:443 plane |
Before and after
The change is almost entirely subtractive. The tabs below show a representative “before” (a raw WebRTC client publishing to an SFU) and the “after” on the QUIC plane.- Before — raw WebRTC to an SFU
- After — diverted onto QUIC
captureMicrophone acquires the mic, spins up the loopback peer connections,
installs the transform, and completes the loopback SDP so the encoder starts —
you never touch WebCodecs or the raw WebRTC API directly. The full mechanics of
the loopback graph and the worker transform live in
One-Line WebRTC Diversion.
Migrate in three moves
Point capture at a publication instead of a peer connection
Wherever your old code called
pc.addTrack(...) and negotiated with a
signaling server, connect a MoQT client and open a publication instead. Set
requireEncodedTransform: true so a browser that can’t uphold the
encoded-frame rule fails the connection rather than the capture.If you already hold a processed MediaStreamTrack — a track you run through
your own graph — hand it to the SDK instead of letting it call getUserMedia:Replace playback with the downlink track
The reverse leg is symmetric. Instead of attaching a remote
MediaStreamTrack
to an <audio> element, subscribe to the downlink track and hand each Opus
frame to the SDK’s OpusPlayer, which decodes with WebCodecs and renders
through an AudioWorklet ring buffer. The
Browser Audio Capture
page has the full capture-and-playback loop.If you also run a WebRTC media server
The diversion covers the client. If part of your deployment terminates WebRTC on the server — decrypting DTLS-SRTP and handling ICE in a media process — the stack ships a server-side WebRTC termination path as a fallback leg for endpoints that can’t run the diversion (for example, a browser without the encoded transform, or a third party you don’t control). It sits alongside the SIP/RTP media plane and hands decoded audio to the same agent runtime, so you don’t need a separate SFU there either. Reach for it only as a fallback; the diverted client is the primary path.Related
One-Line WebRTC Diversion
The loopback encoder and worker transform in full — the mechanics behind
captureMicrophone.Browser Audio Capture
The complete capture-and-playback loop, including the
OpusPlayer downlink.Browser Compatibility
Where the transform ships, and why capture refuses when it doesn’t.
From LiveKit
The same technique, applied to migrating a self-hosted LiveKit deployment.