LevelChatDocs
Docs
Web SDK

Web SDK

TypeScript SDK for browsers.

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/web
~
npm install @levelchat/web @levelchat/web-react

Connect

TypeScript
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

TypeScript
// 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

TypeScript
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

TypeScript
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.

TypeScript (TSX)
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

TypeScript
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:

TypeScript
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

TypeScript
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

BrowserMinimum versionNotes
Chrome / Chromium100+Full feature set including Insertable Streams
Edge100+Same as Chrome
Firefox115+All except SVC encoding (browser limitation)
Safari (macOS / iOS)16.4+Insertable Streams + AV1 decode (where hardware allows)
WebView (mobile in-app)Chrome 100+ engineTest the host app's WebView version