You want a browser tab to act as a voice endpoint: turn the microphone into Opus frames you can publish over QUIC, and turn received Opus frames back into sound. The SDK ships two helpers that do exactly this on top of a live transport session — captureMicrophone(publication, opts) for the send side and OpusPlayer for playback — so you never touch WebCodecs or raw PCM by hand. The capture side reuses the browser’s own audio pipeline. A loopback connection runs the built-in echo cancellation, automatic gain control, and noise suppression, then the platform’s Opus encoder; you tap the encoded frames off the encoder and ship the bytes. Nothing decodes the audio in the page — the browser does the DSP and the codec, you just move the payload. See WebRTC diversion for why this design keeps you off a media SFU, and RTCRtpScriptTransform for how the tap works.
The encoded-frame tap runs only in a Worker via the standard RTCRtpScriptTransform. If the browser lacks it, captureMicrophone refuses to capture rather than falling back to an insecure path — it never grabs the mic it can’t use. Check support first with encodedTransformSupported() and read Browser compatibility for the support matrix.

Capture the microphone

1

Connect a media session

Open a transport session to your relay and set requireEncodedTransform: true. That enforces the encoded-frame rule for the whole session up front, so you fail at connect time on an unsupported browser instead of after acquiring the mic.
import { MoqtClient, encodedTransformSupported } from "@telequick/sdk";

if (!encodedTransformSupported()) {
  // Show a fallback UI; this browser can't send audio through the tap.
  throw new Error("Encoded audio capture unsupported in this browser");
}

const client = await MoqtClient.connect(
  "quic://relay.telequick.dev",
  tenantToken,
  (state) => console.log("transport:", state),
  { requireEncodedTransform: true },
);
2

Publish an audio track

Create an outbound audio track. publishAudio(ns, name, opts) returns an AudioPublication whose write(timestampUs, frame) accepts one encoded Opus frame at a time. Namespaces are room-scoped by call — key them off your call_sid (for example voice/<sid>, track name uplink).
const uplink = client.publishAudio(`voice/${callSid}`, "uplink", {
  capability: "agent.mic.opus",
});
The Opus track runs 48 kHz mono, 20 ms frames on the wire. You don’t set the rate — the browser’s encoder does — but those are the defaults an AudioPublication carries.
3

Start capture

Hand the publication to captureMicrophone. It prompts for the mic (with AEC/AGC/NS enabled), runs the encoder, installs the transform Worker, and calls uplink.write(...) for every encoded frame. It returns a handle — call stop() to tear the capture graph down.
import { captureMicrophone } from "@telequick/sdk";

const capture = await captureMicrophone(uplink);

// later, when the user mutes or hangs up:
capture.stop();
capture.stop() tears down the capture graph (Worker, loopback connection, and the mic track it acquired) but does not close the MoQT publication or the transport session. Close those yourself when the call ends.

Capture options

captureMicrophone(publication, opts) takes an optional MicCaptureOptions:
audioConstraints
MediaTrackConstraints
Constraints passed to getUserMedia({ audio }). Defaults enable echoCancellation, autoGainControl, and noiseSuppression. Override to pin a device or relax the DSP chain.
track
MediaStreamTrack
Supply your own audio track instead of calling getUserMedia — for example a track you’ve already processed. When you pass this, stop() leaves the track running (you own its lifecycle).
The browser’s echo cancellation, gain control, and noise suppression are the only cleanup on the send side — the server-side media path does not run AEC or AGC of its own. On PSTN legs, where there’s no browser AEC, the runtime compensates with voice-activity gating instead (see Turn detection).

Play received audio

OpusPlayer is the receive-side mirror: WebCodecs decodes the Opus frames you receive, and an AudioWorklet ring buffer plays them out, padding with silence on underrun. Feed it the frames delivered by subscribeAudio.
import { OpusPlayer } from "@telequick/sdk";

const ctx = new AudioContext();
const player = new OpusPlayer(ctx);
await player.start(); // load the worklet + configure the decoder once

client.subscribeAudio(`voice/${callSid}`, "downlink", (tsUs, opus) => {
  player.push(tsUs, opus);
});

// on hangup:
player.close();
OpusPlayer accepts an options object: sampleRate (decoder output rate, default 48000) and channels (default 1). Call start() once before the first push, and close() to release the decoder and disconnect the worklet.
Browsers gate AudioContext behind a user gesture. Create the context (or call ctx.resume()) from a click or tap, or playback stays silent until the user interacts with the page.

A minimal two-way softphone

Put both halves together — publish the mic, subscribe the far side, play it — for a complete browser voice leg.
import {
  MoqtClient,
  captureMicrophone,
  OpusPlayer,
  encodedTransformSupported,
} from "@telequick/sdk";

async function joinCall(callSid: string, token: string) {
  if (!encodedTransformSupported()) throw new Error("unsupported browser");

  const client = await MoqtClient.connect(
    "quic://relay.telequick.dev",
    token,
    undefined,
    { requireEncodedTransform: true },
  );

  // send: mic → Opus → uplink track
  const uplink = client.publishAudio(`voice/${callSid}`, "uplink");
  const capture = await captureMicrophone(uplink);

  // receive: downlink track → Opus decode → speakers
  const ctx = new AudioContext();
  const player = new OpusPlayer(ctx);
  await player.start();
  client.subscribeAudio(`voice/${callSid}`, "downlink", (ts, f) =>
    player.push(ts, f),
  );

  return () => {
    capture.stop();
    player.close();
    void ctx.close();
  };
}
These helpers are browser-only: getUserMedia, RTCRtpScriptTransform, and WebCodecs have no server-side equivalent. To send or receive audio from Node, Python, Go, or Rust, use the native-core audio bridge instead — see the voice SDKs. Native mobile capture is plan-only today.
In the browser this transport is WebTransport-only — there is no automatic QUIC→WebSocket fallback rung yet (that ships in the native SDK cores). If a network blocks QUIC, the media session fails rather than downgrading. Plan your fallback UX around WebTransport availability.

Next steps

WebRTC diversion

Why capture reuses the browser’s WebRTC pipeline as a codec tap, not a transport.

RTCRtpScriptTransform

How the Worker taps encoded Opus frames and where E2EE fits in.

Browser compatibility

Which engines run the transform, WebCodecs, and the capture worklet.

Turn detection

How on-device VAD and barge-in decide when the caller is speaking.