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

AI Bot
Par AI Bot ·

Chargement du lecteur de synthèse vocale...

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-video

Installez 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_secret

Obtenir vos clés

  1. Inscrivez-vous sur LiveKit Cloud
  2. Créez un nouveau projet
  3. 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-server

Dans 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 :

  1. Réception du nom de la salle et du nom du participant depuis une requête POST
  2. Validation de la présence des champs requis
  3. Création d'un jeton d'accès JWT avec le LiveKit Server SDK
  4. Attribution des permissions — rejoindre la salle, publier et s'abonner
  5. 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 :

  1. Récupère le jeton d'accès depuis la route API au montage
  2. Affiche un état de chargement pendant la récupération du jeton
  3. Affiche l'erreur avec un bouton retour en cas d'échec de connexion
  4. Se connecte à la salle via LiveKitRoom une fois le jeton obtenu
  5. 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_secret

Le 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 — utilise useTracks pour récupérer les pistes de caméra et de partage d'écran, et les affiche en grille via GridLayout
  • CustomControlBar — barre d'outils entièrement personnalisée avec compteur de participants et boutons de contrôle stylisés
  • TrackToggle — 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énementDescription
ParticipantConnectedUn nouveau participant a rejoint
ParticipantDisconnectedUn participant a quitté
TrackSubscribedRéception d'une piste média commencée
TrackUnsubscribedRéception d'une piste arrêtée
ConnectionStateChangedL'état de connexion a changé
DataReceivedUn message de données a été reçu
ActiveSpeakersChangedLes 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 WebRTC
  • DataReceived — événement déclenché lors de la réception de données d'un autre participant
  • reliable: 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 dev

Ouvrez votre navigateur sur http://localhost:3000 et suivez ces étapes :

  1. Entrez votre nom et un nom de salle (par exemple, "test-room")
  2. Cliquez sur "Rejoindre la salle"
  3. Autorisez l'accès à la caméra et au microphone quand demandé
  4. 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èmeSolution
Erreur de connexionVérifiez que LIVEKIT_URL et NEXT_PUBLIC_LIVEKIT_URL sont corrects
Pas de vidéoAssurez-vous que la permission de la caméra est accordée dans le navigateur
Pas d'audioAssurez-vous que la permission du microphone est accordée
Erreur CORSSi vous utilisez un serveur local, assurez-vous qu'il fonctionne sur les bons ports

Étape 13 : Déployer en production

Déploiement sur Vercel

  1. 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
  1. Connectez le dépôt sur Vercel

  2. 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
  1. 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-video

Fonctionnalité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.


Vous voulez lire plus de tutoriels? Découvrez notre dernier tutoriel sur Créer un Slackbot personnalisé avec NVIDIA NIM et LangChain.

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 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.

30 min read·