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
getUserMediaon secure origins βhttps://orlocalhost).
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 withdynamic(() => 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
localhostin dev). - Audio autoplay: browsers may block audio until a user gesture. With React,
<RoomAudioRenderer />plus a "Join" click handles this; in vanilla JS, startjoinCall()from a click. You can also callroom.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.