RTCRtpScriptTransform in a
Worker. When that transform (or the WebTransport plane it rides on, or the
security context both require) is missing, the SDK refuses rather than degrade —
so the fastest diagnosis is to check what the runtime supports, not to trace
audio.
Work top-to-bottom: the triage table narrows the symptom, then each section below
gives the diagnosis and fix.
Symptom → cause → fix
| Symptom | Likely cause | Fix |
|---|---|---|
captureMicrophone() throws immediately, before any permission prompt | No RTCRtpScriptTransform — the browser lacks the required encoded-frame transform | Use a supporting engine (Chrome/Edge 110+, Firefox 133+, recent Safari); gate with encodedTransformSupported() and show a fallback UI |
MoqtClient.connect() rejects; no session ever opens | No WebTransport, or connecting with requireEncodedTransform: true on an engine without the transform | Confirm WebTransport exists; on the browser SDK there is no WS fallback today — the runtime must support both |
Connection fails only on http:// or a bare IP page | Insecure context — getUserMedia and WebTransport both require a secure origin | Serve over HTTPS (or http://localhost); never a plain-HTTP LAN IP |
| Permission prompt is denied or never appears | Microphone permission blocked, or no user gesture | Prompt from a click handler; re-grant the mic permission for the origin |
| Capture works but you hear nothing | AudioContext suspended by autoplay policy | resume() the context inside a user gesture before playback |
| Playback silent only in Firefox/Safari | The player uses WebCodecs AudioDecoder, which is Chromium-only | Use a Chromium browser for playback, or provide a decode fallback |
| Local dev only: WebTransport handshake fails | Dev certificate hash wrong/expired, or you dialed ::1 | Use a fresh ECDSA cert (≤14 days) via serverCertificateHashes; dial 127.0.0.1, not IPv6 ::1 |
Capture throws before the permission prompt
IfcaptureMicrophone() throws before the browser ever asks for the
microphone, the SDK never reached getUserMedia. It checks for the encoded-frame
transform first and bails if it is absent — it will not acquire hardware it cannot
legally publish from.
Confirm transform support
false here is the whole 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.Check the engine version
The standard Worker transform ships in Chrome/Edge 110+, Firefox 133+, and
recent Safari. Older builds — and any engine that only ever shipped the
main-thread stream — are unsupported by design.
The refusal is enforced at two independent gates so it holds for the whole
session:
captureMicrophone() throws at capture time, and connecting 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 is fine butMoqtClient.connect() never resolves, the browser-native
QUIC plane is the problem. The browser SDK publishes audio over
WebTransport only.
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.
A page that “works on localhost but not on the office IP” is almost always
this.Check WebTransport exists
typeof WebTransport === "function" on the page. Absent means the engine
(or an old build) can’t reach the QUIC plane at all.You captured audio but hear nothing
When capture succeeds and frames are flowing but playback is silent, the receive side is almost always blocked by browser autoplay policy, not by missing data. Playback runs encoded Opus through a WebCodecsAudioDecoder into an
AudioWorklet ring buffer (the OpusPlayer), and the underlying AudioContext
starts suspended until a user gesture resumes it.
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.Rule out a decoder gap
The player’s
AudioDecoder (WebCodecs) is Chromium-only, as is
MediaStreamTrackProcessor. In Firefox or Safari playback can be silent even
when capture works elsewhere — validate playback in a Chromium browser, and
provide a decode fallback if you must support the others.The microphone permission itself fails
getUserMedia rejects if the origin lacks microphone permission or the call is
not driven by a user gesture. The SDK requests the mic with echo cancellation,
gain control, and noise suppression enabled, so the browser must actually grant
capture for the loopback encoder to run.
Prompt from a gesture
Trigger capture from a click/tap handler. Browsers reject or silently ignore
permission requests made on load.
Re-grant a blocked permission
Once a user has denied the mic for an origin, the prompt won’t reappear — the
user must re-enable it in site settings. Detect the rejection and surface that
instruction.
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
serverCertificateHashesand requires a short-lived ECDSA certificate (≤14 days). An RSA cert, or one older than the window, fails the handshake. - Use
127.0.0.1, not::1. Dialing the IPv6 loopback breaks the handshake; the IPv4 literal works. - Secure context still applies.
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 capture is refused.
Browser Audio Capture
captureMicrophone, the OpusPlayer, and the full capture wiring.One-Way Audio
When frames flow one direction only — the next stop if playback is genuinely empty.