When you capture a microphone in the browser with TeleQuick 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 — and an RTCRtpScriptTransform intercepts the encoded Opus frames before they would be packetized. Those frames are handed to the MoQT publication and shipped 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 executes 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() — which exposed the encoded stream on the main thread — has been removed from current browser engines, and the SDK has removed its legacy branch for it too. The Worker transform is the sole encoded-frame path. There is no main-thread fallback.
Keeping every media frame inside one Worker is a deliberate design choice, not just API hygiene:
  • It is what current browsers actually ship. Chrome 110+, Edge, Safari, and Firefox 133+ all expose the standard Worker transform; none still expose the main-thread stream. Targeting one 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 so that even a relay — or a self-hosted SFU you migrate off of — 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 rather than fall back to an insecure or main-thread path. A helper reports support:
import { encodedTransformSupported } from "@telequick/sdk/moqt";

if (!encodedTransformSupported()) {
  // This browser cannot run the required encoded-frame transform.
}
The refusal is enforced at two independent gates so the invariant holds for the whole session, not just at the moment you grab the microphone:
1

At capture time

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

At connect time

When you connect a media session with requireEncodedTransform: true, the client bails before opening the transport if the runtime lacks the transform — you never establish a session you cannot legally publish audio on.

How a frame is tapped and forwarded

Once 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

Two RTCPeerConnections are wired to each other locally (pc1pc2). Your microphone track is added to pc1 as a sender; pc2 is only a sink so the encoder is forced to run. The loopback SDP offer/answer is completed locally — this is what starts the Opus encoder. WebRTC’s ICE/DTLS/SRTP transport is never used to leave the machine.
2

Attach the transform to the sender

A Worker is created and set as the sender’s transform:
sender.transform = new RTCRtpScriptTransform(worker, { side: "sender" });
From here, every encoded Opus frame the sender produces is routed into the Worker instead of being 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.
self.onrtctransform = (event) => {
  const t = event.transformer;
  const reader = t.readable.getReader();
  const writer = t.writable.getWriter();
  (async () => {
    for (;;) {
      const { value, done } = await reader.read();
      if (done) break;
      const buf = value.data.slice(0);            // copy the encoded payload
      self.postMessage({ data: buf, timestamp: value.timestamp }, [buf]);
      await writer.write(value);                  // keep the pipeline flowing
    }
  })();
};
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:
const tsUs = BigInt(Math.round((rtpTimestamp / 48000) * 1_000_000));
publication.write(tsUs, new Uint8Array(data));
The Worker is shipped inlined as a Blob URL, so the whole capture path is a single module with 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 — that lifecycle is yours to manage on the transport.

Lifecycle at a glance

1

Gate

Confirm encodedTransformSupported() — refuse otherwise.
2

Capture

getUserMedia with AEC/AGC/NS enabled.
3

Encode

Loopback RTCPeerConnection runs the Opus encoder.
4

Tap

Worker RTCRtpScriptTransform reads encoded frames, re-enqueues them.
5

Forward

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.