npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@eleven-am/vox-rtc-client

v0.2.5

Published

Browser-side SDK for Vox-hosted WebRTC media sessions

Readme

@eleven-am/vox-rtc-client

Browser WebRTC client for Vox conversations. It captures microphone audio, plays assistant audio, performs full-trickle ICE, and receives Vox control events through one application-owned signaling endpoint.

Install

npm install @eleven-am/vox-rtc-client

Connect

import { VoxRtcBrowserClient } from "@eleven-am/vox-rtc-client";

const client = new VoxRtcBrowserClient({
  signalingEndpoint: "/api/vox/rtc",
  audioElement: document.querySelector("audio")!,
  audioConstraints: {
    echoCancellation: true,
    noiseSuppression: true,
    autoGainControl: true,
  },
  audioDucking: true,
});

client.on("state", (state) => {
  console.log(state.status, state.peerConnectionState, state.iceConnectionState);
});

client.on("signalingMessage", (event) => {
  if (event.type === "conversation.item.input_audio_transcription.completed") {
    console.log("user said", event.data.transcript);
  }
});

client.onClientEvent((event) => {
  console.log("server event", event.event, event.payload);
});

await client.connect();

signalingEndpoint must be a same-origin path. Direct Vox URLs, direct session bootstraps, route collections, and EventSource control bridges are not supported.

Signaling and media

The client opens one WebSocket to the application gateway. It sends the SDP offer, each local ICE candidate, and explicit end-of-candidates as they become available. Vox's answer and candidates arrive over the same socket. Candidates that arrive before the matching answer are buffered and applied in order.

After negotiation, media flows directly between the browser and Vox. The application gateway does not relay audio.

connect() resolves only after the peer connection reports that media is connected. Receiving an SDP answer is not treated as call readiness. If the peer fails, closes, or does not connect within 15 seconds, connect() rejects and releases the microphone, peer connection, and signaling socket. Configure that independent media deadline when needed:

const client = new VoxRtcBrowserClient({
  signalingEndpoint: "/api/vox/rtc",
  mediaConnectionTimeoutMs: 20_000,
});

Use a real ICE restart when the network path changes:

await client.restartIce();

Only one negotiation may run at a time. A failed restart closes the signaling session and media rather than leaving a partially controlled call alive.

Application events

Server-to-browser events sent by the server session's sendClientEvent arrive over the WebRTC data channel:

client.onClientEvent(({ event, payload }) => {
  console.log(event, payload);
});

Browser-to-server events use the same data channel and arrive on the server as browser.event:

client.sendEvent({ event: "ui.select", payload: { id: "choice-a" } });

Error handling

Vox session error frames arrive over the gateway signaling socket and are surfaced as typed session errors:

import { isFatalVoxError, isVoxErrorCode } from "@eleven-am/vox-rtc-client";

client.onSessionError((error) => {
  if (isFatalVoxError(error)) {
    endCallUi(error.message ?? "Call session error");
    return;
  }
  console.warn("recoverable Vox error", error.code, error.generationId);
});

Each conversation error frame carries message, code, recoverable, and generationId. code values are the stable contract set exported as VOX_ERROR_CODES (check membership with isVoxErrorCode). Old Vox servers omit code and recoverable; the SDK normalizes an empty code to undefined and treats missing recoverable as true. Conversation errors do not also emit the generic error event.

A WebRTC signaling failure (rtc.signaling_error, which Vox sends as { message, generation }) is different: it is terminal — Vox closes the session immediately after emitting it. It surfaces through the same onSessionError channel with recoverable: false and no code or generationId, so isFatalVoxError is always true for it.

Only a fatal error (recoverable === false, which includes code === "session_failed") or an actual transport/connection failure (the error event, an unexpected gateway close, or a failed connect()) should end the call UI. Recoverable errors are per-command failures: the session stays healthy, so handle them in place — for example, stop pumping the generation named by generationId after a response_stale_generation error — and keep the call running.

Browser-native data-channel events correlated to a response (response.created, response.done, response.cancelled, response.audio.clear, interruption.detected, interruption.false_positive) expose generationId on the envelope when the server supplies one:

client.onClientEvent(({ event, generationId }) => {
  if (event === "response.cancelled" && generationId) {
    abortGeneration(generationId);
  }
});

Audio ducking

Audio ducking changes playback volume while Vox decides whether detected speech is a real interruption. Vox remains authoritative for VAD, interruption, and response cancellation.

const client = new VoxRtcBrowserClient({
  signalingEndpoint: "/api/vox/rtc",
  audioElement,
  audioDucking: {
    duckVolume: 0.2,
    releaseDelayMs: 350,
  },
});

Ducking follows the authoritative Vox speech and interruption events that the gateway already forwards, so applications do not need another SSE or WebSocket connection. Vox owns the interruption decision; ducking only adjusts local playback volume while that decision is pending. Queued audio is never dropped — only response.audio.clear tells a client to discard playback.

Vox interruption candidates also use response.audio.suspend and response.audio.resume. The browser client applies these events automatically, even when optional ducking is disabled. Suspension mutes the remote media element without pausing, loading, or replacing its stream, and a resume is accepted only from the candidate that owns the suspension. This makes VAD onset quiet immediately while preserving buffered audio for a false-positive resume.

Two independent signals hold the duck, and playback returns to its original volume once both have cleared:

| Signal | Engaged by | Cleared by | | --- | --- | --- | | Speech | input_audio_buffer.speech_started, interruption.detected | input_audio_buffer.speech_stopped, interruption.false_positive, response.audio.clear, response.cancelled, response.done | | Hold | turn.state_changed with state paused, listening, or interrupted | turn.state_changed with state speaking, thinking, or idle |

Vox emits input_audio_buffer.speech_started at VAD onset, before the turn state machine runs, so the speech signal ducks the buffered WebRTC audio a full server round-trip earlier than turn.state_changed: paused could. The hold signal then keeps the duck engaged for as long as Vox reports the output held — including while Vox waits for a final transcript after the user has already stopped speaking, which is when the speech signal alone would release too early.

Private data

Session objects exposed to application code contain only the session ID, ICE servers, and expiry metadata. The Vox API key, internal hostname, and socket endpoint stay on the gateway and never reach the browser.