captureMicrophone runs a loopback RTCPeerConnection to drive the encoder,
taps the encoded Opus frames with an RTCRtpScriptTransform, and publishes
each frame as a MoQT object over QUIC/WebTransport. No ICE, no DTLS, no SRTP, no
SFU.
This is the browser capture path shipped in the TypeScript SDK
(
@@telequick/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 you’d normally take as a bundle: the capture + encode graph (great), the peer transport (ICE/DTLS/SRTP — you don’t need it when you own a QUIC relay), and an SFU to fan media out (a whole server you’d 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 straight 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 purely to make the encoder run. Its far
end is a second in-page peer connection whose decoded output is discarded — a
sink so the sender pipeline stays live.
Divert in one call
Connect a media session
Open a MoQT client against your relay and require the encoded-frame transform,
so the session refuses to start on a browser that can’t uphold the rule.
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.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 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
Diverting encoded frames onto QUIC instead of SRTP/UDP adds a JS-layer hop — frame → worker → main thread → MoQT write — so it’s fair to ask what that costs. A prototype that proved the technique measured the JS-layer round trip on localhost Chrome against a plain WebRTC echo:| Path | JS-layer RTT |
|---|---|
| Pure WebRTC (SRTP/UDP) | 0.84 ms |
| WebRTC capture → QUIC | 1.20 ms |
The +0.36 ms delta was measured in an internal prototype
(localhost Chrome, verified audibly end-to-end against a Node echo server) — it
proved the diversion is essentially free relative to a full voice turn. It is
a prototype figure, not a production SLA; the productionized 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, which 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.