RTCRtpScriptTransform taps in a Worker. If that transform is missing, or
the WebTransport plane is missing, or the security context is missing, the SDK
refuses to run. It does not degrade. So the fastest diagnosis is to check
what the runtime supports, not to trace audio.
Work from top to bottom. The triage table narrows the symptom. Each section
below gives the diagnosis and fix.
Symptom → cause → fix
Capture throws before the permission prompt
IfcaptureMicrophone() throws before the browser asks for the microphone,
the SDK never reached getUserMedia. The SDK checks for the encoded-frame
transform first. If the transform is absent, the SDK stops. It does not
acquire hardware that it cannot legally publish from.
1
Confirm transform support
false result is the full explanation: the browser does not expose
RTCRtpScriptTransform. There is no main-thread fallback. The legacy
createEncodedStreams() path was removed, so the Worker transform is the
only capture path.2
Check the engine version
The standard Worker transform ships in Chrome/Edge 110+, Firefox 133+, and
recent Safari. Older builds are unsupported by design. Engines that only
shipped the main-thread stream are also unsupported by design.
3
Gate your UI, don't retry
Branch on
encodedTransformSupported(). Show a “browser not supported”
state instead of a call to captureMicrophone(). A retry cannot succeed.
The capability is binary.Two independent gates apply the refusal, so it holds for the whole session:
captureMicrophone() throws at capture time, and a connection with
requireEncodedTransform: true refuses to open the transport at all. See
RTCRtpScriptTransform
for why the invariant exists (frame-level E2EE) and how it is wired.The session never connects (no WebTransport)
If capture works butMoqtClient.connect() never resolves, the browser-native
QUIC plane is the problem. The browser SDK publishes audio over WebTransport
only.
1
Verify a secure context
WebTransport and
getUserMedia both require a secure origin. https://…
or http://localhost work. A plain http:// page served on a LAN IP does
not work. A page that “works on localhost but not on the office IP” almost
always has this cause.2
Check WebTransport exists
Check
typeof WebTransport === "function" on the page. If it is absent,
the engine (or an old build) cannot reach the QUIC plane at all.3
Confirm the relay URL and reachability
The client dials the relay host (
relay.clutchcall.dev) over QUIC/UDP. If
the network blocks UDP/443, the WebTransport handshake stalls. In the same
condition, the native SDKs drop to WS, but the browser cannot.You captured audio but hear nothing
When capture succeeds and frames flow but playback is silent, the browser autoplay policy almost always blocks the receive side. Missing data is almost never the cause. Playback runs encoded Opus through a WebCodecsAudioDecoder
into an AudioWorklet ring buffer (the OpusPlayer). The underlying
AudioContext starts suspended until a user gesture resumes it.
1
Resume the AudioContext on a gesture
Call
audioContext.resume() from inside a real user interaction (a click
or tap handler), not on page load. A suspended context silently drops
output.2
Rule out a decoder gap
The player’s
AudioDecoder (WebCodecs) is Chromium-only.
MediaStreamTrackProcessor is also Chromium-only. In Firefox or Safari,
playback can be silent even when capture works in other browsers. Validate
playback in a Chromium browser. Supply a decode fallback if you must
support the others.3
Confirm frames are arriving
If the context is resumed, and you are on Chromium, and you still hear
nothing, the problem is upstream delivery, not playback. Treat it as a
one-way-audio issue below.
The microphone permission itself fails
getUserMedia rejects if the origin does not have microphone permission, or
if a user gesture did not start the call. The SDK requests the mic with echo
cancellation, gain control, and noise suppression enabled. The browser must
grant capture for the loopback encoder to run.
1
Prompt from a gesture
Start capture from a click/tap handler. Browsers reject or silently ignore
permission requests made on load.
2
Re-grant a blocked permission
After a user denies the mic for an origin, the prompt does not appear
again. The user must enable the permission again in site settings. Detect
the rejection and show that instruction.
3
Check for a device in use
If another tab or app holds the microphone,
getUserMedia can fail.
Release the other consumer and try again.Local development gotchas
Most “works in prod, fails locally” reports come from the dev certificate flow, not the audio path.- Certificate hash. WebTransport in dev is trusted via
serverCertificateHashes. It requires a short-lived ECDSA certificate (≤14 days). An RSA cert fails the handshake. A cert older than the window also fails the handshake. - Use
127.0.0.1, not::1. The IPv6 loopback breaks the handshake. The IPv4 literal works. - A secure context is still required.
http://localhostis a secure context.http://<lan-ip>is not.
Related
RTCRtpScriptTransform
The Worker transform and the hard capture gate, in depth.
Browser Compatibility
Which engines ship the transform and WebTransport, and where the SDK refuses capture.
Browser Audio Capture
captureMicrophone, the OpusPlayer, and the full capture wiring.One-Way Audio
When frames flow in one direction only — the next stop if playback is empty.

