Web calls with LiveKit

In the browser, poolse calls work like Google Meet. A conversation has a room that members drop into for voice or video while the chat carries on. Nobody's phone rings unless you ask for it. Phones join the same room, and web users can join a call that phones started.

The audio and video go through LiveKit. You install livekit-client and hand it to the SDK. poolse issues the media tokens, so you never run or configure a LiveKit server yourself.

Before you start, chat should already work with PoolseProvider. See the Quickstart.

Install

npm install @poolse/sdk @poolse/react @poolse/react-ui livekit-client

livekit-client is passed in rather than bundled, so you choose its version. The reference app uses livekit-client@^2.22.0.

Drop-in UI

useRoom is the room for one conversation. <RoomJoinBar> is the "Start a room / Join" bar, and <RoomStage> shows everyone's video and the controls once you are in.

'use client';
import * as livekit from 'livekit-client';
import { useRoom } from '@poolse/react';
import { ConversationView, RoomJoinBar, RoomStage } from '@poolse/react-ui';
import '@poolse/react-ui/styles.css';

export function ChatWithRoom({ conversationId, title }: { conversationId: string; title: string }) {
  const room = useRoom(conversationId, { livekit });

  return (
    <div className="chat">
      <RoomJoinBar room={room} />
      {room.joined && <RoomStage room={room} title={title} />}
      <ConversationView conversationId={conversationId} />
    </div>
  );
}
  • With no call live, <RoomJoinBar> offers to start a voice or video room. Pass allowStart={false} to show only a "Join" banner while something is live.
  • While a call or room is live, it shows what kind it is, how many people are in it, and Join.
  • <RoomStage> has the video grid and microphone, camera and output controls. Pass labelFor={(userId) => name} to show names.
  • Switching conversationId leaves the previous room.

useRoom

const room = useRoom(conversationId, { livekit });
FieldMeaning
liveThe call or room running on the conversation right now, whether or not this user is in it, or null. Same shape as call in call:started.
presentIdsPoolse user ids in it right now.
joinedThis user is in it, or connecting.
joiningA start or join is in flight.
join(media?, { ring? })Start the room, or join whatever is live. media is 'audio' (default) or 'video'; video turns the camera on as you arrive.
leave()Leave. The room carries on for everyone else.
mediaThe media side (see below).
errorThe last error, or null.

join() calls POST /v1/conversations/:id/room, then connects to the media server. If a phone-style call is already ringing on the conversation, join() joins that call. A room ends on its own once it is empty.

Ringing phones from the web

Rooms never ring. To ring the other members' phones (a support desk calling a customer, for example), pass ring:

await room.join('audio', { ring: true }); // POST /v1/conversations/:id/calls

This places a real call: their phones ring through push and it ends like a call does. Web browsers are never rung. There is no incoming-call screen for the web.

The media: room.media

room.media (the same object useCallRoom returns) is what people sound and look like:

FieldMeaning
statusidle, connecting, connected or error.
participantsEveryone in the call, you first: { userId, name, isSelf, micEnabled, cameraEnabled, speaking, videoTrack }.
micEnabled, cameraEnabled, facingModeYour own state.
setMicrophoneEnabled(on), toggleMic()Microphone.
setCameraEnabled(on), toggleCamera(), switchCamera()Camera. Both can be turned on and off at any time.
devices, activeDevices, selectDevice(kind, id), refreshDevices()Cameras (videoinput), microphones (audioinput) and speakers (audiooutput).
sfuAvailablefalse if the deployment has no media server (503 sfu_not_configured).
errorThe last media error.

Browsers only reveal device names once the page has camera or microphone permission. Call refreshDevices() when you open a device picker.

When the workspace has video off, every call is audio-only and setCameraEnabled(true) cannot publish. Check room.live?.video_allowed and hide the camera button.

Building your own UI

The pieces under <RoomStage> are exported too:

  • <CallVideoGrid participants={room.media.participants} labelFor={…} /> draws each person's video, or their initial while the camera is off.
  • <CallControlButton> and <CallDeviceMenu> are the round buttons and device menu.
  • <LiveCallBanner call={room.live} inCall={room.joined} onJoin={() => room.join()} /> is the banner alone.

To draw video yourself, attach a participant's videoTrack to a <video> element with LiveKit's track.attach(element).

For full control without React, @poolse/sdk has the same parts:

const session = await poolse.rooms.start(conversationId, 'video'); // or rooms.ring(...)
const call = poolse.call(conversationId, { livekit, video: true });
await call.join();                     // false when there is no media server
call.onParticipants((ps) => render(ps));
// …
call.leave();
await poolse.rooms.leave(session);     // POST /v1/calls/:id/hangup with the control token

Joining from a conversation view

<ConversationView> can show the "Join" banner and call-log rows for you:

<ConversationView
  conversationId={conversationId}
  onJoinCall={() => room.join()}
  currentCallId={room.joined ? room.live?.id ?? null : null}
/>

Call-log rows ("Missed call", "Video call · 4:20") come from the system message poolse writes when a call ends. See the call log.

Limits and errors

SituationWhat happens
The call is full (max_participants, default 16)409 call_full. Someone already in it can always rejoin.
The user is not a member403 forbidden.
The deployment has no media server503 sfu_not_configured; sfuAvailable becomes false.
The browser blocks the camera or microphoneroom.media.error is set. Ask the user to allow access.

Checklist

  • livekit-client installed and passed as { livekit }.
  • @poolse/react-ui/styles.css imported.
  • Camera button hidden when video_allowed is false.
  • Device picker calls refreshDevices() when it opens.
  • Decide whether the web should ever ring phones ({ ring: true }).
Web calls with LiveKit · Guides · poolse docs