# Divert WebRTC Audio to FirstByte

> Use captureMicrophone to keep your browser WebRTC audio pipeline and send the encoded Opus over QUIC. The ICE/DTLS/SRTP transport is not used.

You already have a browser app that runs `getUserMedia` and an `RTCPeerConnection`.
You do not have to remove it to move media onto FirstByte. `captureMicrophone`
keeps that exact pipeline. A **loopback `RTCPeerConnection`** runs the browser's
AEC / AGC / noise-suppression and the Opus encoder. A **worker
`RTCRtpScriptTransform`** taps the *encoded* Opus frames. It writes them to a
MoQT audio track over QUIC. WebRTC's ICE / DTLS / SRTP transport is never used.
Only its capture graph and codec are used.

> **NOTE:**
> This is a **browser / TypeScript** technique. Microphone capture is a
>   JavaScript-only API. The browser SDK is WebTransport-only, with no WebSocket
>   fallback rung yet. If you need a TCP/WebSocket fallback today, terminate on
>   the server-side WebRTC leg instead. See
>   [Browser compatibility](/modalities/voice/transport-web/browser-compatibility).

## Tap the mic onto QUIC

1. Connect a MoQT client with the encoded-frame rule enforced up front.
2. Publish an audio track.
3. Give that publication to `captureMicrophone`.
4. Subscribe to the return track and decode it with `OpusPlayer`.

  
```ts
import { MoqtClient, captureMicrophone, OpusPlayer } from "clutchcall_connect/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.firstbyte.fast",
    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();
  };
}
```
  
  
```python
# Browser mic capture is a JavaScript-only API (getUserMedia +
# RTCRtpScriptTransform), so there is no Python captureMicrophone. From Python
# you consume the same audio server-side over the AudioBridge — no WebRTC.
import os
from clutchcall.voice import Voice

v = Voice(
    base_url="https://engine.firstbyte.fast",
    api_key=os.environ["CLUTCHCALL_CREDENTIALS"],
    org_id="org_abc",
)

def bridge(sid: str):
    b = v.audio_bridge.attach(
        sid,
        codec="pcm16",
        sample_rate=16000,
        on_uplink=lambda pcm, ts_us: my_asr.feed(pcm),  # caller/browser audio in
    )
    my_tts.on_chunk(lambda pcm: b.publish_downlink(pcm))  # audio back out
    return b
```
  

## Reuse a track you already have

Your existing WebRTC app possibly called `getUserMedia` already, or produced a
processed `MediaStreamTrack`. Pass that track in with `track` so the browser
does not prompt for the microphone twice. `captureMicrophone` then skips
acquisition and taps your track directly.

```ts
// `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.
```

> **WARNING:**
> `captureMicrophone` **throws** if `RTCRtpScriptTransform` is absent. It does
>   not fall back to an insecure main-thread path. The worker transform is the
>   only encoded-frame path, and 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 you show a "call" button. Local
>   development needs a secure context and a short-lived ECDSA cert via
>   `serverCertificateHash`. Dial `127.0.0.1`, not `::1`.

## Related

  - **[WebRTC diversion](/modalities/voice/transport-web/webrtc-diversion)** — Why we tap WebRTC as a codec/capture layer, not a transport.
  - **[RTCRtpScriptTransform](/modalities/voice/transport-web/rtcrtpscripttransform)** — The worker transform that diverts encoded frames.
  - **[Migrate from WebRTC](/modalities/voice/migration/from-webrtc)** — Move an existing WebRTC/SFU app onto QUIC.
  - **[Browser audio capture](/modalities/voice/transport-web/browser-audio-capture)** — The full capture + playback surface.
