captureMicrophone(publication, opts) for the send side
and OpusPlayer for playback — so you never touch WebCodecs or raw PCM by
hand.
The capture side reuses the browser’s own audio pipeline. A loopback
connection runs the built-in echo cancellation, automatic gain control, and
noise suppression, then the platform’s Opus encoder; you tap the encoded
frames off the encoder and ship the bytes. Nothing decodes the audio in the
page — the browser does the DSP and the codec, you just move the payload. See
WebRTC diversion for why
this design keeps you off a media SFU, and
RTCRtpScriptTransform
for how the tap works.
The encoded-frame tap runs only in a Worker via the standard
RTCRtpScriptTransform. If the browser lacks it, captureMicrophone refuses
to capture rather than falling back to an insecure path — it never grabs the
mic it can’t use. Check support first with encodedTransformSupported() and
read Browser compatibility
for the support matrix.Capture the microphone
Connect a media session
Open a transport session to your relay and set
requireEncodedTransform: true.
That enforces the encoded-frame rule for the whole session up front, so you
fail at connect time on an unsupported browser instead of after acquiring the
mic.Publish an audio track
Create an outbound audio track. The Opus track runs 48 kHz mono, 20 ms frames on the wire. You don’t set the
rate — the browser’s encoder does — but those are the defaults an
publishAudio(ns, name, opts) returns an
AudioPublication whose write(timestampUs, frame) accepts one encoded Opus
frame at a time. Namespaces are room-scoped by call — key them off your
call_sid (for example voice/<sid>, track name uplink).AudioPublication carries.Capture options
captureMicrophone(publication, opts) takes an optional MicCaptureOptions:
Constraints passed to
getUserMedia({ audio }). Defaults enable
echoCancellation, autoGainControl, and noiseSuppression. Override to pin
a device or relax the DSP chain.Supply your own audio track instead of calling
getUserMedia — for example a
track you’ve already processed. When you pass this, stop() leaves the track
running (you own its lifecycle).The browser’s echo cancellation, gain control, and noise suppression are the
only cleanup on the send side — the server-side media path does not run AEC
or AGC of its own. On PSTN legs, where there’s no browser AEC, the runtime
compensates with voice-activity gating instead (see
Turn detection).
Play received audio
OpusPlayer is the receive-side mirror: WebCodecs decodes the Opus frames you
receive, and an AudioWorklet ring buffer plays them out, padding with silence
on underrun. Feed it the frames delivered by subscribeAudio.
OpusPlayer accepts an options object: sampleRate (decoder output rate,
default 48000) and channels (default 1). Call start() once before the first
push, and close() to release the decoder and disconnect the worklet.
A minimal two-way softphone
Put both halves together — publish the mic, subscribe the far side, play it — for a complete browser voice leg.These helpers are browser-only:
getUserMedia, RTCRtpScriptTransform,
and WebCodecs have no server-side equivalent. To send or receive audio from
Node, Python, Go, or Rust, use the native-core audio bridge instead — see the
voice SDKs. Native mobile capture is
plan-only today.In the browser this transport is WebTransport-only — there is no automatic
QUIC→WebSocket fallback rung yet (that ships in the native SDK cores). If a
network blocks QUIC, the media session fails rather than downgrading. Plan your
fallback UX around
WebTransport availability.
Next steps
WebRTC diversion
Why capture reuses the browser’s WebRTC pipeline as a codec tap, not a
transport.
RTCRtpScriptTransform
How the Worker taps encoded Opus frames and where E2EE fits in.
Browser compatibility
Which engines run the transform, WebCodecs, and the capture worklet.
Turn detection
How on-device VAD and barge-in decide when the caller is speaking.