07 β€” Web (JavaScript / React)

Add calls to a website or web app. Like every client, your web app gets a token from your backend (see Node / Laravel), then connects with LiveKit's browser SDK.

Your API key/secret stay on your backend. The browser only ever sees a token. HTTPS is required for camera/mic (browsers only allow getUserMedia on secure origins β€” https:// or localhost).


Option A β€” React (fastest, recommended)

Prebuilt components handle layout, rendering, and controls for you.

Install

npm install @livekit/components-react@^2.9 @livekit/components-styles@^1.2 livekit-client@^2.20

Requires React β‰₯ 18.

A complete call component

import '@livekit/components-styles';
import {
  LiveKitRoom, GridLayout, ParticipantTile,
  RoomAudioRenderer, ControlBar, useTracks,
} from '@livekit/components-react';
import { Track } from 'livekit-client';

function Conference() {
  // Camera + screenshare tracks from everyone (incl. you).
  const tracks = useTracks(
    [
      { source: Track.Source.Camera, withPlaceholder: true },
      { source: Track.Source.ScreenShare, withPlaceholder: false },
    ],
    { onlySubscribed: false },
  );

  return (
    <GridLayout tracks={tracks} style={{ height: 'calc(100vh - var(--lk-control-bar-height))' }}>
      <ParticipantTile />
    </GridLayout>
  );
}

export default function CallRoom({ token }: { token: string }) {
  return (
    <LiveKitRoom
      token={token}
      serverUrl="wss://calls.arzbridge.com"
      connect={true}
      video={true}
      audio={true}
      data-lk-theme="default"
      style={{ height: '100vh' }}
      onDisconnected={() => console.log('left the call')}
    >
      <Conference />
      <RoomAudioRenderer />   {/* plays remote audio */}
      <ControlBar />          {/* mic/cam/screenshare/leave buttons */}
    </LiveKitRoom>
  );
}

Get the token, then render

import { useEffect, useState } from 'react';
import CallRoom from './CallRoom';

export function CallPage({ roomId }: { roomId: string }) {
  const [token, setToken] = useState<string | null>(null);

  useEffect(() => {
    fetch('/api/calls/join', {                         // YOUR backend endpoint
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      credentials: 'include',                          // your own auth
      body: JSON.stringify({ room_id: roomId }),
    })
      .then((r) => r.json())
      .then((d) => setToken(d.token));                 // { token, wsUrl }
  }, [roomId]);

  if (!token) return <div>Joining…</div>;
  return <CallRoom token={token} />;
}

Next.js: components-react is client-side. Put 'use client' at the top of the file, or import the call component with dynamic(() => import('./CallRoom'), { ssr: false }).


Option B β€” Vanilla JavaScript (no framework)

Full control with the core livekit-client SDK.

Install (bundler)

npm install livekit-client@^2.20

…or load straight from a CDN (no build step):

<script type="module">
  import { Room, RoomEvent, Track }
    from 'https://cdn.jsdelivr.net/npm/livekit-client@2.20.0/+esm';
  // ... code below ...
</script>

Connect, publish, render

import { Room, RoomEvent, Track } from 'livekit-client';

async function joinCall(roomId) {
  // 1) token from YOUR backend
  const res = await fetch('/api/calls/join', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    credentials: 'include',
    body: JSON.stringify({ room_id: roomId }),
  });
  const { token, wsUrl } = await res.json();

  // 2) set up the room
  const room = new Room({ adaptiveStream: true, dynacast: true });

  room
    .on(RoomEvent.TrackSubscribed, (track) => {
      if (track.kind === Track.Kind.Video || track.kind === Track.Kind.Audio) {
        document.getElementById('remote').appendChild(track.attach());
      }
    })
    .on(RoomEvent.TrackUnsubscribed, (track) => track.detach().forEach((el) => el.remove()))
    .on(RoomEvent.Disconnected, () => console.log('disconnected'));

  // 3) connect + publish your camera & mic
  await room.connect(wsUrl, token);
  await room.localParticipant.enableCameraAndMicrophone();

  // 4) show your own camera
  const cam = room.localParticipant.getTrackPublication(Track.Source.Camera);
  if (cam?.videoTrack) {
    document.getElementById('local').appendChild(cam.videoTrack.attach());
  }

  return room;
}
<div id="local"></div>
<div id="remote"></div>

Controls

await room.localParticipant.setMicrophoneEnabled(false); // mute
await room.localParticipant.setCameraEnabled(false);     // camera off
await room.localParticipant.setScreenShareEnabled(true); // share screen
await room.disconnect();                                  // leave

Data messages

const enc = new TextEncoder(), dec = new TextDecoder();

// send
await room.localParticipant.publishData(
  enc.encode(JSON.stringify({ type: 'reaction', emoji: 'πŸ‘' })),
  { reliable: true, topic: 'signals' },
);

// receive
room.on(RoomEvent.DataReceived, (payload, participant, _kind, topic) => {
  const msg = JSON.parse(dec.decode(payload));
  // handle msg, participant?.identity, topic
});

Notes & gotchas

  • HTTPS only for camera/mic (or localhost in dev).
  • Audio autoplay: browsers may block audio until a user gesture. With React, <RoomAudioRenderer /> plus a "Join" click handles this; in vanilla JS, start joinCall() from a click. You can also call room.startAudio() after a click.
  • Token expiry: if a connection is rejected, re-fetch a fresh token from your backend (the call itself continues after a token expires).
  • Roles: ask your backend to mint a viewer token (can_publish: false) for webinar attendees, etc. β€” see 01-getting-started.md.
  • Mobile web works too, but for native apps prefer the Flutter SDK (or the native iOS/Android SDKs).

Reference: LiveKit JS SDK Β· React components. arzbridge runs standard LiveKit, so all official SDKs work unchanged.