The Web SDK ships as two packages:
@levelchat/web— framework-agnostic, ESM + CJS builds, ~80 kB gz, zero runtime deps.@levelchat/web-react— React hooks + signature components on top of@levelchat/web.
Install
npm install @levelchat/webnpm install @levelchat/web @levelchat/web-reactConnect
import { LevelChat } from '@levelchat/web';
const lc = new LevelChat({ logLevel: 'info' });
// `joinLive` returns a `LiveStream` that wraps the underlying `Room` and
// exposes the lifecycle helpers most apps need.
const live = await lc.joinLive({
token, // minted on your server
signalingUrl: url, // included in the token-mint response
roomType: 'meeting', // 'meeting' | '1to1' | 'live' | 'webinar'
});Publish + subscribe
// Publish a camera with simulcast — three quality layers the SFU can pick
// from based on each subscriber's bandwidth.
const camera = await live.publishCamera({
resolution: '720p',
encodings: [
{ rid: 'h', maxBitrate: 1_500_000 },
{ rid: 'm', maxBitrate: 500_000 },
{ rid: 'l', maxBitrate: 150_000 },
],
scalabilityMode: 'L1T3',
});
await live.publishMic();
// Subscribe to remote tracks as they arrive.
live.room.on('track-subscribed', (track) => {
if (!track.mediaStreamTrack) return;
const el = document.createElement('video');
el.srcObject = new MediaStream([track.mediaStreamTrack]);
el.autoplay = true;
el.playsInline = true;
document.body.appendChild(el);
});
live.room.on('participant-joined', (p) => console.log('hello', p.identity));
live.room.on('participant-left', (p) => console.log('goodbye', p.identity));Screen share
const screen = await live.room.publishScreen({ frameRate: 15 });
// returns an array of TrackView (one for the picked surface, optionally one for system audio)
// Stop sharing:
await live.room.stopPublishing(screen[0].id);Recording
const recording = await live.room.startRecording({ layout: 'tracks' });
// `recording.id` is the recording id; surface it in your UI.
await live.room.stopRecording();React bindings
The @levelchat/web-react package wraps the vanilla SDK with a context provider, hooks,
and pre-styled components. Same WebRTC underneath; the React layer is purely ergonomic.
import {
LevelChatProvider,
useLocalParticipant,
useParticipants,
ParticipantGrid,
VideoTile,
} from '@levelchat/web-react';
export function Call({ tokenEndpoint, room }: { tokenEndpoint: string; room: string }) {
return (
<LevelChatProvider tokenEndpoint={tokenEndpoint} room={room} roomType="meeting">
<ParticipantGrid />
<Controls />
</LevelChatProvider>
);
}
function Controls() {
const { toggleCamera, toggleMic, camOn, micOn } = useLocalParticipant();
return (
<>
<button onClick={toggleCamera}>{camOn ? 'Camera off' : 'Camera on'}</button>
<button onClick={toggleMic}>{micOn ? 'Mic off' : 'Mic on'}</button>
</>
);
}End-to-end encryption
import { LevelChat } from '@levelchat/web';
const lc = new LevelChat();
const live = await lc.joinLive({ token, signalingUrl: url, roomType: 'meeting' });
// Provide a 32-byte key from your own keying system (KMS / MLS group key /
// per-room derived secret). The SFU never sees this key.
await live.room.setEncryptionKey(crypto.getRandomValues(new Uint8Array(32)));The SDK uses Insertable Streams (Chrome/Edge/Safari 16.4+) to encrypt frames before they leave the device. The SFU sees routing metadata — never plaintext.
Network resilience
The SDK auto-reconnects on transient network loss with exponential backoff (250 ms → 8 s, ±25 % jitter) and restarts ICE on resume. You can listen in:
live.room.on('connection-state', (s) => {
// s ∈ 'connected' | 'reconnecting' | 'disconnected' | 'failed'
});
live.room.on('connection-quality', (id, q) => {
// q.label ∈ 'excellent' | 'good' | 'fair' | 'poor' | 'disconnected'
});Error handling
import { LevelChatError } from '@levelchat/web';
try {
await live.publishCamera();
} catch (err) {
if (err instanceof LevelChatError) {
if (err.code === 'media/permission-denied') promptForCamera();
if (err.code === 'transport/ice-failed') retryWithTurn();
if (err.code === 'room/full') showFullRoomMessage();
}
}Every error is a LevelChatError with .code, .message, and .retryable. Full code list:
LevelChatErrorCode.
Bundle size
The vanilla SDK is tree-shakeable — importing only LevelChat ships ~52 kB gz.
@levelchat/web/headless (no DOM dependencies, used by bots and SSR) is 30 kB gz.
@levelchat/web/noise-suppression is a separate entry point that lazy-loads ~250 kB of
RNNoise WASM only when the operator flips on AI noise gating.
Browser support
| Browser | Minimum version | Notes |
|---|---|---|
| Chrome / Chromium | 100+ | Full feature set including Insertable Streams |
| Edge | 100+ | Same as Chrome |
| Firefox | 115+ | All except SVC encoding (browser limitation) |
| Safari (macOS / iOS) | 16.4+ | Insertable Streams + AV1 decode (where hardware allows) |
| WebView (mobile in-app) | Chrome 100+ engine | Test the host app's WebView version |