Serveur de Signalisation WebRTC : Guide Complet avec Exemples Node.js
WebRTC permet aux navigateurs d'envoyer de la vidéo, de l'audio et des données directement les uns aux autres. Pas de plugins, pas de Flash, pas de serveurs intermédiaires touchant vos flux médias. Mais voici le truc qui confond chaque développeur la première fois : deux navigateurs ne peuvent pas simplement se trouver magiquement sur Internet. Ils ont besoin d'un entremetteur. Cet entremetteur est le serveur de signalisation.
J'ai construit des serveurs de signalisation pour des plateformes de consultation vidéo, des jeux de navigateur multijoueurs et des outils d'édition collaborative. Chaque fois, la couche de signalisation est là où vit la confusion -- non pas parce que c'est difficile, mais parce que WebRTC la laisse intentionnellement non spécifiée. La spécification vous dit quelles informations doivent être échangées mais ne dit rien sur comment les échanger. C'est à la fois un cadeau et une malédiction.
Ce guide couvre tout : ce que la signalisation fait réellement sous le capot, le protocole offer/answer avec SDP, l'échange de candidats ICE et la traversée NAT, puis deux implémentations prêtes pour la production -- l'une avec Node.js + Socket.io et une autre utilisant Supabase Realtime. Nous verrons du vrai code, pas du pseudocode.
Table des matières
- Qu'est-ce qu'un serveur de signalisation ?
- Le protocole Offer/Answer et SDP
- Candidats ICE et traversée NAT
- Options de transport de signalisation comparées
- Implémentation 1 : Node.js + Socket.io
- Implémentation 2 : Supabase Realtime
- Conseils de renforcement pour la production
- Serveurs TURN : Quand les connexions directes échouent
- FAQ

Qu'est-ce qu'un serveur de signalisation ?
Dépouillons-nous du jargon. Un serveur de signalisation est un relais de messages. C'est tout. Deux pairs doivent échanger quelques kilobits de texte avant de pouvoir se connecter directement. Le serveur de signalisation transporte ces messages d'avant en arrière.
Spécifiquement, un serveur de signalisation gère :
- Découverte des pairs -- L'utilisateur A doit dire à l'utilisateur B « Je veux me connecter avec toi. » Quelqu'un doit acheminer ce message.
- Échange SDP -- Les deux pairs générent des blobs de protocole de description de session qui décrivent leurs capacités médias (codecs, chiffrement, etc.). Ceux-ci doivent aller de A à B et vice-versa.
- Relais de candidats ICE -- Chaque pair découvre les chemins réseau potentiels (candidats) et les envoie à l'autre pair via le serveur de signalisation.
- Cycle de vie de la session -- Démarrage, renégociation et arrêt des connexions.
Voici ce que le serveur de signalisation ne fait PAS : il ne touche jamais vos données audio ou vidéo. Une fois que les deux pairs établissent une connexion directe, le travail du serveur de signalisation est essentiellement terminé. Les médias circulent de pair à pair.
Pensez-y comme à présenter deux personnes à une fête. Vous vous approchez et dites « Hé Sarah, voici Mike, vous aimez tous les deux l'escalade. » Ensuite, vous vous en allez. Sarah et Mike prennent les choses en main à partir de là. Vous êtes le serveur de signalisation dans ce scénario.
Pourquoi WebRTC ne standardise pas la signalisation
C'est un choix délibéré, pas une inadvertance. Les auteurs de la spécification WebRTC ont reconnu que la plupart des applications ont déjà un canal de communication entre les utilisateurs -- un système de chat, un service d'appariement, une plateforme de collaboration. Forcer un protocole de signalisation spécifique signifierait que chaque application doit implémenter deux couches de communication au lieu de s'appuyer sur ce qui existe déjà.
Une application de rencontre a déjà une infrastructure de messagerie. Une plateforme de télésanté a déjà des systèmes de rendez-vous. Une plateforme de jeu a déjà des serveurs de hall d'attente. WebRTC vous permet d'utiliser ce que vous avez déjà pour transmettre ces messages SDP et ICE autour.
Vous pourriez littéralement utiliser des pigeons voyageurs transportant des clés USB si la latence n'avait pas d'importance. (Cela a de l'importance, mais le point reste valable.)
Le protocole Offer/Answer et SDP
L'échange de signalisation principal suit un modèle appelé le modèle offer/answer, emprunté à SIP (Session Initiation Protocol). Voici le flux :
- Le pair A crée une
RTCPeerConnectionet appellecreateOffer() - Le pair A définit l'offre comme sa description locale via
setLocalDescription() - Le pair A envoie l'offre (un blob SDP) au pair B via le serveur de signalisation
- Le pair B reçoit l'offre et la définit comme sa description distante via
setRemoteDescription() - Le pair B appelle
createAnswer() - Le pair B définit la réponse comme sa description locale
- Le pair B renvoie la réponse via le serveur de signalisation
- Le pair A reçoit la réponse et la définit comme sa description distante
Après cet échange, les deux pairs connaissent les capacités médias de l'autre. Mais ils ne peuvent toujours pas se connecter -- ils ont besoin d'informations sur les chemins réseau, qui proviennent d'ICE.
Qu'y a-t-il dans un blob SDP ?
SDP est un format texte qui semble avoir été conçu en 1996. Parce qu'il l'a été. Voici un exemple réduit :
v=0
o=- 4625943584070133116 2 IN IP4 127.0.0.1
s=-
t=0 0
m=video 9 UDP/TLS/RTP/SAVPF 96 97 98
c=IN IP4 0.0.0.0
a=rtcp:9 IN IP4 0.0.0.0
a=ice-ufrag:aR7B
a=ice-pwd:hN3Y2mDoS68sGLprHvBGNbKp
a=fingerprint:sha-256 D1:3C:A0:...
a=setup:actpass
a=mid:0
a=rtpmap:96 VP8/90000
a=rtpmap:97 H264/90000
Les bits importants : il liste les codecs supportés (VP8, H264), les empreintes digitales de chiffrement, les identifiants ICE et les types de médias. Votre serveur de signalisation n'a besoin de parser aucun de cela. Il passe simplement le blob comme une chaîne opaque.
L'Offer/Answer SDP en code
Voici le JavaScript côté client pour le pair qui fait l'offre :
const pc = new RTCPeerConnection({
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'turn:your-turn-server.com:3478', username: 'user', credential: 'pass' }
]
});
// Ajouter des pistes médias locales
const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
stream.getTracks().forEach(track => pc.addTrack(track, stream));
// Créer et envoyer l'offre
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
// Envoyer au serveur de signalisation
signalingServer.send({
type: 'offer',
sdp: pc.localDescription,
targetPeerId: 'peer-b-id'
});
Et le pair qui répond :
signalingServer.on('offer', async (data) => {
await pc.setRemoteDescription(new RTCSessionDescription(data.sdp));
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
signalingServer.send({
type: 'answer',
sdp: pc.localDescription,
targetPeerId: data.fromPeerId
});
});
Candidats ICE et traversée NAT
La négociation SDP gère quels médias envoyer. ICE gère comment atteindre l'autre pair sur le réseau. C'est là que les choses deviennent intéressantes -- et c'est là que la plupart des connexions WebRTC échouent réellement dans le monde réel.
Le problème NAT
La plupart des appareils se trouvent derrière un routeur exécutant NAT (Network Address Translation). Votre ordinateur portable peut avoir une adresse IP locale de 192.168.1.42, mais le monde extérieur voit l'adresse IP publique de votre routeur. Quand deux pairs se trouvent tous les deux derrière des NAT, aucun ne sait comment atteindre l'autre directement.
ICE (Interactive Connectivity Establishment) résout cela en rassemblant plusieurs chemins candidats et en les essayant tous :
| Type de candidat | Source | Taux de succès | Latence |
|---|---|---|---|
| Hôte | Interface réseau locale | Fonctionne seulement sur le même LAN | Plus basse |
| Server-reflexive (srflx) | Découvert via serveur STUN | ~80-85% des connexions | Basse |
| Relais | Alloué via serveur TURN | ~99%+ (secours) | Plus élevée (relayée) |
Trickling de candidats ICE
Voici une optimisation critique : trickle ICE. Au lieu d'attendre que tous les candidats soient rassemblés avant de les envoyer, vous envoyez chaque candidat au pair distant dès qu'il est découvert. Cela peut économiser des secondes dans l'établissement de la connexion.
// Côté appelant : envoyer les candidats au fur et à mesure qu'ils arrivent
pc.onicecandidate = (event) => {
if (event.candidate) {
signalingServer.send({
type: 'ice-candidate',
candidate: event.candidate,
targetPeerId: remotePeerId
});
}
};
// Côté récepteur : ajouter les candidats au fur et à mesure qu'ils arrivent
signalingServer.on('ice-candidate', async (data) => {
try {
await pc.addIceCandidate(new RTCIceCandidate(data.candidate));
} catch (err) {
// Les candidats peuvent arriver avant la description distante
// Les tamponner et ajouter plus tard
console.error('Failed to add ICE candidate:', err);
}
});
Ce bloc catch donne un indice sur une vraie difficulté : les candidats ICE peuvent arriver avant d'avoir appelé setRemoteDescription(). Vous devez les tamponner. Nous gérerons cela correctement dans les implémentations de production ci-dessous.
Tampon de candidats ICE
C'est quelque chose que presque tous les tutoriels ignorent, et cela cause des bugs en production. Voici le modèle :
let pendingCandidates = [];
let remoteDescriptionSet = false;
signalingServer.on('ice-candidate', async (data) => {
if (remoteDescriptionSet) {
await pc.addIceCandidate(new RTCIceCandidate(data.candidate));
} else {
pendingCandidates.push(data.candidate);
}
});
// Après avoir défini la description distante :
await pc.setRemoteDescription(remoteDesc);
remoteDescriptionSet = true;
for (const candidate of pendingCandidates) {
await pc.addIceCandidate(new RTCIceCandidate(candidate));
}
pendingCandidates = [];

Options de transport de signalisation comparées
Vous pouvez utiliser à peu près n'importe quel canal de communication bidirectionnel pour la signalisation. Voici comment les options courantes se comparent :
| Transport | Avantages | Inconvénients | Meilleur pour |
|---|---|---|---|
| WebSocket (raw) | Faible latence, full-duplex, léger | Logique de reconnexion manuelle, pas de salons | Applications 1:1 simples |
| Socket.io | Reconnexion automatique, salons, recours à l'interrogation | Taille de la bibliothèque plus grande (~45KB), pas une norme | Plupart des applications web |
| Supabase Realtime | Infrastructure gérée, authentification intégrée, intégration Postgres | Verrouillage du fournisseur, limites de taille de message | Applications déjà sur Supabase |
| Firebase Realtime DB | Gérée, bonne documentation, support hors ligne | Verrouillage du fournisseur, tarification à grande échelle | Applications basées sur Firebase |
| Interrogation HTTP | Fonctionne partout, simple à implémenter | Latence élevée, charge serveur | Environnements hérités |
| SIP sur WebSocket | Interopérabilité avec les systèmes téléphoniques | Complexe, excessif pour la plupart des applications web | Intégration VoIP |
Pour la plupart des projets, Socket.io ou un service realtime géré comme Supabase est le bon choix. Construisons les deux.
Implémentation 1 : Node.js + Socket.io
C'est l'approche la plus courante et celle que je recommanderais pour les équipes qui veulent le contrôle total de leur infrastructure de signalisation. Nous construisons un serveur de signalisation qui prend en charge les salons avec plusieurs pairs.
Configuration du serveur
import { createServer } from 'http';
import { Server } from 'socket.io';
const httpServer = createServer();
const io = new Server(httpServer, {
cors: {
origin: process.env.CLIENT_ORIGIN || 'http://localhost:3000',
methods: ['GET', 'POST']
}
});
// Suivre les salons et leurs participants
const rooms = new Map();
io.on('connection', (socket) => {
console.log(`Peer connected: ${socket.id}`);
socket.on('join-room', (roomId) => {
socket.join(roomId);
if (!rooms.has(roomId)) {
rooms.set(roomId, new Set());
}
rooms.get(roomId).add(socket.id);
// Notifier les pairs existants dans le salon
socket.to(roomId).emit('peer-joined', { peerId: socket.id });
// Envoyer au nouveau pair une liste des pairs existants
const existingPeers = [...rooms.get(roomId)].filter(id => id !== socket.id);
socket.emit('existing-peers', { peers: existingPeers });
});
socket.on('offer', ({ targetPeerId, sdp }) => {
io.to(targetPeerId).emit('offer', {
sdp,
fromPeerId: socket.id
});
});
socket.on('answer', ({ targetPeerId, sdp }) => {
io.to(targetPeerId).emit('answer', {
sdp,
fromPeerId: socket.id
});
});
socket.on('ice-candidate', ({ targetPeerId, candidate }) => {
io.to(targetPeerId).emit('ice-candidate', {
candidate,
fromPeerId: socket.id
});
});
socket.on('disconnect', () => {
// Nettoyer les salons
for (const [roomId, peers] of rooms.entries()) {
if (peers.has(socket.id)) {
peers.delete(socket.id);
socket.to(roomId).emit('peer-left', { peerId: socket.id });
if (peers.size === 0) {
rooms.delete(roomId);
}
}
}
});
});
const PORT = process.env.PORT || 3001;
httpServer.listen(PORT, () => {
console.log(`Signaling server running on port ${PORT}`);
});
Intégration côté client
import { io } from 'socket.io-client';
const socket = io('wss://your-signaling-server.com');
const peerConnections = new Map();
const ICE_SERVERS = {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'stun:stun1.l.google.com:19302' },
// Ajouter un serveur TURN pour la production
]
};
function createPeerConnection(remotePeerId) {
const pc = new RTCPeerConnection(ICE_SERVERS);
let pendingCandidates = [];
let isRemoteDescSet = false;
pc.onicecandidate = (event) => {
if (event.candidate) {
socket.emit('ice-candidate', {
targetPeerId: remotePeerId,
candidate: event.candidate
});
}
};
pc.ontrack = (event) => {
// Attacher le flux distant à un élément vidéo
const remoteVideo = document.getElementById(`video-${remotePeerId}`);
if (remoteVideo) {
remoteVideo.srcObject = event.streams[0];
}
};
pc.onconnectionstatechange = () => {
if (pc.connectionState === 'failed' || pc.connectionState === 'disconnected') {
console.warn(`Connection to ${remotePeerId} ${pc.connectionState}`);
// Implémenter la logique de reconnexion ici
}
};
// Stocker la connexion et son état
peerConnections.set(remotePeerId, {
pc,
pendingCandidates,
isRemoteDescSet,
setRemoteDescDone() {
this.isRemoteDescSet = true;
this.pendingCandidates.forEach(c => pc.addIceCandidate(new RTCIceCandidate(c)));
this.pendingCandidates = [];
}
});
return pc;
}
// Rejoindre un salon
socket.emit('join-room', 'my-room-id');
// Quand un nouveau pair rejoint, créer une offre
socket.on('peer-joined', async ({ peerId }) => {
const pc = createPeerConnection(peerId);
localStream.getTracks().forEach(track => pc.addTrack(track, localStream));
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
socket.emit('offer', { targetPeerId: peerId, sdp: pc.localDescription });
});
socket.on('offer', async ({ sdp, fromPeerId }) => {
const pc = createPeerConnection(fromPeerId);
localStream.getTracks().forEach(track => pc.addTrack(track, localStream));
await pc.setRemoteDescription(new RTCSessionDescription(sdp));
peerConnections.get(fromPeerId).setRemoteDescDone();
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
socket.emit('answer', { targetPeerId: fromPeerId, sdp: pc.localDescription });
});
socket.on('answer', async ({ sdp, fromPeerId }) => {
const conn = peerConnections.get(fromPeerId);
await conn.pc.setRemoteDescription(new RTCSessionDescription(sdp));
conn.setRemoteDescDone();
});
socket.on('ice-candidate', ({ candidate, fromPeerId }) => {
const conn = peerConnections.get(fromPeerId);
if (conn.isRemoteDescSet) {
conn.pc.addIceCandidate(new RTCIceCandidate(candidate));
} else {
conn.pendingCandidates.push(candidate);
}
});
Cela gère plusieurs pairs dans un salon, stocke les candidats ICE correctement, et nettoie à la déconnexion. C'est le modèle que nous utilisons chez Social Animal lors de la création de fonctionnalités en temps réel dans les applications Next.js.
Implémentation 2 : Supabase Realtime
Si vous utilisez déjà Supabase (ou voulez éviter d'exécuter votre propre serveur WebSocket), les canaux Supabase Realtime fonctionnent magnifiquement pour la signalisation. Cette approche utilise la fonction Broadcast de Supabase -- aucune écriture de base de données requise.
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
);
const roomId = 'video-call-room-123';
const myPeerId = crypto.randomUUID();
const channel = supabase.channel(roomId, {
config: { broadcast: { self: false } }
});
// Écouter les messages de signalisation
channel
.on('broadcast', { event: 'offer' }, ({ payload }) => {
if (payload.targetPeerId === myPeerId) {
handleOffer(payload);
}
})
.on('broadcast', { event: 'answer' }, ({ payload }) => {
if (payload.targetPeerId === myPeerId) {
handleAnswer(payload);
}
})
.on('broadcast', { event: 'ice-candidate' }, ({ payload }) => {
if (payload.targetPeerId === myPeerId) {
handleIceCandidate(payload);
}
})
.on('broadcast', { event: 'peer-joined' }, ({ payload }) => {
if (payload.peerId !== myPeerId) {
initiateConnection(payload.peerId);
}
})
.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
// Annoncer la présence
await channel.send({
type: 'broadcast',
event: 'peer-joined',
payload: { peerId: myPeerId }
});
}
});
// Envoyer une offre
async function sendOffer(targetPeerId, sdp) {
await channel.send({
type: 'broadcast',
event: 'offer',
payload: { sdp, fromPeerId: myPeerId, targetPeerId }
});
}
// Envoyer une réponse
async function sendAnswer(targetPeerId, sdp) {
await channel.send({
type: 'broadcast',
event: 'answer',
payload: { sdp, fromPeerId: myPeerId, targetPeerId }
});
}
// Envoyer un candidat ICE
async function sendIceCandidate(targetPeerId, candidate) {
await channel.send({
type: 'broadcast',
event: 'ice-candidate',
payload: { candidate, fromPeerId: myPeerId, targetPeerId }
});
}
L'approche Supabase a un avantage notable : vous obtenez le suivi de la présence gratuitement en utilisant la fonction Presence de Supabase. Vous pouvez voir qui se trouve dans un salon sans code supplémentaire. Le compromis est que les messages diffusés sont visibles pour tous les abonnés du canal -- vous filtrez par targetPeerId côté client, ce qui va bien pour la signalisation (ce n'est pas des données sensibles ; les médias réels sont chiffrés).
Pour les équipes construisant sur Supabase, cela élimine un serveur entier de votre infrastructure. Nous avons utilisé ce modèle dans des projets CMS headless où la collaboration en temps réel était une exigence.
Conseils de renforcement pour la production
Faire fonctionner la signalisation en développement est simple. La garder fiable en production est où le vrai travail se fait.
1. Ordre des messages et déduplication
Les messages WebSocket peuvent arriver dans un ordre différent pendant les reconnexions. Ajoutez des numéros de séquence à vos messages et gérez la réorganisation côté client :
let messageSeq = 0;
function sendSignalingMessage(type, payload) {
socket.emit(type, { ...payload, seq: ++messageSeq, timestamp: Date.now() });
}
2. Battements de cœur et reconnexion
Socket.io gère la reconnexion automatiquement, mais vous devez rejoindre les salons après la reconnexion :
socket.on('connect', () => {
if (currentRoom) {
socket.emit('join-room', currentRoom);
}
});
3. Limitation de débit
Le trickling de candidats ICE peut générer des dizaines de messages en succession rapide. Sur un serveur occupé, cela s'accumule. Implémentez une limitation de débit par socket :
import { RateLimiterMemory } from 'rate-limiter-flexible';
const rateLimiter = new RateLimiterMemory({
points: 50, // messages
duration: 10, // par 10 secondes
});
io.on('connection', (socket) => {
socket.use(async ([event, ...args], next) => {
try {
await rateLimiter.consume(socket.id);
next();
} catch {
next(new Error('Rate limit exceeded'));
}
});
});
4. Authentification
Ne lancez jamais un serveur de signalisation sans authentification en production. Avec Socket.io, utilisez un middleware :
io.use(async (socket, next) => {
const token = socket.handshake.auth.token;
try {
const user = await verifyJWT(token);
socket.data.userId = user.id;
next();
} catch {
next(new Error('Authentication failed'));
}
});
5. Mise à l'échelle horizontale
Un seul processus Node.js peut gérer des milliers de connexions de signalisation (la signalisation est légère -- juste des messages texte). Quand vous devez vous mettre à l'échelle au-delà d'un serveur, utilisez @socket.io/redis-adapter :
import { createAdapter } from '@socket.io/redis-adapter';
import { createClient } from 'redis';
const pubClient = createClient({ url: process.env.REDIS_URL });
const subClient = pubClient.duplicate();
await Promise.all([pubClient.connect(), subClient.connect()]);
io.adapter(createAdapter(pubClient, subClient));
Cela garantit que socket.to(peerId).emit() fonctionne sur plusieurs instances de serveur.
Serveurs TURN : Quand les connexions directes échouent
STUN fonctionne pour environ 80-85% des connexions. Pour les 15-20% restants (NAT symétriques, pare-feu restrictifs, réseaux d'entreprise), vous avez besoin d'un serveur TURN pour relayer le trafic médias.
En 2026, vos options pour TURN incluent :
| Fournisseur | Tarification (approx.) | Notes |
|---|---|---|
| Cloudflare Calls TURN | Niveau gratuit disponible, basé sur l'utilisation | Meilleur rapport qualité-prix pour la plupart des projets |
| Twilio TURN | 0,40 $/Go | Fiable, bien documenté |
| Metered.ca | 500 Go gratuits/mo, puis 0,40 $/Go | Populaire pour les projets indépendants |
| coturn auto-hébergé | Coûts du serveur uniquement | Contrôle total, charge opérationnelle |
| Xirsys | À partir de 24,99 $/mo | PoPs globaux |
Incluez toujours au moins un serveur TURN dans votre configuration ICE. Dire aux utilisateurs « cela fonctionne pour la plupart des gens » n'est pas acceptable quand un VP essaie de présenter votre produit sur un WiFi hôtelier.
Si vous construisez quelque chose qui doit fonctionner de manière fiable sur les réseaux d'entreprise, contactez-nous -- nous avons aidé les équipes à architecturer des solutions WebRTC qui gèrent les cas limites difficiles.
FAQ
Qu'est-ce qu'un serveur de signalisation WebRTC ?
Un serveur de signalisation est un relais de messages qui aide deux pairs WebRTC à échanger les informations dont ils ont besoin pour établir une connexion directe. Il transporte les offres et réponses SDP (qui décrivent les capacités médias) et les candidats ICE (qui décrivent les chemins réseau). Le serveur de signalisation ne touche jamais aux données audio ou vidéo -- une fois que les pairs se connectent, les médias circulent directement entre eux.
Pourquoi WebRTC n'inclut-il pas un protocole de signalisation standard ?
La spécification WebRTC laisse intentionnellement la signalisation non spécifiée parce que la plupart des applications ont déjà un canal de communication entre les utilisateurs. Une application de chat, un service d'appariement ou un outil de collaboration peuvent réutiliser leur infrastructure existante pour la signalisation. Cette flexibilité signifie que vous pouvez utiliser WebSockets, HTTP, SIP, XMPP ou tout autre transport capable de transporter des messages texte.
Quelle est la différence entre une offre SDP et une réponse SDP ?
Une offre SDP est créée par le pair qui initie la connexion. Elle décrit les capacités médias de ce pair -- codecs supportés, méthodes de chiffrement et types de médias. La réponse SDP est créée par le pair qui reçoit et confirme les capacités qu'il supporte de l'offre. Ensemble, ils négocient les paramètres partagés que les deux pairs utiliseront pour la session médias.
Qu'est-ce que les candidats ICE et pourquoi doivent-ils être échangés ?
Les candidats ICE sont les chemins réseau potentiels par lesquels un pair peut être atteint. Chaque pair découvre les candidats en vérifiant ses interfaces réseau locales, en interrogeant les serveurs STUN pour les adresses visibles de l'extérieur, et en allouant les adresses de relais sur les serveurs TURN. Ces candidats doivent être envoyés à l'autre pair via le serveur de signalisation afin que les deux côtés puissent essayer plusieurs chemins et en trouver un qui fonctionne à travers les NAT et pare-feu.
Puis-je utiliser Supabase Realtime comme serveur de signalisation WebRTC ?
Oui. La fonction Broadcast de Supabase Realtime fonctionne bien pour la signalisation car elle fournit une livraison de messages à faible latence entre les clients connectés sans nécessiter aucune écriture de base de données. Vous créez un canal pour chaque salon, diffusez des messages offer/answer/ICE, et filtrez par ID de pair cible côté client. C'est un excellent choix pour les projets utilisant déjà Supabase qui veulent éviter d'exécuter un serveur de signalisation séparé.
Combien de connexions simultanées un serveur de signalisation peut-il gérer ?
Un processus Node.js + Socket.io peut généralement gérer 10 000 à 50 000 connexions de signalisation simultanées, selon la fréquence des messages et les ressources du serveur. Le trafic de signalisation est léger -- juste de petits messages JSON. Pour une plus grande échelle, utilisez l'adaptateur Redis avec Socket.io pour distribuer les connexions sur plusieurs instances de serveur. Le goulot d'étranglement dans WebRTC est rarement le serveur de signalisation ; c'est généralement la bande passante de relais TURN.
Ai-je besoin d'un serveur TURN en production ?
Oui. Alors que STUN gère environ 80-85% des connexions, environ 15-20% des utilisateurs se trouvent derrière des NAT symétriques ou des pare-feu restrictifs qui empêchent les connexions directes. Sans serveur TURN, ces utilisateurs ne peuvent simplement pas se connecter. Dans les environnements d'entreprise, le taux d'échec sans TURN peut être encore plus élevé. Des services comme Cloudflare Calls, Twilio et Metered.ca offrent TURN avec des niveaux gratuits.
Que se passe-t-il si le serveur de signalisation tombe en panne pendant un appel actif ?
Une fois qu'une connexion WebRTC est établie, elle ne dépend plus du serveur de signalisation. Un appel existant continuera de fonctionner si le serveur de signalisation tombe en panne. Cependant, les nouvelles connexions ne peuvent pas être établies, et si la connexion existante doit être renégociée (par exemple, l'ajout de partage d'écran), cela échouera. Vous devriez concevoir votre serveur de signalisation pour une haute disponibilité en utilisant un clustering ou des services gérés.