Skip to main content

WebRTC audio with WebSocket control

The webrtcws.v1 binding adds timed WebRTC audio alongside the existing WebSocket binding. It does not replace WebSocket binary audio. A client chooses at connection setup:

Same protocol, different media path

Both profiles use the frozen babelforce.v1 catalog and classic.v1 envelope. Selecting WebRTC changes transport behavior, not application handlers or payload meaning.

WebSocket subprotocolControlAudio
rtvbp.v1WebSocket textWebSocket binary L16
rtvbp.webrtc.v1WebSocket textWebRTC RTP PCMU

Both bindings expose the same RTVBP operations, events, and classic envelope. Go and Rust retain the same L16 SDK audio API across both. Browsers use L16 byte audio for rtvbp.v1 and native media tracks for rtvbp.webrtc.v1, behind the explicit browser device adapter. An endpoint may offer both; a WebRTC-capable client explicitly offers rtvbp.webrtc.v1, while an ordinary or headerless client continues to use rtvbp.v1.

Connection flow​

Authentication is evaluated before the WebSocket upgrade and therefore before SDP parsing, ICE, or Pion peer creation. The bearer policy is deployment-specific, just as it is for the plain WebSocket binding.

Signaling​

Initial SDP negotiation uses one reserved transport.webrtc.offer request and its correlated response. These messages use the selected envelope, but the transport consumes them before the session starts: they are not catalog operations and never reach an application handler.

webrtcws.v1 uses non-trickle ICE. Each SDK waits for candidate gathering to complete and embeds the candidates in SDP, so the initial exchange is bounded to one request and one response. SDP is limited to 512 KiB and the complete signaling frame to 1 MiB. Do not log SDP in production because it contains network addressing information.

Audio formats​

The WebRTC wire codec is PCMU/8000/1 (RTP payload type 0), which WebRTC endpoints and browsers support without a custom codec. The Go and Rust SDK boundary remains the frozen v1 format:

  • signed 16-bit little-endian L16 PCM;
  • 8,000 samples per second;
  • one channel;
  • 20 ms / 320 bytes per SDK frame.

The transport encodes each outbound L16 frame to a 160-byte G.711 mu-law sample and decodes inbound PCMU back to L16. G.711 is lossy, so decoded samples need not equal the original PCM values. Incoming media frames are timed from the RTP 8 kHz clock. The session's byte stream preserves that decoded order while callers that use the media-channel layer can inspect PTS directly.

Go configuration​

Add WebRTC support to an existing server configuration; plain WebSocket audio remains enabled:

base := ws.ServerConfig{
Addr: "0.0.0.0:8080",
Path: "/rtvbp",
AudioFormat: l16Format,
}
server := ws.NewServer(webrtcws.AddToServer(base, webrtcws.Config{
PeerConnection: pionConfig,
AudioFormat: l16Format,
}), handler)

Choose the binding on the client by selecting its session option:

// Existing WebSocket-binary audio:
option := ws.Client(websocketConfig)

// Or WebRTC audio with the same WebSocket control configuration:
option = webrtcws.Client(webrtcws.ClientConfig{
WebSocket: websocketConfig,
PeerConnection: pionConfig,
})

The existing compile-tested demo server offers both bindings. The matching demo client selects one with -audio-transport websocket or -audio-transport webrtc; its protocol, call, DTMF, audio-device, and shutdown behavior is otherwise the same. The server's -preferred-audio-transport flag controls selection order when a client offers both.

# Terminal 1: one server, both bindings
cd sdk/go/examples/rtvbp-demo-server
go run . -preferred-audio-transport webrtc

# Terminal 2: choose WebRTC (use "websocket" to choose the existing binding)
cd ../rtvbp-demo-client
go run . -audio-transport webrtc -sample-rate 8000

Rust configuration​

The Rust server advertises the WebRTC token alongside the classic fallback, then decorates only connections that selected it:

let server = ws::Server::bind(webrtcws::add_to_server(server_config)).await?;
let base = server.accept().await?;
let transport = if base.wire_subprotocol() == webrtcws::SUBPROTOCOL {
webrtcws::accept(base, envelope.clone(), webrtc_config).await?
} else {
base
};

Clients choose either ws::ClientFactory or webrtcws::ClientFactory. Both implement the same TransportFactory consumed by Session, so catalog handlers and audio-buffer code do not change. See the Rust SDK quickstart and its compile-tested dual-profile demo.

TypeScript browser configuration​

The browser transport uses native RTCPeerConnection, requires the mandatory PCMU capability, and keeps microphone and speaker ownership in BrowserAudioDevice:

const device = new BrowserAudioDevice();
const session = new Session({
envelope: classicV1.classicV1Envelope,
handler: new Handler({
onBegin: async (context) => await context.acceptAudio(),
}),
transportFactory: browserWebRtcTransport({
url: "wss://voice.example/rtvbp",
audioDevice: device,
rtcConfiguration: {
iceServers: [{ urls: "stun:stun.example:3478" }],
},
}),
});

const running = session.run();
await session.ready;
const transport = session.transport;
if (!(transport instanceof BrowserWebRtcTransport)) {
throw new Error("WebRTC transport was not selected");
}
const report = await transport.getStats();

The device track is attached before the offer, the remote audio track is connected to its rendering graph, and the connected transport is available as session.transport for BrowserWebRtcTransport.getStats(), connection state, and track replacement. See the TypeScript/browser quickstart for lifecycle, authentication, Content Security Policy, and cleanup details.

ICE and deployment​

The caller's Go Pion or Rust webrtc::RTCConfiguration controls ICE. Empty configuration is enough for same-host and many direct-network tests. Production deployments normally provide STUN and often TURN:

webrtc.Configuration{ICEServers: []webrtc.ICEServer{{
URLs: []string{"stun:stun.example.com:3478", "turns:turn.example.com:5349"},
Username: turnUsername,
Credential: turnCredential,
}}}

Supply TURN credentials through secret management and rotate them independently of WebSocket bearer credentials. The demo pair reads comma-separated URLs from RTVBP_ICE_SERVERS and optional RTVBP_ICE_USERNAME / RTVBP_ICE_CREDENTIAL; it contains no default public service or secret.

Allow the UDP/TCP paths required by the configured ICE servers and Pion candidates. A successful WebSocket connection alone does not prove the media path is reachable.

Current limitations​

  • one bidirectional audio channel named audio;
  • PCMU only on WebRTC; no Opus yet;
  • L16/8000/16-bit/mono/20 ms only at the SDK boundary;
  • initial non-trickle ICE only; no renegotiation or ICE restart in webrtcws.v1;
  • no packet-loss concealment in the transport.

Unsupported formats and duplicate media binding fail explicitly. WebRTC failure closes the media channel and the session follows its normal failure path; orderly shutdown closes the WebRTC peer and then flushes WebSocket control.