xiaohongshu
xiaohongshu
livekit-nextjs-frontend
Build and review production-grade web and mobile frontends using LiveKit with Next.js. Covers real-time video/audio/data communication, WebRTC connections, track management, and best practices for LiveKit React components.
Full skill instructions
This skill guides the development and review of production-grade web and mobile frontends using LiveKit with Next.js. Use this when building real-time communication features including video conferencing, live streaming, audio rooms, or data synchronization.
LiveKit is a WebRTC-based platform for building real-time video, audio, and data applications. The official React components library (@livekit/components-react) provides battle-tested hooks and components for Next.js applications.
Latest Versions (as of 2025):
@livekit/components-react: v2.9.16+livekit-client: Latestlivekit-server-sdk: v2+ (supports Node.js, Deno, and Bun){
"dependencies": {
"livekit-client": "latest",
"@livekit/components-react": "latest",
"livekit-server-sdk": "latest"
},
"devDependencies": {
"tailwindcss": "latest",
"autoprefixer": "latest",
"postcss": "latest"
}
}
Optional (for custom UI with icons):
npm install lucide-react
The examples use Tailwind CSS for styling and lucide-react for icons. These are optional - you can use your own styling solution and icons/text alternatives.
LiveKit uses JWT-based access tokens signed with your API secret. Tokens must be generated server-side to prevent secret exposure.
Environment Setup (.env.local):
# Client-accessible (for LiveKitRoom component)
NEXT_PUBLIC_LIVEKIT_URL=wss://your-project.livekit.cloud
# Server-only (never exposed to client)
LIVEKIT_API_KEY=your-api-key
LIVEKIT_API_SECRET=your-api-secret
Note: For server-side features like recording, you may also need:
LIVEKIT_URL=wss://your-project.livekit.cloud
Token Generation API Route (app/api/token/route.ts):
import { AccessToken } from 'livekit-server-sdk';
import { NextRequest, NextResponse } from 'next/server';
export async function GET(request: NextRequest) {
const roomName = request.nextUrl.searchParams.get('room');
const participantName = request.nextUrl.searchParams.get('username');
if (!roomName || !participantName) {
return NextResponse.json(
{ error: 'Missing room or username' },
{ status: 400 }
);
}
const at = new AccessToken(
process.env.LIVEKIT_API_KEY!,
process.env.LIVEKIT_API_SECRET!,
{
identity: participantName,
ttl: '6h', // Token expires after 6 hours
}
);
// Set permissions
at.addGrant({
roomJoin: true,
room: roomName,
canPublish: true,
canSubscribe: true,
canPublishData: true,
});
const token = await at.toJwt();
return NextResponse.json({ token });
}
Security Best Practices:
Basic Room Component:
'use client';
import { LiveKitRoom, VideoConference } from '@livekit/components-react';
import '@livekit/components-styles';
import { useEffect, useState } from 'react';
interface RoomPageProps {
roomName: string;
username: string;
}
export default function RoomPage({ roomName, username }: RoomPageProps) {
const [token, setToken] = useState('');
useEffect(() => {
// Fetch token from API route
fetch(`/api/token?room=${roomName}&username=${username}`)
.then(res => res.json())
.then(data => setToken(data.token));
}, [roomName, username]);
if (!token) {
return <div>Loading...</div>;
}
return (
<LiveKitRoom
token={token}
serverUrl={process.env.NEXT_PUBLIC_LIVEKIT_URL!}
connect={true}
video={true}
audio={true}
onDisconnected={() => {
// Handle disconnection
}}
onError={(error) => {
console.error('Room error:', error);
}}
>
<VideoConference />
</LiveKitRoom>
);
}
CRITICAL BEST PRACTICE: Always use LiveKit's provided hooks instead of creating custom implementations. These hooks manage React state and are rigorously tested.
Essential Hooks:
useRoom() - Access room state and eventsuseTracks() - Subscribe to track updatesuseParticipants() - Get participant listuseLocalParticipant() - Access local participantuseTrackToggle() - Toggle audio/videouseLiveKitRoom() - Lower-level room managementCustom Controls Example:
'use client';
import { useRoom, useLocalParticipant, useTrackToggle } from '@livekit/components-react';
import { Track } from 'livekit-client';
export function CustomControls() {
const room = useRoom();
const { localParticipant } = useLocalParticipant();
// Use built-in hook for track toggling
const { buttonProps: audioProps, enabled: audioEnabled } = useTrackToggle({
source: Track.Source.Microphone,
});
const { buttonProps: videoProps, enabled: videoEnabled } = useTrackToggle({
source: Track.Source.Camera,
});
return (
<div className="controls">
<button {...audioProps}>
{audioEnabled ? 'Mute' : 'Unmute'}
</button>
<button {...videoProps}>
{videoEnabled ? 'Stop Video' : 'Start Video'}
</button>
<button onClick={() => room.disconnect()}>
Leave Room
</button>
</div>
);
}
Publishing Tracks:
import { useLocalParticipant } from '@livekit/components-react';
import { Track } from 'livekit-client';
function ScreenShareButton() {
const { localParticipant } = useLocalParticipant();
const startScreenShare = async () => {
await localParticipant.setScreenShareEnabled(true);
};
const stopScreenShare = async () => {
await localParticipant.setScreenShareEnabled(false);
};
return (
<button onClick={startScreenShare}>Share Screen</button>
);
}
Subscribing to Remote Tracks:
import { useTracks, VideoTrack } from '@livekit/components-react';
import { Track } from 'livekit-client';
function RemoteParticipants() {
// Subscribe to all camera tracks
const tracks = useTracks([
{ source: Track.Source.Camera, withPlaceholder: true }
]);
return (
<div className="participants-grid">
{tracks.map((track) => (
<VideoTrack key={track.participant.sid} trackRef={track} />
))}
</div>
);
}
IMPORTANT: LiveKit recommends using higher-level APIs like text streams, byte streams, or RPC for most use cases. Use the low-level publishData API only when you need advanced control over individual packet behavior.
Message Size Limits:
Sending Data:
import { useLocalParticipant } from '@livekit/components-react';
function ChatComponent() {
const { localParticipant } = useLocalParticipant();
const sendMessage = (message: string) => {
const encoder = new TextEncoder();
const data = encoder.encode(JSON.stringify({ message }));
// Validate size (16KiB limit for reliable messages)
if (data.byteLength > 16 * 1024) {
console.error('Message too large');
return;
}
// Use topic to differentiate message types
localParticipant.publishData(data, {
reliable: true, // Reliable delivery with retransmission
topic: 'chat', // Topic helps filter different message types
});
};
return (
<button onClick={() => sendMessage('Hello!')}>
Send Message
</button>
);
}
Receiving Data:
import { useRoom } from '@livekit/components-react';
import { useEffect, useState } from 'react';
import { RemoteParticipant } from 'livekit-client';
function ChatDisplay() {
const room = useRoom();
const [messages, setMessages] = useState<string[]>([]);
useEffect(() => {
const handleData = (
payload: Uint8Array,
participant?: RemoteParticipant,
kind?: any,
topic?: string
) => {
// Filter by topic
if (topic !== 'chat') return;
const decoder = new TextDecoder();
const data = JSON.parse(decoder.decode(payload));
setMessages(prev => [...prev, data.message]);
};
room.on('dataReceived', handleData);
return () => {
room.off('dataReceived', handleData);
};
}, [room]);
return (
<div>
{messages.map((msg, i) => (
<div key={i}>{msg}</div>
))}
</div>
);
}
Delivery Modes:
reliable: true): Packets delivered in order with retransmission. Best for chat, critical updates.reliable: false): Each packet sent once, no ordering guarantee. Best for real-time updates where speed matters more than delivery.When reviewing LiveKit Next.js code, verify:
useTrackToggle, useTracks) instead of custom implementationsfunction PreJoinScreen({ onJoin }: { onJoin: (username: string) => void }) {
const [username, setUsername] = useState('');
const [devices, setDevices] = useState({ audio: true, video: true });
return (
<div>
<input
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="Enter your name"
/>
<label>
<input
type="checkbox"
checked={devices.audio}
onChange={(e) => setDevices({ ...devices, audio: e.target.checked })}
/>
Enable Microphone
</label>
<label>
<input
type="checkbox"
checked={devices.video}
onChange={(e) => setDevices({ ...devices, video: e.target.checked })}
/>
Enable Camera
</label>
<button onClick={() => onJoin(username)}>
Join Room
</button>
</div>
);
}
import { useTracks, VideoTrack } from '@livekit/components-react';
import { Track } from 'livekit-client';
function ActiveSpeakerView() {
const tracks = useTracks([
{ source: Track.Source.Camera, withPlaceholder: true }
]);
// Sort by speaking status and audio level
const sortedTracks = tracks.sort((a, b) => {
if (a.participant.isSpeaking && !b.participant.isSpeaking) return -1;
if (!a.participant.isSpeaking && b.participant.isSpeaking) return 1;
return (b.participant.audioLevel || 0) - (a.participant.audioLevel || 0);
});
return (
<div>
{/* Show active speaker large */}
{sortedTracks[0] && (
<VideoTrack
trackRef={sortedTracks[0]}
className="active-speaker"
/>
)}
{/* Show others small */}
<div className="other-participants">
{sortedTracks.slice(1).map(track => (
<VideoTrack
key={track.participant.sid}
trackRef={track}
/>
))}
</div>
</div>
);
}
// Server-side API route for starting recording
import { RoomServiceClient } from 'livekit-server-sdk';
import { NextRequest, NextResponse } from 'next/server';
export async function POST(request: NextRequest) {
const { roomName } = await request.json();
const client = new RoomServiceClient(
process.env.LIVEKIT_URL!,
process.env.LIVEKIT_API_KEY!,
process.env.LIVEKIT_API_SECRET!
);
try {
const egressId = await client.startRoomCompositeEgress(roomName, {
file: {
filepath: `recordings/${roomName}-${Date.now()}.mp4`,
},
});
return NextResponse.json({ egressId });
} catch (error) {
return NextResponse.json({ error: 'Failed to start recording' }, { status: 500 });
}
}
Problem: Room fails to connect
NEXT_PUBLIC_LIVEKIT_URL uses wss:// protocol (for client-side connections)Problem: Camera/microphone not working
localParticipant.videoTrackPublicationsProblem: Video quality poor or choppy
localParticipant.publishTrack(track, { simulcast: true })Problem: Data messages not received
canPublishData permission in token@livekit/react-native package insteadWhen building a LiveKit feature:
Plan Architecture
Set Up Authentication
Build Core Components
Add Advanced Features
Test Thoroughly
Optimize & Polish
Remember: LiveKit handles the complex WebRTC infrastructure. Focus on building excellent user experiences with their battle-tested components and hooks.
xiaohongshu
technical spec
product ux expert
database patterns
Conduct multi-agent task orchestration and workflow coordination.
Initialize project with Conductor artifacts (product definition,
Expert in web animations, transitions, and motion design using Framer Motion and CSS
Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...
PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.
Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...
Display project status, active tracks, and next actions
Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.
Generate, edit, and beat-sync AI video with leading models in one workspace.
The world's fastest calendar for remote work
Transform Your Design with AI Designer by ImgCreator.ai
Revolutionizing Video Production with AI-Powered Creativity
Extend an image past the frame and let AI fill the new aspect ratio.
Discover your celebrity doppelgänger with StarByFace!
ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.
Opus.ai: Revolutionize Your Web Experience