You already have a browser app that runs getUserMedia and an RTCPeerConnection. You do not have to rip it out to move media onto TeleQuick. captureMicrophone keeps that exact pipeline: a loopback RTCPeerConnection runs the browser’s AEC / AGC / noise-suppression and the Opus encoder, and a worker RTCRtpScriptTransform taps the encoded Opus frames and writes them to a MoQT audio track over QUIC. WebRTC’s ICE / DTLS / SRTP transport is never used — only its capture graph and codec.
This is a browser / TypeScript technique — mic capture is a JavaScript-only API. The browser SDK is WebTransport-only (no WebSocket fallback rung yet); if you need a TCP/WebSocket fallback today, terminate on the server-side WebRTC leg instead. See Browser compatibility.

Tap the mic onto QUIC

Connect a MoQT client with the encoded-frame rule enforced up front, publish an audio track, and hand that publication to captureMicrophone. Subscribe the return track and decode it with OpusPlayer.
import { MoqtClient, captureMicrophone, OpusPlayer } from "@telequick/sdk/moqt";

async function divertToQuic(sid: string, token: string) {
  // requireEncodedTransform: refuse to connect if this browser can't run the
  // worker RTCRtpScriptTransform, so the encoded-frame rule holds all session.
  const client = await MoqtClient.connect(
    "quic://relay.telequick.dev",
    token,
    (state) => console.log("moqt", state),
    { requireEncodedTransform: true },
  );

  // Uplink: publish the mic track (distinct name from the speaker track so the
  // relay never fans your own audio back to you).
  const uplink = client.publishAudio(`moqtaudio/${sid}`, "mic", {
    capability: "agent.mic.opus",
  });

  // captureMicrophone runs the loopback pc + worker transform and writes each
  // encoded Opus frame straight to `uplink` (raw PCM never crosses the wire).
  const mic = await captureMicrophone(uplink, {
    audioConstraints: {
      echoCancellation: true,
      autoGainControl: true,
      noiseSuppression: true,
    },
  });

  // Downlink: decode the agent/caller audio and play it back.
  const ctx = new AudioContext();
  const player = new OpusPlayer(ctx, { sampleRate: 48000, channels: 1 });
  await player.start();
  client.subscribeAudio(`moqtaudio/${sid}`, "spk", (tsUs, opus) => player.push(tsUs, opus));

  return function hangUp() {
    mic.stop();        // tears down the capture graph (not the publication)
    player.close();
    client.close();
  };
}

Reuse a track you already have

If your existing WebRTC app already called getUserMedia (or produced a processed MediaStreamTrack), pass it in with track so you never prompt for the mic twice — captureMicrophone skips acquisition and taps your track directly.
// `myExistingTrack` is the audio track your current WebRTC pipeline already owns.
const mic = await captureMicrophone(uplink, { track: myExistingTrack });
// mic.stop() will NOT stop a track you provided — you still own its lifecycle.
captureMicrophone throws if RTCRtpScriptTransform is absent rather than falling back to an insecure main-thread path — the worker transform is the only encoded-frame path (it is what lets the relay carry frame-level E2EE later). It ships in Chrome 110+, Edge, Safari, and Firefox 133+. Gate your UI with encodedTransformSupported() before offering a “call” button. Local dev needs a secure context and a short-lived ECDSA cert via serverCertificateHash; dial 127.0.0.1, not ::1.

WebRTC diversion

Why we tap WebRTC as a codec/capture layer, not a transport.

RTCRtpScriptTransform

The worker transform that diverts encoded frames.

Migrate from WebRTC

Move an existing WebRTC/SFU app onto QUIC.

Browser audio capture

The full capture + playback surface.