When you capture a microphone in the browser with ClutchCall Voice, the audio never leaves over WebRTC’s own transport. Instead, a loopback RTCPeerConnection runs the browser’s real audio pipeline: echo cancellation, gain control, noise suppression, and the Opus encoder. An RTCRtpScriptTransform intercepts the encoded Opus frames before packetization. The SDK hands those frames to the MoQT publication and sends them over QUIC/WebTransport. This page explains the tap in depth: where it runs, why the older API is gone, and exactly how each frame moves from the encoder to the wire. For the one-call developer surface, see One-Line WebRTC Diversion and Browser Audio Capture. This page is the mechanism underneath both.

The transform runs in a Worker — and only there

RTCRtpScriptTransform is the W3C Encoded Transform API. You construct it on the main thread and attach it to an RTCRtpSender, but the transform callback itself runs in a dedicated Worker. The SDK ships exactly one code path for tapping encoded frames, and it is this Worker-based transform.
The older, Chromium-only RTCRtpSender.createEncodedStreams() exposed the encoded stream on the main thread. Current browser engines removed it, and the SDK removed its legacy branch for it too. The Worker transform is the only encoded-frame path. There is no main-thread fallback.
The design keeps every media frame inside one Worker. This is a deliberate choice, not just API hygiene:
  • It is what current browsers ship. Chrome 110+, Edge, Safari, and Firefox 133+ all expose the standard Worker transform. None still expose the main-thread stream. One target API keeps the capture path identical everywhere.
  • It is the seam for frame-level end-to-end encryption. Because every frame passes through one Worker, the SDK can later encrypt payloads there. Then even a relay, or a self-hosted SFU you migrate away from, sees ciphertext only.

The hard gate: capture refuses when the transform is absent

The SDK treats the encoded-frame rule as non-negotiable. If RTCRtpScriptTransform is not a function on globalThis, the SDK refuses to capture. It does not fall back to an insecure or main-thread path. A helper reports support:
Two independent gates enforce the refusal, so the invariant holds for the whole session, not just at the moment you acquire the microphone:
1

At capture time

captureMicrophone() checks support before it calls getUserMedia, so it never acquires hardware it cannot use. If the transform is unsupported, it throws immediately.
2

At connect time

When you connect a media session with requireEncodedTransform: true, and the runtime lacks the transform, the client stops before it opens the transport. You never establish a session on which you cannot legally publish audio.

How a frame is tapped and forwarded

After support is confirmed, the capture graph is a short loopback. The steps below trace one microphone frame from silicon to the QUIC wire.
1

Run the browser audio pipeline in a loopback

The SDK wires two RTCPeerConnections to each other locally (pc1pc2). It adds your microphone track to pc1 as a sender. pc2 is only a sink; it forces the encoder to run. The SDK completes the loopback SDP offer/answer locally, which starts the Opus encoder. The SDK never uses WebRTC’s ICE/DTLS/SRTP transport to leave the machine.
2

Attach the transform to the sender

The SDK creates a Worker and sets it as the sender’s transform:
From here, every encoded Opus frame from the sender goes into the Worker. The frames are not packetized for WebRTC.
3

Read frames off the transform inside the Worker

The Worker handles the rtctransform event. Each transformer exposes a readable (incoming encoded frames) and a writable (the onward pipeline). The Worker loops: read a frame, copy its payload, post it to the main thread, then re-enqueue the frame to writable so the sender pipeline stays unblocked.
The timestamp on each frame is the RTP timestamp on Opus’s 48 kHz clock.
4

Forward each frame to the MoQT publication

Back on the main thread, the Worker’s message handler converts the 48 kHz RTP timestamp to microseconds and writes the raw encoded payload to the publication: one MoQT object per Opus frame, carried over WebTransport:
The SDK ships the Worker inlined as a Blob URL, so the whole capture path is a single module. There is no extra worker file to host or configure. Nothing about the transform requires a separate build step.

What crosses the wire

The transform hands off encoded Opus payloads, not PCM and not RTP packets. On the wire, the Opus stays opus/48000/2 with a 48 kHz RTP clock, even though the encoder’s internal rate is driven narrowband for voice. The receiver side reverses this: an Opus decoder (WebCodecs) feeds an audio worklet ring buffer. See the OpusPlayer covered in Browser Audio Capture.

Teardown

The handle returned by capture tears down the graph deterministically: it terminates the Worker, stops the microphone track (unless you supplied your own), and closes both loopback peer connections. It does not close the MoQT publication. You manage that lifecycle on the transport.

Lifecycle at a glance

1

Gate

Check encodedTransformSupported(). Refuse otherwise.
2

Capture

getUserMedia runs with AEC/AGC/NS enabled.
3

Encode

The loopback RTCPeerConnection runs the Opus encoder.
4

Tap

The Worker RTCRtpScriptTransform reads encoded frames and re-enqueues them.
5

Forward

The main thread writes each frame to the MoQT publication over QUIC.

One-Line WebRTC Diversion

The developer-facing wrapper that hides this whole graph behind one call.

Browser Audio Capture

captureMicrophone, the OpusPlayer, and end-to-end capture wiring.

Browser Compatibility

Which engines ship the transform, and where capture is refused.

WebTransport

The browser-native QUIC plane the tapped frames ride on.