LiveKit Integration for Video Conferencing on Website
LiveKit is an open-source WebRTC server for building video conferences, streams and voice calls. Self-hosted variant with no participant limits and no revenue share.
Architecture
Client (browser)
│
└── WebRTC ──► LiveKit Server (SFU)
│
LiveKit API ◄── Your backend
(room management, access tokens)
Installing LiveKit Server
# docker-compose.yml
services:
livekit:
image: livekit/livekit-server:latest
command: --dev # for development
ports:
- "7880:7880" # HTTP
- "7881:7881" # TCP for WebRTC
- "7882:7882/udp" # UDP for WebRTC
environment:
LIVEKIT_KEYS: "APIkey: APIsecret"
# livekit.yaml (production)
port: 7880
rtc:
tcp_port: 7881
udp_port: 7882
use_external_ip: true
keys:
APIkey: APIsecret
Backend: Token Generation
import { AccessToken, RoomServiceClient } from 'livekit-server-sdk';
const roomService = new RoomServiceClient(
process.env.LIVEKIT_URL,
process.env.LIVEKIT_API_KEY,
process.env.LIVEKIT_API_SECRET
);
// Create room
await roomService.createRoom({
name: `room-${meetingId}`,
maxParticipants: 50,
emptyTimeout: 10 * 60 // close after 10 min if empty
});
// Access token for participant
function generateToken(roomName: string, participant: Participant): string {
const at = new AccessToken(
process.env.LIVEKIT_API_KEY,
process.env.LIVEKIT_API_SECRET,
{
identity: participant.id,
name: participant.name,
ttl: '2h'
}
);
at.addGrant({
roomJoin: true,
room: roomName,
canPublish: true,
canSubscribe: true,
canPublishData: true
});
return at.toJwt();
}
// Endpoint to get token
app.post('/api/meetings/:id/join', authenticate, async (req, res) => {
const meeting = await meetingRepo.findById(req.params.id);
if (!meeting) return res.status(404).json({ error: 'Meeting not found' });
const token = generateToken(
`room-${meeting.id}`,
{ id: req.user.id, name: req.user.displayName }
);
res.json({
token,
serverUrl: process.env.LIVEKIT_URL
});
});
Client (React)
import {
LiveKitRoom,
VideoConference,
GridLayout,
ParticipantTile,
RoomAudioRenderer,
ControlBar,
useTracks
} from '@livekit/components-react';
import { Track } from 'livekit-client';
import '@livekit/components-styles';
function VideoRoom({ roomId }) {
const [token, setToken] = useState<string | null>(null);
const [serverUrl, setServerUrl] = useState('');
useEffect(() => {
fetch(`/api/meetings/${roomId}/join`, { method: 'POST' })
.then(r => r.json())
.then(({ token, serverUrl }) => {
setToken(token);
setServerUrl(serverUrl);
});
}, [roomId]);
if (!token) return <div>Connecting...</div>;
return (
<LiveKitRoom
token={token}
serverUrl={serverUrl}
connect={true}
onDisconnected={() => router.push('/meetings')}
video={true}
audio={true}
>
<VideoConference />
<RoomAudioRenderer />
</LiveKitRoom>
);
}
// Custom interface
function CustomVideoRoom({ roomId }) {
return (
<LiveKitRoom token={token} serverUrl={serverUrl} connect>
<MyVideoGrid />
<ControlBar />
<RoomAudioRenderer />
</LiveKitRoom>
);
}
function MyVideoGrid() {
const tracks = useTracks([
{ source: Track.Source.Camera, withPlaceholder: true },
{ source: Track.Source.ScreenShare }
]);
return (
<GridLayout tracks={tracks}>
<ParticipantTile />
</GridLayout>
);
}
Recording
// Record room via Egress API
import { EgressClient, EncodedFileType } from 'livekit-server-sdk';
const egressClient = new EgressClient(
process.env.LIVEKIT_URL,
process.env.LIVEKIT_API_KEY,
process.env.LIVEKIT_API_SECRET
);
const egress = await egressClient.startRoomCompositeEgress(
`room-${meetingId}`,
{
file: {
fileType: EncodedFileType.MP4,
s3: {
accessKey: process.env.AWS_ACCESS_KEY,
secret: process.env.AWS_SECRET,
bucket: 'recordings',
key: `meetings/${meetingId}/${Date.now()}.mp4`
}
}
}
);
Timeline
LiveKit server + token generation + React UI with @livekit/components-react: 1 week. Custom UI + recording + room management: 2–3 weeks.







