Claude Code for LiveKit: Real-Time Video and Audio — Claude Skills 360 Blog
Blog / Backend / Claude Code for LiveKit: Real-Time Video and Audio
Backend

Claude Code for LiveKit: Real-Time Video and Audio

Published: April 30, 2027
Read time: 7 min read
By: Claude Skills 360

LiveKit is an open-source WebRTC platform for real-time video and audio — <LiveKitRoom url={wsUrl} token={participantToken}> connects to a room. useParticipants() returns all participants. useTracks([Track.Source.Camera]) subscribes to video tracks. <VideoTrack trackRef={track}> renders a participant’s video. useLocalParticipant() returns localParticipant with setCameraEnabled(true) and setMicrophoneEnabled(true). Access tokens are generated server-side with new AccessToken(apiKey, apiSecret) and token.addGrant({ room, roomJoin: true, canPublish: true }). RoomEvent.TrackSubscribed and RoomEvent.ParticipantConnected handle lifecycle events. Screen sharing uses participant.setScreenShareEnabled(true). Data channel messaging sends arbitrary bytes between participants. useConnectionState() tracks connection status. LiveKit has a hosted cloud offering (LiveKit Cloud) and a self-hosted Docker image. Claude Code generates LiveKit token servers, video grid components, audio-only rooms, and screen sharing patterns.

CLAUDE.md for LiveKit

## LiveKit Stack
- Version: livekit-server-sdk >= 2.5 (server), @livekit/components-react >= 2.5 (client)
- Token: new AccessToken(LIVEKIT_API_KEY, LIVEKIT_API_SECRET, { identity, name }).addGrant({ room, roomJoin: true, canPublish: true })
- Room: <LiveKitRoom url={process.env.NEXT_PUBLIC_LIVEKIT_URL} token={token} connectOptions={{ autoSubscribe: true }}>
- Participants: const participants = useParticipants()
- Tracks: const tracks = useTracks([Track.Source.Camera, Track.Source.ScreenShare])
- Local: const { localParticipant } = useLocalParticipant(); localParticipant.setCameraEnabled(true)
- Events: room.on(RoomEvent.TrackSubscribed, (track, pub, participant) => ...)
- Data: localParticipant.publishData(bytes, { reliable: true })

Token Server

// app/api/livekit/token/route.ts — generate room access tokens
import { NextRequest, NextResponse } from "next/server"
import { AccessToken, RoomServiceClient } from "livekit-server-sdk"
import { auth } from "@clerk/nextjs/server"
import { db } from "@/lib/db"
import { z } from "zod"

const TokenRequestSchema = z.object({
  roomName: z.string().min(1).max(64).regex(/^[a-zA-Z0-9_-]+$/),
  participantName: z.string().min(1).max(50).optional(),
})

export async function POST(request: NextRequest) {
  const { userId } = await auth()
  if (!userId) return NextResponse.json({ error: "Unauthorized" }, { status: 401 })

  const body = TokenRequestSchema.safeParse(await request.json())
  if (!body.success) return NextResponse.json({ error: "Invalid request" }, { status: 400 })

  const { roomName, participantName } = body.data

  const user = await db.user.findUniqueOrThrow({ where: { clerkId: userId } })

  // Create access token — server-side only
  const token = new AccessToken(
    process.env.LIVEKIT_API_KEY!,
    process.env.LIVEKIT_API_SECRET!,
    {
      identity: userId,
      name: participantName ?? user.name,
      ttl: "4h",  // Token valid for 4 hours
    },
  )

  token.addGrant({
    room: roomName,
    roomJoin: true,
    canPublish: true,
    canSubscribe: true,
    canPublishData: true,
    // Screen share — only for paid tiers
    canPublishSources: ["camera", "microphone", ...(user.plan === "pro" ? ["screen_share"] : [])],
  })

  return NextResponse.json({
    token: await token.toJwt(),
    livekitUrl: process.env.NEXT_PUBLIC_LIVEKIT_URL,
  })
}

// Create a room via LiveKit REST API
export async function createRoom(roomName: string) {
  const roomService = new RoomServiceClient(
    process.env.LIVEKIT_URL!,
    process.env.LIVEKIT_API_KEY!,
    process.env.LIVEKIT_API_SECRET!,
  )

  return roomService.createRoom({
    name: roomName,
    emptyTimeout: 300,     // Delete room after 5 minutes empty
    maxParticipants: 50,
    metadata: JSON.stringify({ createdAt: Date.now() }),
  })
}

Video Conference Component

// components/video/VideoConference.tsx — multi-participant video room
"use client"
import { useEffect, useState } from "react"
import {
  LiveKitRoom,
  VideoConference as LiveKitVideoConference,
  GridLayout,
  ParticipantTile,
  RoomAudioRenderer,
  ControlBar,
  useTracks,
  useLocalParticipant,
  useParticipants,
  useConnectionState,
} from "@livekit/components-react"
import "@livekit/components-styles"
import { Track, ConnectionState } from "livekit-client"

interface VideoRoomProps {
  roomName: string
  onLeave: () => void
}

export function VideoRoom({ roomName, onLeave }: VideoRoomProps) {
  const [token, setToken] = useState<string | null>(null)
  const [livekitUrl, setLivekitUrl] = useState<string | null>(null)
  const [error, setError] = useState<string | null>(null)

  useEffect(() => {
    fetch("/api/livekit/token", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ roomName }),
    })
      .then(r => r.json())
      .then(data => {
        if (data.error) throw new Error(data.error)
        setToken(data.token)
        setLivekitUrl(data.livekitUrl)
      })
      .catch(err => setError(err.message))
  }, [roomName])

  if (error) return <div className="text-red-500 p-4">{error}</div>
  if (!token || !livekitUrl) return <div className="p-4">Connecting...</div>

  return (
    <LiveKitRoom
      video={true}
      audio={true}
      token={token}
      serverUrl={livekitUrl}
      connectOptions={{ autoSubscribe: true }}
      onDisconnected={onLeave}
      style={{ height: "100vh" }}
    >
      <CustomVideoGrid />
      <RoomAudioRenderer />
      <ControlBar
        controls={{
          camera: true,
          microphone: true,
          screenShare: true,
          leave: true,
          chat: false,
        }}
      />
    </LiveKitRoom>
  )
}

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

  const participants = useParticipants()
  const { localParticipant } = useLocalParticipant()
  const connectionState = useConnectionState()

  if (connectionState === ConnectionState.Connecting) {
    return (
      <div className="flex-1 flex items-center justify-center">
        <div className="text-center">
          <div className="size-10 border-4 border-primary border-t-transparent rounded-full animate-spin mx-auto mb-3" />
          <p className="text-sm text-muted-foreground">Joining room...</p>
        </div>
      </div>
    )
  }

  return (
    <div className="flex-1 overflow-hidden">
      {/* Participant count */}
      <div className="absolute top-4 left-4 z-10 bg-black/50 text-white text-sm px-3 py-1 rounded-full">
        {participants.length} participant{participants.length !== 1 ? "s" : ""}
      </div>

      {tracks.length <= 4 ? (
        // Small rooms — equal grid
        <div className={`h-full grid gap-2 p-2 ${
          tracks.length === 1 ? "grid-cols-1" :
          tracks.length === 2 ? "grid-cols-2" :
          "grid-cols-2"
        }`}>
          {tracks.map(track => (
            <ParticipantTile
              key={track.participant.identity + track.source}
              trackRef={track}
              className="rounded-xl overflow-hidden"
            />
          ))}
        </div>
      ) : (
        // Large rooms — LiveKit's built-in grid
        <GridLayout tracks={tracks} style={{ height: "100%" }}>
          <ParticipantTile />
        </GridLayout>
      )}
    </div>
  )
}

Audio-Only Room

// components/voice/VoiceRoom.tsx — audio-only podcast/call room
"use client"
import { useParticipants, useLocalParticipant, AudioTrack, useTracks } from "@livekit/components-react"
import { Track } from "livekit-client"

export function VoiceParticipantList() {
  const participants = useParticipants()
  const { localParticipant, isMicrophoneEnabled } = useLocalParticipant()
  const tracks = useTracks([Track.Source.Microphone])

  return (
    <div className="space-y-2">
      {/* Render audio tracks invisibly */}
      {tracks.map(track => (
        <AudioTrack key={track.participant.identity} trackRef={track} />
      ))}

      {/* Participant cards */}
      {participants.map(participant => {
        const isLocal = participant.identity === localParticipant.identity
        const isMuted = isLocal ? !isMicrophoneEnabled : participant.isMicrophoneEnabled === false

        return (
          <div
            key={participant.identity}
            className="flex items-center gap-3 p-3 rounded-lg bg-muted"
          >
            <div className={`size-10 rounded-full flex items-center justify-center text-white font-semibold ${participant.isSpeaking ? "bg-green-500" : "bg-primary"}`}>
              {participant.name?.charAt(0) ?? "?"}
            </div>
            <div className="flex-1">
              <p className="text-sm font-medium">{participant.name} {isLocal && "(You)"}</p>
              {participant.isSpeaking && <p className="text-xs text-green-600">Speaking...</p>}
            </div>
            {isMuted && <span className="text-xs text-muted-foreground">🔇 Muted</span>}
          </div>
        )
      })}
    </div>
  )
}

For the Daily.co alternative when a fully managed video API with a pre-built React call UI (<DailyProvider> + <DailyVideo>), recording, transcription, and network quality monitoring are offered as managed services without self-hosting — Daily.co trades LiveKit’s open-source flexibility for a turn-key solution, see the Daily.co guide. For the Agora alternative when a mature, battle-tested RTC SDK with ultra-low latency, massive concurrent user support, and extensive mobile SDK coverage is needed — Agora powers large-scale live streaming and interactive broadcasts where LiveKit targets smaller team video use cases, see the Agora guide. The Claude Skills 360 bundle includes LiveKit skill sets covering token auth, video grid, and screen sharing. Start with the free tier to try real-time video generation.

Keep Reading

Backend

Claude Code for Bun: Fast JavaScript Runtime and Toolkit

Build with Bun and Claude Code — Bun.serve for HTTP servers, Bun.file for fast file I/O, Bun.$ for shell commands, Bun.sql for SQLite and PostgreSQL, Bun.build for bundling, bun:test for testing, Bun.hash for hashing, bun.lock for deterministic installs, bun run for package.json scripts, hot reloading with --hot, bun init for project scaffolding, and compatibility with Node.js modules.

6 min read Jun 13, 2027
Backend

Claude Code for Express.js Advanced: Patterns for Production APIs

Advanced Express.js patterns with Claude Code — typed request handlers with RequestHandler generics, async error handling middleware, Zod validation middleware factory, rate limiting with express-rate-limit and Redis store, helmet security middleware, compression, dependency injection with tsyringe, file upload with multer and S3, pagination utilities, JWT middleware, and structured logging with pino.

6 min read Jun 8, 2027
Backend

Claude Code for KeystoneJS: Node.js CMS and App Framework

Build full-stack apps with KeystoneJS and Claude Code — config with lists, fields.text and fields.relationship for schema definition, access control with isAuthenticated and isAdmin functions, hooks with beforeOperation and afterOperation, GraphQL API auto-generation from schema, AdminUI for content management, session with statelessSessions, Prisma adapter for database, file storage with images and files fields, and custom REST endpoints.

6 min read Jun 7, 2027

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free