Most browser audio failures in ClutchCall Voice are not audio bugs. They are a capability or context problem that stops the pipeline before the browser captures or plays one frame. The browser capture path has one hard rule: the SDK publishes audio only as encoded Opus frames that an 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

If captureMicrophone() 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

A 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 but MoqtClient.connect() never resolves, the browser-native QUIC plane is the problem. The browser SDK publishes audio over WebTransport only.
The QUIC→WebSocket fallback ladder ships only in the native SDK core (Python, Go, Rust, Java, .NET wrappers). The browser/TypeScript SDK is WebTransport-only today. If the runtime or network cannot establish WebTransport, the browser has no automatic WS fallback. Plan your supported-browser matrix with this limit in mind.
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 WebCodecs AudioDecoder 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://localhost is a secure context. http://<lan-ip> is not.

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.