Construire une application de visioconférence en temps réel avec LiveKit et Next.js

La communication vidéo et audio en temps réel est devenue une exigence fondamentale des applications modernes — de la visioconférence aux agents vocaux IA en passant par le streaming en direct. Mais construire ces systèmes à partir de zéro avec WebRTC brut est extrêmement complexe : il faut gérer les serveurs TURN/STUN, négocier les sessions et gérer les cas limites réseau.
LiveKit résout ce problème en fournissant une infrastructure open source pour la communication en temps réel. Il gère toute la complexité WebRTC et offre des API simples pour créer des salles, gérer les participants et diffuser les médias. Avec des composants React prêts à l'emploi, vous pouvez construire une application complète d'appels vidéo avec un minimum d'effort.
Dans ce guide, vous construirez une application de visioconférence qui prend en charge :
- Des salles multi-participants avec vidéo et audio
- Le partage d'écran
- Les contrôles du microphone et de la caméra
- Une disposition en grille des participants
- La génération sécurisée de jetons d'accès côté serveur
- Une interface responsive et moderne
Prérequis
Avant de commencer, assurez-vous d'avoir :
- Node.js 20+ installé
- Des connaissances de base en React et TypeScript
- Une familiarité avec le Next.js App Router
- Un compte gratuit sur LiveKit Cloud (ou un serveur LiveKit local via Docker)
- Un éditeur de code (VS Code recommandé)
Ce que vous allez construire
Une application de visioconférence complète comprenant :
- Page de connexion — l'utilisateur entre son nom et le nom de la salle, puis rejoint
- Salle vidéo — vue en grille de tous les participants avec leurs flux en direct
- Barre d'outils — boutons pour contrôler le microphone, la caméra, le partage d'écran et quitter
- Route API sécurisée — génération de jetons d'accès JWT côté serveur
- État de connexion — indicateurs visuels pour l'état de chaque participant
Étape 1 : Créer un projet Next.js
Créez un nouveau projet Next.js 15 :
npx create-next-app@latest livekit-video --typescript --tailwind --eslint --app --src-dir --use-npm
cd livekit-videoInstallez les packages LiveKit :
npm install livekit-client livekit-server-sdk @livekit/components-react @livekit/components-styles- livekit-client — bibliothèque client pour communiquer avec le serveur LiveKit
- livekit-server-sdk — génération de jetons d'accès côté serveur
- @livekit/components-react — composants React prêts à l'emploi pour la vidéo et l'audio
- @livekit/components-styles — styles CSS par défaut pour les composants
Étape 2 : Configurer les variables d'environnement
Créez un fichier .env.local à la racine du projet :
LIVEKIT_URL=wss://your-project.livekit.cloud
LIVEKIT_API_KEY=your_api_key
LIVEKIT_API_SECRET=your_api_secretObtenir vos clés
- Inscrivez-vous sur LiveKit Cloud
- Créez un nouveau projet
- Copiez l'URL, la clé API et le secret API depuis le tableau de bord
Vous pouvez aussi lancer un serveur LiveKit localement avec Docker :
docker run --rm -p 7880:7880 -p 7881:7881 -p 7882:7882/udp \
-e LIVEKIT_KEYS="devkey: secret" \
livekit/livekit-serverDans ce cas, utilisez :
LIVEKIT_URL=ws://localhost:7880
LIVEKIT_API_KEY=devkey
LIVEKIT_API_SECRET=secretÉtape 3 : Créer la route API pour les jetons
Créez le fichier src/app/api/token/route.ts :
import { AccessToken } from "livekit-server-sdk";
import { NextRequest, NextResponse } from "next/server";
export async function POST(request: NextRequest) {
const { roomName, participantName } = await request.json();
if (!roomName || !participantName) {
return NextResponse.json(
{ error: "roomName and participantName are required" },
{ status: 400 }
);
}
const apiKey = process.env.LIVEKIT_API_KEY;
const apiSecret = process.env.LIVEKIT_API_SECRET;
if (!apiKey || !apiSecret) {
return NextResponse.json(
{ error: "Server misconfigured" },
{ status: 500 }
);
}
const token = new AccessToken(apiKey, apiSecret, {
identity: participantName,
name: participantName,
});
token.addGrant({
room: roomName,
roomJoin: true,
canPublish: true,
canSubscribe: true,
canPublishData: true,
});
const jwt = await token.toJwt();
return NextResponse.json({ token: jwt });
}Cette route effectue les opérations suivantes :
- Réception du nom de la salle et du nom du participant depuis une requête POST
- Validation de la présence des champs requis
- Création d'un jeton d'accès JWT avec le LiveKit Server SDK
- Attribution des permissions — rejoindre la salle, publier et s'abonner
- Retour du jeton au client
Le jeton accorde toutes les permissions : publication, abonnement et envoi de données. En production, personnalisez les permissions selon le rôle de l'utilisateur.
Étape 4 : Construire la page de connexion
Créez le fichier src/app/page.tsx :
"use client";
import { useState, FormEvent } from "react";
import { useRouter } from "next/navigation";
export default function JoinPage() {
const [participantName, setParticipantName] = useState("");
const [roomName, setRoomName] = useState("");
const [isLoading, setIsLoading] = useState(false);
const router = useRouter();
async function handleJoin(e: FormEvent) {
e.preventDefault();
if (!participantName.trim() || !roomName.trim()) return;
setIsLoading(true);
const params = new URLSearchParams({
room: roomName.trim(),
name: participantName.trim(),
});
router.push(`/room?${params.toString()}`);
}
return (
<main className="min-h-screen flex items-center justify-center bg-gray-950">
<div className="w-full max-w-md p-8 bg-gray-900 rounded-2xl shadow-2xl">
<h1 className="text-3xl font-bold text-white text-center mb-2">
LiveKit Video
</h1>
<p className="text-gray-400 text-center mb-8">
Rejoignez une salle de visioconférence
</p>
<form onSubmit={handleJoin} className="space-y-5">
<div>
<label
htmlFor="name"
className="block text-sm font-medium text-gray-300 mb-1"
>
Votre nom
</label>
<input
id="name"
type="text"
value={participantName}
onChange={(e) => setParticipantName(e.target.value)}
placeholder="Entrez votre nom"
className="w-full px-4 py-3 bg-gray-800 border border-gray-700 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500"
required
/>
</div>
<div>
<label
htmlFor="room"
className="block text-sm font-medium text-gray-300 mb-1"
>
Nom de la salle
</label>
<input
id="room"
type="text"
value={roomName}
onChange={(e) => setRoomName(e.target.value)}
placeholder="Entrez le nom de la salle"
className="w-full px-4 py-3 bg-gray-800 border border-gray-700 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500"
required
/>
</div>
<button
type="submit"
disabled={isLoading}
className="w-full py-3 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-800 text-white font-semibold rounded-lg transition-colors"
>
{isLoading ? "Connexion en cours..." : "Rejoindre la salle"}
</button>
</form>
</div>
</main>
);
}Une page simple contenant un formulaire pour entrer le nom du participant et le nom de la salle. À la soumission, l'utilisateur est redirigé vers la page de la salle avec les informations dans les paramètres URL.
Étape 5 : Construire le composant de salle vidéo
Créez le fichier src/components/VideoRoom.tsx :
"use client";
import { useEffect, useState } from "react";
import {
LiveKitRoom,
VideoConference,
RoomAudioRenderer,
} from "@livekit/components-react";
import "@livekit/components-styles";
interface VideoRoomProps {
roomName: string;
participantName: string;
onLeave: () => void;
}
export function VideoRoom({
roomName,
participantName,
onLeave,
}: VideoRoomProps) {
const [token, setToken] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
async function fetchToken() {
try {
const response = await fetch("/api/token", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ roomName, participantName }),
});
if (!response.ok) {
throw new Error("Failed to get access token");
}
const data = await response.json();
setToken(data.token);
} catch (err) {
setError(
err instanceof Error ? err.message : "Erreur de connexion"
);
}
}
fetchToken();
}, [roomName, participantName]);
if (error) {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-950">
<div className="text-center">
<p className="text-red-400 text-lg mb-4">{error}</p>
<button
onClick={onLeave}
className="px-6 py-2 bg-gray-700 text-white rounded-lg hover:bg-gray-600"
>
Retour
</button>
</div>
</div>
);
}
if (!token) {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-950">
<div className="text-white text-lg">
Connexion à la salle en cours...
</div>
</div>
);
}
return (
<LiveKitRoom
token={token}
serverUrl={process.env.NEXT_PUBLIC_LIVEKIT_URL}
connect={true}
onDisconnected={onLeave}
data-lk-theme="default"
style={{ height: "100vh" }}
>
<VideoConference />
<RoomAudioRenderer />
</LiveKitRoom>
);
}Ce composant effectue les opérations suivantes :
- Récupère le jeton d'accès depuis la route API au montage
- Affiche un état de chargement pendant la récupération du jeton
- Affiche l'erreur avec un bouton retour en cas d'échec de connexion
- Se connecte à la salle via
LiveKitRoomune fois le jeton obtenu - Affiche l'interface de conférence avec le composant pré-construit
VideoConference
Le composant VideoConference de LiveKit fournit une interface complète incluant l'affichage des participants, la barre d'outils et le partage d'écran automatique.
Étape 6 : Construire la page de la salle
Créez le fichier src/app/room/page.tsx :
"use client";
import { useSearchParams, useRouter } from "next/navigation";
import { Suspense } from "react";
import { VideoRoom } from "@/components/VideoRoom";
function RoomContent() {
const searchParams = useSearchParams();
const router = useRouter();
const roomName = searchParams.get("room");
const participantName = searchParams.get("name");
if (!roomName || !participantName) {
router.push("/");
return null;
}
return (
<VideoRoom
roomName={roomName}
participantName={participantName}
onLeave={() => router.push("/")}
/>
);
}
export default function RoomPage() {
return (
<Suspense
fallback={
<div className="min-h-screen flex items-center justify-center bg-gray-950">
<div className="text-white text-lg">Chargement...</div>
</div>
}
>
<RoomContent />
</Suspense>
);
}La page de la salle extrait les informations de l'URL et les transmet au composant VideoRoom. Si les informations sont manquantes, l'utilisateur est redirigé vers la page de connexion.
Étape 7 : Ajouter la variable d'environnement publique
Ajoutez NEXT_PUBLIC_LIVEKIT_URL à votre fichier .env.local :
NEXT_PUBLIC_LIVEKIT_URL=wss://your-project.livekit.cloud
LIVEKIT_URL=wss://your-project.livekit.cloud
LIVEKIT_API_KEY=your_api_key
LIVEKIT_API_SECRET=your_api_secretLe préfixe NEXT_PUBLIC_ rend la variable disponible dans le code côté client — nécessaire pour connecter LiveKitRoom au serveur.
Étape 8 : Construire un composant vidéo personnalisé
Le composant pré-construit VideoConference est excellent pour un démarrage rapide. Mais pour une personnalisation complète de l'interface, construisez le vôtre. Créez src/components/CustomVideoRoom.tsx :
"use client";
import { useEffect, useState } from "react";
import {
LiveKitRoom,
RoomAudioRenderer,
GridLayout,
ParticipantTile,
useTracks,
useParticipants,
TrackToggle,
DisconnectButton,
} from "@livekit/components-react";
import "@livekit/components-styles";
import { Track } from "livekit-client";
interface CustomVideoRoomProps {
roomName: string;
participantName: string;
onLeave: () => void;
}
function StageArea() {
const tracks = useTracks(
[
{ source: Track.Source.Camera, withPlaceholder: true },
{ source: Track.Source.ScreenShare, withPlaceholder: false },
],
{ onlySubscribed: false }
);
return (
<GridLayout
tracks={tracks}
style={{ height: "calc(100vh - 80px)" }}
>
<ParticipantTile />
</GridLayout>
);
}
function CustomControlBar() {
const participants = useParticipants();
return (
<div className="h-20 bg-gray-900 border-t border-gray-800 flex items-center justify-between px-6">
<div className="text-gray-400 text-sm">
{participants.length} participant{participants.length !== 1 ? "s" : ""}
</div>
<div className="flex items-center gap-3">
<TrackToggle
source={Track.Source.Microphone}
className="px-4 py-2 bg-gray-700 hover:bg-gray-600 text-white rounded-full transition-colors"
/>
<TrackToggle
source={Track.Source.Camera}
className="px-4 py-2 bg-gray-700 hover:bg-gray-600 text-white rounded-full transition-colors"
/>
<TrackToggle
source={Track.Source.ScreenShare}
className="px-4 py-2 bg-gray-700 hover:bg-gray-600 text-white rounded-full transition-colors"
/>
<DisconnectButton className="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-full transition-colors">
Quitter
</DisconnectButton>
</div>
<div className="w-24" />
</div>
);
}
export function CustomVideoRoom({
roomName,
participantName,
onLeave,
}: CustomVideoRoomProps) {
const [token, setToken] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
async function fetchToken() {
try {
const response = await fetch("/api/token", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ roomName, participantName }),
});
if (!response.ok) throw new Error("Failed to get token");
const data = await response.json();
setToken(data.token);
} catch (err) {
setError(err instanceof Error ? err.message : "Erreur");
}
}
fetchToken();
}, [roomName, participantName]);
if (error) {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-950">
<div className="text-center">
<p className="text-red-400 text-lg mb-4">{error}</p>
<button
onClick={onLeave}
className="px-6 py-2 bg-gray-700 text-white rounded-lg"
>
Retour
</button>
</div>
</div>
);
}
if (!token) {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-950">
<div className="text-white">Connexion en cours...</div>
</div>
);
}
return (
<LiveKitRoom
token={token}
serverUrl={process.env.NEXT_PUBLIC_LIVEKIT_URL}
connect={true}
onDisconnected={onLeave}
data-lk-theme="default"
style={{ height: "100vh" }}
>
<div className="flex flex-col h-screen bg-gray-950">
<StageArea />
<CustomControlBar />
</div>
<RoomAudioRenderer />
</LiveKitRoom>
);
}Les différences clés par rapport au composant pré-construit :
StageArea— utiliseuseTrackspour récupérer les pistes de caméra et de partage d'écran, et les affiche en grille viaGridLayoutCustomControlBar— barre d'outils entièrement personnalisée avec compteur de participants et boutons de contrôle stylisésTrackToggle— composant pré-construit qui bascule l'état de la piste (activé/désactivé) avec mise à jour automatique de l'icône
Étape 9 : Ajouter les événements et la gestion de l'état de connexion
Pour ajouter la gestion des événements de la salle et le suivi de l'état des participants, créez src/hooks/useRoomEvents.ts :
import { useEffect } from "react";
import { useRoomContext } from "@livekit/components-react";
import { RoomEvent, ConnectionState } from "livekit-client";
export function useRoomEvents() {
const room = useRoomContext();
useEffect(() => {
function handleParticipantConnected(participant: any) {
console.log(`${participant.identity} a rejoint la salle`);
}
function handleParticipantDisconnected(participant: any) {
console.log(`${participant.identity} a quitté la salle`);
}
function handleConnectionStateChanged(state: ConnectionState) {
console.log(`État de connexion : ${state}`);
}
room.on(RoomEvent.ParticipantConnected, handleParticipantConnected);
room.on(RoomEvent.ParticipantDisconnected, handleParticipantDisconnected);
room.on(
RoomEvent.ConnectionStateChanged,
handleConnectionStateChanged
);
return () => {
room.off(RoomEvent.ParticipantConnected, handleParticipantConnected);
room.off(
RoomEvent.ParticipantDisconnected,
handleParticipantDisconnected
);
room.off(
RoomEvent.ConnectionStateChanged,
handleConnectionStateChanged
);
};
}, [room]);
}Utilisez ce hook dans n'importe quel composant enveloppé par LiveKitRoom :
function StageArea() {
useRoomEvents(); // suivi des événements
const tracks = useTracks([
{ source: Track.Source.Camera, withPlaceholder: true },
{ source: Track.Source.ScreenShare, withPlaceholder: false },
]);
return (
<GridLayout tracks={tracks}>
<ParticipantTile />
</GridLayout>
);
}Événements principaux de la salle LiveKit :
| Événement | Description |
|---|---|
ParticipantConnected | Un nouveau participant a rejoint |
ParticipantDisconnected | Un participant a quitté |
TrackSubscribed | Réception d'une piste média commencée |
TrackUnsubscribed | Réception d'une piste arrêtée |
ConnectionStateChanged | L'état de connexion a changé |
DataReceived | Un message de données a été reçu |
ActiveSpeakersChanged | Les intervenants actifs ont changé |
Étape 10 : Envoyer et recevoir des messages texte
LiveKit prend en charge l'envoi de données entre participants via le canal de données (Data Channel). Créez src/components/Chat.tsx :
"use client";
import { useState, useEffect, useRef, FormEvent } from "react";
import { useRoomContext } from "@livekit/components-react";
import { RoomEvent } from "livekit-client";
interface ChatMessage {
sender: string;
text: string;
timestamp: number;
}
export function Chat() {
const room = useRoomContext();
const [messages, setMessages] = useState<ChatMessage[]>([]);
const [input, setInput] = useState("");
const scrollRef = useRef<HTMLDivElement>(null);
useEffect(() => {
function handleDataReceived(
payload: Uint8Array,
participant: any
) {
const text = new TextDecoder().decode(payload);
const message: ChatMessage = {
sender: participant?.identity || "Inconnu",
text,
timestamp: Date.now(),
};
setMessages((prev) => [...prev, message]);
}
room.on(RoomEvent.DataReceived, handleDataReceived);
return () => {
room.off(RoomEvent.DataReceived, handleDataReceived);
};
}, [room]);
useEffect(() => {
scrollRef.current?.scrollIntoView({ behavior: "smooth" });
}, [messages]);
async function sendMessage(e: FormEvent) {
e.preventDefault();
if (!input.trim()) return;
const data = new TextEncoder().encode(input.trim());
await room.localParticipant.publishData(data, {
reliable: true,
});
setMessages((prev) => [
...prev,
{
sender: room.localParticipant.identity,
text: input.trim(),
timestamp: Date.now(),
},
]);
setInput("");
}
return (
<div className="w-80 bg-gray-900 border-l border-gray-800 flex flex-col">
<div className="p-4 border-b border-gray-800">
<h3 className="text-white font-semibold">Chat</h3>
</div>
<div className="flex-1 overflow-y-auto p-4 space-y-3">
{messages.map((msg, i) => (
<div key={i} className="text-sm">
<span className="font-medium text-blue-400">
{msg.sender} :
</span>{" "}
<span className="text-gray-300">{msg.text}</span>
</div>
))}
<div ref={scrollRef} />
</div>
<form
onSubmit={sendMessage}
className="p-4 border-t border-gray-800"
>
<div className="flex gap-2">
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Tapez un message..."
className="flex-1 px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white text-sm placeholder-gray-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
/>
<button
type="submit"
className="px-4 py-2 bg-blue-600 text-white text-sm rounded-lg hover:bg-blue-700"
>
Envoyer
</button>
</div>
</form>
</div>
);
}Ce composant ajoute un chat textuel dans la salle :
publishData— envoie des données à tous les participants via le canal de données WebRTCDataReceived— événement déclenché lors de la réception de données d'un autre participantreliable: true— utilise un canal de données fiable (comme TCP) pour garantir la livraison des messages
Pour intégrer le chat avec la salle vidéo personnalisée :
<LiveKitRoom token={token} serverUrl={url} connect={true}>
<div className="flex h-screen bg-gray-950">
<div className="flex-1 flex flex-col">
<StageArea />
<CustomControlBar />
</div>
<Chat />
</div>
<RoomAudioRenderer />
</LiveKitRoom>Étape 11 : Ajouter les paramètres de pré-connexion
Une meilleure expérience utilisateur inclut un aperçu de la caméra et du microphone avant de rejoindre. Créez src/components/PreJoin.tsx :
"use client";
import { useState, useEffect, useRef } from "react";
interface PreJoinProps {
onJoin: (settings: {
videoEnabled: boolean;
audioEnabled: boolean;
}) => void;
participantName: string;
}
export function PreJoin({ onJoin, participantName }: PreJoinProps) {
const [videoEnabled, setVideoEnabled] = useState(true);
const [audioEnabled, setAudioEnabled] = useState(true);
const [stream, setStream] = useState<MediaStream | null>(null);
const videoRef = useRef<HTMLVideoElement>(null);
useEffect(() => {
async function getMedia() {
try {
const mediaStream =
await navigator.mediaDevices.getUserMedia({
video: videoEnabled,
audio: audioEnabled,
});
setStream(mediaStream);
if (videoRef.current) {
videoRef.current.srcObject = mediaStream;
}
} catch (err) {
console.error(
"Échec d'accès aux périphériques médias :",
err
);
}
}
getMedia();
return () => {
stream?.getTracks().forEach((track) => track.stop());
};
}, [videoEnabled, audioEnabled]);
return (
<div className="min-h-screen flex items-center justify-center bg-gray-950">
<div className="w-full max-w-lg p-8 bg-gray-900 rounded-2xl">
<h2 className="text-xl font-bold text-white text-center mb-6">
Préparez-vous à rejoindre
</h2>
<div className="aspect-video bg-gray-800 rounded-xl overflow-hidden mb-6 relative">
{videoEnabled ? (
<video
ref={videoRef}
autoPlay
muted
playsInline
className="w-full h-full object-cover"
/>
) : (
<div className="w-full h-full flex items-center justify-center">
<div className="w-20 h-20 bg-gray-700 rounded-full flex items-center justify-center">
<span className="text-2xl text-white">
{participantName[0]?.toUpperCase()}
</span>
</div>
</div>
)}
</div>
<div className="flex justify-center gap-4 mb-6">
<button
onClick={() => setAudioEnabled(!audioEnabled)}
className={`px-4 py-2 rounded-full transition-colors ${
audioEnabled
? "bg-gray-700 text-white"
: "bg-red-600 text-white"
}`}
>
{audioEnabled ? "Micro activé" : "Micro désactivé"}
</button>
<button
onClick={() => setVideoEnabled(!videoEnabled)}
className={`px-4 py-2 rounded-full transition-colors ${
videoEnabled
? "bg-gray-700 text-white"
: "bg-red-600 text-white"
}`}
>
{videoEnabled ? "Caméra activée" : "Caméra désactivée"}
</button>
</div>
<button
onClick={() => {
stream?.getTracks().forEach((track) => track.stop());
onJoin({ videoEnabled, audioEnabled });
}}
className="w-full py-3 bg-blue-600 hover:bg-blue-700 text-white font-semibold rounded-lg transition-colors"
>
Rejoindre maintenant
</button>
</div>
</div>
);
}Ce composant affiche un aperçu de la caméra et permet à l'utilisateur d'activer ou de désactiver le microphone et la caméra avant de rejoindre la salle.
Étape 12 : Exécuter et tester l'application
Démarrez le serveur de développement :
npm run devOuvrez votre navigateur sur http://localhost:3000 et suivez ces étapes :
- Entrez votre nom et un nom de salle (par exemple, "test-room")
- Cliquez sur "Rejoindre la salle"
- Autorisez l'accès à la caméra et au microphone quand demandé
- Pour tester l'appel, ouvrez une deuxième fenêtre de navigateur (ou un autre navigateur) et rejoignez la même salle avec un nom différent
Vous devriez voir le flux vidéo des deux participants et entendre l'audio.
Résolution des problèmes courants
| Problème | Solution |
|---|---|
| Erreur de connexion | Vérifiez que LIVEKIT_URL et NEXT_PUBLIC_LIVEKIT_URL sont corrects |
| Pas de vidéo | Assurez-vous que la permission de la caméra est accordée dans le navigateur |
| Pas d'audio | Assurez-vous que la permission du microphone est accordée |
| Erreur CORS | Si vous utilisez un serveur local, assurez-vous qu'il fonctionne sur les bons ports |
Étape 13 : Déployer en production
Déploiement sur Vercel
- Poussez votre code vers un dépôt Git :
git init
git add .
git commit -m "feat: livekit video app"
git remote add origin https://github.com/your-username/livekit-video.git
git push -u origin main-
Connectez le dépôt sur Vercel
-
Ajoutez les variables d'environnement dans les paramètres du projet :
NEXT_PUBLIC_LIVEKIT_URL=wss://your-project.livekit.cloud
LIVEKIT_URL=wss://your-project.livekit.cloud
LIVEKIT_API_KEY=your_api_key
LIVEKIT_API_SECRET=your_api_secret
- Déployez l'application
Déploiement avec Docker
Créez un fichier Dockerfile :
FROM node:20-alpine AS base
FROM base AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
EXPOSE 3000
CMD ["node", "server.js"]Puis construisez et exécutez :
docker build -t livekit-video .
docker run -p 3000:3000 --env-file .env.local livekit-videoFonctionnalités avancées
Enregistrement des appels
LiveKit prend en charge l'enregistrement des appels via l'API Egress :
import { EgressClient, EncodedFileOutput } from "livekit-server-sdk";
const egressClient = new EgressClient(
process.env.LIVEKIT_URL!,
process.env.LIVEKIT_API_KEY!,
process.env.LIVEKIT_API_SECRET!
);
// Démarrer l'enregistrement d'une salle
const output = new EncodedFileOutput({
filepath: "recordings/room-{room_name}-{time}.mp4",
// Configurer S3 ou GCS pour le stockage
});
await egressClient.startRoomCompositeEgress(roomName, { file: output });Agent vocal IA
Vous pouvez construire un agent vocal IA qui rejoint la salle et répond par audio avec LiveKit Agents :
# agents/voice_agent.py (Python SDK)
from livekit.agents import AutoSubscribe, JobContext, WorkerOptions, cli
from livekit.agents.voice_assistant import VoiceAssistant
from livekit.plugins import openai, silero
async def entrypoint(ctx: JobContext):
await ctx.connect(auto_subscribe=AutoSubscribe.AUDIO_ONLY)
assistant = VoiceAssistant(
vad=silero.VAD.load(),
stt=openai.STT(),
llm=openai.LLM(),
tts=openai.TTS(),
)
assistant.start(ctx.room)
await assistant.say("Bonjour ! Comment puis-je vous aider ?")Cela ouvre des possibilités énormes pour construire des assistants vocaux interactifs, des bots de support client et des outils éducatifs vocaux.
Prochaines étapes
Après avoir terminé ce guide, vous pouvez :
- Ajouter l'authentification — utilisez NextAuth ou Better Auth pour protéger les salles
- Ajouter des salles d'attente — laissez l'hôte accepter les participants avant qu'ils n'entrent
- Construire un agent IA — utilisez LiveKit Agents pour ajouter un assistant vocal intelligent
- Ajouter l'enregistrement — enregistrez les appels et stockez-les dans S3
- Optimiser les performances — utilisez Simulcast pour adapter la qualité à la vitesse du réseau
- Ajouter un tableau blanc — intégrez un outil de dessin collaboratif dans la salle
Conclusion
Dans ce guide, nous avons construit une application de visioconférence complète avec LiveKit et Next.js. Nous avons appris à générer des jetons d'accès depuis le serveur, construire une interface vidéo avec des composants React prêts à l'emploi, personnaliser la barre d'outils, ajouter un chat textuel via les canaux de données et construire un écran de prévisualisation avant la connexion.
LiveKit simplifie considérablement la construction d'applications vidéo et audio en temps réel. Son architecture open source et ses bibliothèques React prêtes à l'emploi facilitent un démarrage rapide, tandis que ses API avancées (Agents, Egress, Ingress) vous donnent le pouvoir de construire n'importe quoi, des simples visioconférences aux agents vocaux IA sophistiqués.
Discutez de votre projet avec nous
Nous sommes ici pour vous aider avec vos besoins en développement Web. Planifiez un appel pour discuter de votre projet et comment nous pouvons vous aider.
Trouvons les meilleures solutions pour vos besoins.
Articles connexes

Construire une application complète avec Firebase et Next.js 15 : Auth, Firestore et temps réel
Apprenez à créer une application full-stack avec Next.js 15 et Firebase. Ce guide couvre l'authentification, Firestore, les mises à jour en temps réel, les Server Actions et le déploiement sur Vercel.

Construire un Agent IA Autonome avec Agentic RAG et Next.js
Apprenez a construire un agent IA qui decide de maniere autonome quand et comment recuperer des informations depuis des bases de donnees vectorielles. Un guide pratique complet avec Vercel AI SDK et Next.js, accompagne d'exemples executables.

Construire un moteur de recherche sémantique avec Next.js 15, OpenAI et Pinecone
Apprenez à construire un moteur de recherche sémantique prêt pour la production avec Next.js 15, OpenAI Embeddings et la base de données vectorielle Pinecone. Ce tutoriel complet couvre la configuration, l'indexation, les requêtes et le déploiement.