Skip to main content
Technology & EngineeringVideo Services445 lines

LiveKit

"LiveKit: real-time video/audio, WebRTC, rooms, tracks, screen sharing, recording, React components"

Quick Summary26 lines
LiveKit is an open-source WebRTC infrastructure platform for building real-time video and audio applications. Unlike video hosting services that handle pre-recorded content, LiveKit focuses on live, interactive communication — video calls, live streams, screen shares, and audio rooms. The architecture revolves around rooms, participants, and tracks. A room is a session where participants connect. Each participant publishes tracks (camera, microphone, screen) and subscribes to others' tracks. LiveKit handles the hard parts of WebRTC: SFU routing, bandwidth estimation, simulcast, and codec negotiation. Your server generates access tokens that control who can join which room and what permissions they have. Use the React SDK for UI, the server SDK for room management, and webhooks for lifecycle events.

## Key Points

- Generate short-lived access tokens (1 hour or less) with the minimum required permissions per participant role.
- Use the `emptyTimeout` room option to auto-close rooms that are no longer in use, preventing resource leaks.
- Pass `identity` consistently across sessions so LiveKit can track reconnections properly.
- Use `contentHint: "detail"` for screen sharing presentations and `contentHint: "motion"` for video content.
- Implement the `onDisconnected` callback to handle network drops gracefully and attempt reconnection.
- Use reliable data messages for chat and signaling; use unreliable for high-frequency updates like cursor positions.
- Store room metadata as JSON for structured data that all participants and your server can access.
- Use egress to S3 or compatible storage rather than recording client-side for reliability and quality.
- Do not expose your API key and secret on the client side; generate tokens on the server only.
- Do not create rooms without `emptyTimeout`; orphaned rooms consume server resources indefinitely.
- Do not subscribe to all tracks when building an audio-only experience; filter by `Track.Source` to reduce bandwidth.
- Do not use `canPublish: true` for audience members in broadcast scenarios; it wastes resources and creates security risks.

## Quick Example

```
LIVEKIT_URL=wss://your-project.livekit.cloud
LIVEKIT_API_KEY=your-api-key
LIVEKIT_API_SECRET=your-api-secret
```
skilldb get video-services-skills/LiveKitFull skill: 445 lines
Paste into your CLAUDE.md or agent config

LiveKit

Core Philosophy

LiveKit is an open-source WebRTC infrastructure platform for building real-time video and audio applications. Unlike video hosting services that handle pre-recorded content, LiveKit focuses on live, interactive communication — video calls, live streams, screen shares, and audio rooms. The architecture revolves around rooms, participants, and tracks. A room is a session where participants connect. Each participant publishes tracks (camera, microphone, screen) and subscribes to others' tracks. LiveKit handles the hard parts of WebRTC: SFU routing, bandwidth estimation, simulcast, and codec negotiation. Your server generates access tokens that control who can join which room and what permissions they have. Use the React SDK for UI, the server SDK for room management, and webhooks for lifecycle events.

Setup

Install the server SDK and React components:

// Server-side
// "@livekit/server-sdk": "^2.0.0"

// Client-side
// "@livekit/components-react": "^2.0.0"
// "livekit-client": "^2.0.0"

Configure credentials:

import { AccessToken, RoomServiceClient } from "livekit-server-sdk";

const LIVEKIT_URL = process.env.LIVEKIT_URL!; // wss://your-project.livekit.cloud
const LIVEKIT_API_KEY = process.env.LIVEKIT_API_KEY!;
const LIVEKIT_API_SECRET = process.env.LIVEKIT_API_SECRET!;

const roomService = new RoomServiceClient(
  LIVEKIT_URL,
  LIVEKIT_API_KEY,
  LIVEKIT_API_SECRET
);

Environment variables required:

LIVEKIT_URL=wss://your-project.livekit.cloud
LIVEKIT_API_KEY=your-api-key
LIVEKIT_API_SECRET=your-api-secret

Key Techniques

Generating Access Tokens

async function createRoomToken(
  roomName: string,
  participantName: string,
  participantIdentity: string,
  options: {
    canPublish?: boolean;
    canSubscribe?: boolean;
    canPublishData?: boolean;
    ttlSeconds?: number;
  } = {}
) {
  const {
    canPublish = true,
    canSubscribe = true,
    canPublishData = true,
    ttlSeconds = 3600,
  } = options;

  const token = new AccessToken(LIVEKIT_API_KEY, LIVEKIT_API_SECRET, {
    identity: participantIdentity,
    name: participantName,
    ttl: ttlSeconds,
  });

  token.addGrant({
    room: roomName,
    roomJoin: true,
    canPublish,
    canSubscribe,
    canPublishData,
  });

  return token.toJwt();
}

// Generate a view-only token (audience member)
async function createViewerToken(roomName: string, identity: string) {
  return createRoomToken(roomName, identity, identity, {
    canPublish: false,
    canSubscribe: true,
    canPublishData: false,
  });
}

Room Management (Server-Side)

async function manageRooms() {
  // Create a room
  const room = await roomService.createRoom({
    name: "meeting-abc-123",
    emptyTimeout: 300, // close after 5 min empty
    maxParticipants: 50,
    metadata: JSON.stringify({ createdBy: "user-456", type: "meeting" }),
  });

  // List all active rooms
  const rooms = await roomService.listRooms();

  // Get participants in a room
  const participants = await roomService.listParticipants("meeting-abc-123");

  // Remove a participant
  await roomService.removeParticipant("meeting-abc-123", "participant-identity");

  // Update room metadata
  await roomService.updateRoomMetadata(
    "meeting-abc-123",
    JSON.stringify({ status: "in-progress" })
  );

  // Delete a room
  await roomService.deleteRoom("meeting-abc-123");

  return { room, rooms, participants };
}

// Mute a participant's track from the server
async function muteParticipant(roomName: string, identity: string) {
  const participant = await roomService.getParticipant(roomName, identity);
  const audioTrack = participant.tracks.find(
    (t) => t.source === "MICROPHONE"
  );

  if (audioTrack?.sid) {
    await roomService.mutePublishedTrack(roomName, identity, audioTrack.sid, true);
  }
}

React Video Conference Component

"use client";

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

interface MeetingRoomProps {
  token: string;
  serverUrl: string;
  onDisconnected?: () => void;
}

function MeetingRoom({ token, serverUrl, onDisconnected }: MeetingRoomProps) {
  return (
    <LiveKitRoom
      token={token}
      serverUrl={serverUrl}
      connect={true}
      video={true}
      audio={true}
      onDisconnected={onDisconnected}
      data-lk-theme="default"
      style={{ height: "100vh" }}
    >
      <VideoConference />
    </LiveKitRoom>
  );
}

Custom Video Layout

"use client";

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

function CustomVideoGrid() {
  const tracks = useTracks(
    [
      { source: Track.Source.Camera, withPlaceholder: true },
      { source: Track.Source.ScreenShare, withPlaceholder: false },
    ],
    { onlySubscribed: false }
  );

  return (
    <div style={{ display: "flex", flexDirection: "column", height: "100vh" }}>
      <div style={{ flex: 1 }}>
        <GridLayout tracks={tracks} style={{ height: "100%" }}>
          <ParticipantTile />
        </GridLayout>
      </div>
      <RoomAudioRenderer />
      <ControlBar
        controls={{
          camera: true,
          microphone: true,
          screenShare: true,
          leave: true,
          chat: true,
        }}
      />
    </div>
  );
}

Screen Sharing

"use client";

import { useLocalParticipant } from "@livekit/components-react";
import { useState } from "react";

function ScreenShareButton() {
  const { localParticipant } = useLocalParticipant();
  const [isSharing, setIsSharing] = useState(false);

  const toggleScreenShare = async () => {
    if (isSharing) {
      await localParticipant.setScreenShareEnabled(false);
      setIsSharing(false);
    } else {
      await localParticipant.setScreenShareEnabled(true, {
        audio: true, // Share system audio too
        resolution: { width: 1920, height: 1080, frameRate: 30 },
        contentHint: "detail", // Optimize for text/slides
      });
      setIsSharing(true);
    }
  };

  return (
    <button onClick={toggleScreenShare}>
      {isSharing ? "Stop Sharing" : "Share Screen"}
    </button>
  );
}

Data Messages Between Participants

"use client";

import { useLocalParticipant, useRoomContext } from "@livekit/components-react";
import { DataPacket_Kind, RoomEvent } from "livekit-client";
import { useEffect, useState, useCallback } from "react";

interface ChatMessage {
  sender: string;
  text: string;
  timestamp: number;
}

function useDataChannel() {
  const room = useRoomContext();
  const { localParticipant } = useLocalParticipant();
  const [messages, setMessages] = useState<ChatMessage[]>([]);

  useEffect(() => {
    const handleDataReceived = (
      payload: Uint8Array,
      participant: any,
    ) => {
      const decoded = new TextDecoder().decode(payload);
      const message: ChatMessage = JSON.parse(decoded);
      setMessages((prev) => [...prev, message]);
    };

    room.on(RoomEvent.DataReceived, handleDataReceived);
    return () => {
      room.off(RoomEvent.DataReceived, handleDataReceived);
    };
  }, [room]);

  const sendMessage = useCallback(
    async (text: string) => {
      const message: ChatMessage = {
        sender: localParticipant.identity,
        text,
        timestamp: Date.now(),
      };

      const encoded = new TextEncoder().encode(JSON.stringify(message));
      await localParticipant.publishData(encoded, {
        reliable: true,
      });

      setMessages((prev) => [...prev, message]);
    },
    [localParticipant]
  );

  return { messages, sendMessage };
}

Recording and Egress

import { EgressClient, EncodedFileOutput, SegmentedFileOutput } from "livekit-server-sdk";

const egressClient = new EgressClient(LIVEKIT_URL, LIVEKIT_API_KEY, LIVEKIT_API_SECRET);

async function startRoomRecording(roomName: string) {
  const output: EncodedFileOutput = {
    fileType: 0, // MP4
    filepath: `recordings/${roomName}-{time}.mp4`,
    s3: {
      accessKey: process.env.AWS_ACCESS_KEY_ID!,
      secret: process.env.AWS_SECRET_ACCESS_KEY!,
      region: "us-east-1",
      bucket: "my-recordings-bucket",
    },
  };

  const egress = await egressClient.startRoomCompositeEgress(roomName, {
    file: output,
  }, {
    layout: "grid",
    audioOnly: false,
  });

  return { egressId: egress.egressId };
}

async function startHlsStream(roomName: string) {
  const output: SegmentedFileOutput = {
    filenamePrefix: `live/${roomName}/segment`,
    playlistName: "index.m3u8",
    segmentDuration: 4,
    s3: {
      accessKey: process.env.AWS_ACCESS_KEY_ID!,
      secret: process.env.AWS_SECRET_ACCESS_KEY!,
      region: "us-east-1",
      bucket: "my-live-bucket",
    },
  };

  const egress = await egressClient.startRoomCompositeEgress(roomName, {
    segments: output,
  });

  return { egressId: egress.egressId };
}

async function stopRecording(egressId: string) {
  await egressClient.stopEgress(egressId);
}

Webhook Handling

import { WebhookReceiver } from "livekit-server-sdk";
import type { NextApiRequest, NextApiResponse } from "next";

const webhookReceiver = new WebhookReceiver(LIVEKIT_API_KEY, LIVEKIT_API_SECRET);

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  const authHeader = req.headers.authorization as string;
  const body = typeof req.body === "string" ? req.body : JSON.stringify(req.body);

  let event;
  try {
    event = await webhookReceiver.receive(body, authHeader);
  } catch {
    return res.status(401).json({ error: "Invalid webhook" });
  }

  switch (event.event) {
    case "room_started":
      console.log(`Room created: ${event.room?.name}`);
      break;
    case "room_finished":
      console.log(`Room closed: ${event.room?.name}`);
      break;
    case "participant_joined":
      console.log(`${event.participant?.identity} joined ${event.room?.name}`);
      break;
    case "participant_left":
      console.log(`${event.participant?.identity} left ${event.room?.name}`);
      break;
    case "track_published":
      console.log(`Track published: ${event.track?.source}`);
      break;
    case "egress_ended":
      console.log(`Recording finished: ${event.egressInfo?.egressId}`);
      break;
  }

  res.status(200).json({ ok: true });
}

Best Practices

  • Generate short-lived access tokens (1 hour or less) with the minimum required permissions per participant role.
  • Use the emptyTimeout room option to auto-close rooms that are no longer in use, preventing resource leaks.
  • Pass identity consistently across sessions so LiveKit can track reconnections properly.
  • Use contentHint: "detail" for screen sharing presentations and contentHint: "motion" for video content.
  • Implement the onDisconnected callback to handle network drops gracefully and attempt reconnection.
  • Use reliable data messages for chat and signaling; use unreliable for high-frequency updates like cursor positions.
  • Store room metadata as JSON for structured data that all participants and your server can access.
  • Use egress to S3 or compatible storage rather than recording client-side for reliability and quality.

Anti-Patterns

  • Do not expose your API key and secret on the client side; generate tokens on the server only.
  • Do not create rooms without emptyTimeout; orphaned rooms consume server resources indefinitely.
  • Do not subscribe to all tracks when building an audio-only experience; filter by Track.Source to reduce bandwidth.
  • Do not use canPublish: true for audience members in broadcast scenarios; it wastes resources and creates security risks.
  • Do not poll for participant lists from the server; use webhooks or client-side events for real-time updates.
  • Do not ignore the onDisconnected event; users need feedback and reconnection logic when the connection drops.
  • Do not use client-side recording as the primary capture method; server-side egress is more reliable and captures all participants.
  • Do not hardcode room names; generate unique names to avoid collisions between concurrent sessions.

Install this skill directly: skilldb add video-services-skills

Get CLI access →