WebRTC Signaling Server: Complete Guide with Node.js Examples
WebRTC stelt browsers in staat om video, audio en data rechtstreeks naar elkaar te verzenden. Geen plugins, geen Flash, geen intermediaire servers die uw mediastromen aanraken. Maar hier is het probleem dat elke ontwikkelaar de eerste keer tegenkomt: twee browsers kunnen elkaar niet zomaar magisch op het internet vinden. Ze hebben een bemiddelaar nodig. Die bemiddelaar is de signaling server.
Ik heb signaling servers gebouwd voor videoraadplegingsplatforms, multiplayer browsergames en samenwerkingstools. Elke keer is de signaling laag waar de verwarring leeft -- niet omdat het moeilijk is, maar omdat WebRTC de signaling opzettelijk niet specificeert. De spec vertelt u welke informatie moet worden uitgewisseld, maar zegt niets over hoe deze uit te wisselen. Dat is zowel een geschenk als een vloek.
Deze gids behandelt alles: wat signaling eigenlijk doet onder de motorkap, het offer/answer protocol met SDP, ICE candidate uitwisseling en NAT traversal, en vervolgens twee production-ready implementaties -- één met Node.js + Socket.io en een ander met Supabase Realtime. We kijken naar echte code, niet naar pseudocode.
Inhoudsopgave
- Wat doet een signaling server eigenlijk?
- Het Offer/Answer protocol en SDP
- ICE Candidates en NAT Traversal
- Signaling transport opties vergeleken
- Implementatie 1: Node.js + Socket.io
- Implementatie 2: Supabase Realtime
- Production hardening tips
- TURN-servers: als directe verbindingen mislukken
- Veelgestelde vragen

Wat doet een signaling server eigenlijk?
Laten we de jargon weg laten. Een signaling server is een berichtenstatieven. Dat is alles. Twee peers moeten wat kilobytes tekst uitwisselen voordat ze rechtstreeks kunnen verbinden. De signaling server brengt die berichten heen en terug.
Specifiek zorgt een signaling server voor:
- Peer discovery -- Gebruiker A moet Gebruiker B vertellen "Ik wil met je verbinden." Iemand moet dat bericht routeren.
- SDP-uitwisseling -- Beide peers genereren Session Description Protocol blobs die hun mediahandelingen beschrijven (codecs, codering, enz.). Deze moeten van A naar B en terug.
- ICE candidate relay -- Elke peer ontdekt mogelijke netwerkpaden (candidates) en stuurt deze naar de andere peer via de signaling server.
- Sessielevenscyclus -- Het starten, heronderhandelen en afbreken van verbindingen.
Hier is wat de signaling server NIET doet: het raakt nooit uw audio- of videogegevens aan. Zodra de twee peers een directe verbinding tot stand brengen, is de taak van de signaling server in feite voltooid. Media stroomt peer-to-peer.
Denk eraan als twee mensen op een feestje voorstellen. U loopt erheen en zegt "Hey Sarah, dit is Mike, jullie houden allebei van klimmen." Dan loopt u weg. Sarah en Mike gaan verder. U bent de signaling server in dat scenario.
Waarom WebRTC signaling niet standaardiseert
Dit is een opzettelijke keuze, geen verspreiding. De auteurs van de WebRTC-specificatie erkenden dat de meeste applicaties al een communicatiekanaal tussen gebruikers hebben -- een chatsysteem, een matchmakingservice, een samenwerkingsplatform. Het forceren van een specifiek signaling protocol zou betekenen dat elke app twee communicatielagen moet implementeren in plaats van te profiteren van wat al bestaat.
Een datingapp heeft al berichtinfrastructuur. Een telehealth platform heeft al afsprakensystemen. Een gamingplatform heeft al lobbyservers. WebRTC laat u gebruiken wat u al hebt om die SDP- en ICE-berichten door te geven.
U kunt letterlijk postduiven gebruiken die USB-sticks dragen als de latentie niet uitmaakt. (Het maakt uit, maar het punt staat vast.)
Het Offer/Answer protocol en SDP
De kernuitwisseling van signaling volgt een patroon dat het offer/answer model heet, geleend van SIP (Session Initiation Protocol). Hier is de stroom:
- Peer A maakt een
RTCPeerConnectionen roeptcreateOffer()aan - Peer A stelt het aanbod in als zijn lokale beschrijving via
setLocalDescription() - Peer A stuurt het aanbod (een SDP blob) naar Peer B via de signaling server
- Peer B ontvangt het aanbod en stelt het in als zijn externe beschrijving via
setRemoteDescription() - Peer B roept
createAnswer()aan - Peer B stelt het antwoord in als zijn lokale beschrijving
- Peer B stuurt het antwoord via de signaling server terug
- Peer A ontvangt het antwoord en stelt het in als zijn externe beschrijving
Na deze uitwisseling kennen beide peers elkaars mediahandelingen. Maar ze kunnen nog niet verbinden -- ze hebben netwerkpadgegevens nodig, die afkomstig zijn van ICE.
Wat zit er in een SDP Blob?
SDP is een tekstformaat dat eruitziet alsof het in 1996 is ontworpen. Omdat het was. Hier is een afgekapt voorbeeld:
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
De belangrijke onderdelen: het vermeldt ondersteunde codecs (VP8, H264), coderingsvingerafdrukken, ICE-referenties en mediatypes. Uw signaling server hoeft geen van dit te parseren. Het geeft de blob gewoon door als een ondoorzichtige tekenreeks.
Het SDP Offer/Answer in code
Hier is de JavaScript aan de clientzijde voor de aanbiedende peer:
const pc = new RTCPeerConnection({
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'turn:your-turn-server.com:3478', username: 'user', credential: 'pass' }
]
});
// Add local media tracks
const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
stream.getTracks().forEach(track => pc.addTrack(track, stream));
// Create and send offer
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
// Send to signaling server
signalingServer.send({
type: 'offer',
sdp: pc.localDescription,
targetPeerId: 'peer-b-id'
});
En de antwoordende peer:
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
});
});
ICE Candidates en NAT Traversal
SDP-onderhandeling behandelt wat media moet worden verzonden. ICE behandelt hoe u de andere peer op het netwerk bereikt. Dit is waar het interessant wordt -- en waar de meeste WebRTC-verbindingen eigenlijk falen in de echte wereld.
Het NAT-probleem
De meeste apparaten bevinden zich achter een router met NAT (Network Address Translation). Uw laptop heeft mogelijk een lokaal IP van 192.168.1.42, maar de buitenwereld ziet het publieke IP van uw router. Als twee peers beide achter NAT's zitten, weet geen van beiden hoe de ander rechtstreeks te bereiken.
ICE (Interactive Connectivity Establishment) lost dit op door meerdere kandidaatpaden te verzamelen en deze allemaal uit te proberen:
| Candidaattype | Bron | Succespercentage | Latentie |
|---|---|---|---|
| Host | Lokale netwerkinterface | Werkt alleen in hetzelfde LAN | Laagste |
| Server-reflexief (srflx) | Ontdekt via STUN-server | ~80-85% van verbindingen | Laag |
| Relay | Toegewezen via TURN-server | ~99%+ (fallback) | Hoger (doorgestuurd) |
ICE Candidate Trickling
Hier is een kritieke optimalisatie: trickle ICE. In plaats van te wachten tot alle candidates zijn verzameld voordat u deze verzendt, verzendt u elke candidate naar de externe peer zodra deze is ontdekt. Dit kan seconden schelen bij de verbindingsopbouw.
// Caller side: send candidates as they arrive
pc.onicecandidate = (event) => {
if (event.candidate) {
signalingServer.send({
type: 'ice-candidate',
candidate: event.candidate,
targetPeerId: remotePeerId
});
}
};
// Receiver side: add candidates as they arrive
signalingServer.on('ice-candidate', async (data) => {
try {
await pc.addIceCandidate(new RTCIceCandidate(data.candidate));
} catch (err) {
// Candidates can arrive before remote description is set
// Buffer them and add later
console.error('Failed to add ICE candidate:', err);
}
});
Dat catch blok wijst op een echte gotcha: ICE candidates kunnen aankomen voordat u setRemoteDescription() hebt aangeroepen. U moet ze bufferen. We zullen dit correct aanpakken in de production implementaties hieronder.
ICE Candidate buffering
Dit is iets wat bijna elke tutorial overslaat, en het veroorzaakt bugs in production. Hier is het patroon:
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);
}
});
// After setting remote description:
await pc.setRemoteDescription(remoteDesc);
remoteDescriptionSet = true;
for (const candidate of pendingCandidates) {
await pc.addIceCandidate(new RTCIceCandidate(candidate));
}
pendingCandidates = [];

Signaling transport opties vergeleken
U kunt in feite elk bidirectioneel communicatiekanaal voor signaling gebruiken. Hier ziet u hoe de veelgebruikte opties zich stapelen:
| Transport | Voordelen | Nadelen | Beste voor |
|---|---|---|---|
| WebSocket (raw) | Lage latentie, full-duplex, lichtgewicht | Handmatige herverbindingslogica, geen rooms | Eenvoudige 1:1 apps |
| Socket.io | Auto-herverbinding, rooms, fallback naar polling | Grotere bibliotheekgrootte (~45KB), geen standaard | De meeste web apps |
| Supabase Realtime | Beheerde infrastructuur, ingebouwde auth, Postgres-integratie | Vendor lock-in, berichtgroottebeperkingen | Apps al op Supabase |
| Firebase Realtime DB | Beheerd, goede docs, offline ondersteuning | Vendor lock-in, prijzen op schaal | Firebase-gebaseerde apps |
| HTTP polling | Werkt overal, eenvoudig te implementeren | Hoge latentie, serverbelasting | Verouderde omgevingen |
| SIP over WebSocket | Interop met telefooniesystemen | Complex, overkill voor de meeste web apps | VoIP-integratie |
Voor de meeste projecten is Socket.io of een beheerde realtime service zoals Supabase de juiste keuze. Laten we beide bouwen.
Implementatie 1: Node.js + Socket.io
Dit is de meest voorkomende benadering en degene die ik zou aanbevelen voor teams die volledige controle over hun signaling infrastructuur willen. We bouwen een signaling server die rooms met meerdere peers ondersteunt.
Server Setup
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']
}
});
// Track rooms and their 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);
// Notify existing peers in the room
socket.to(roomId).emit('peer-joined', { peerId: socket.id });
// Send the new peer a list of existing peers
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', () => {
// Clean up rooms
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}`);
});
Integratie aan de clientzijde
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' },
// Add TURN server for 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) => {
// Attach remote stream to a video element
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}`);
// Implement reconnection logic here
}
};
// Store the connection and its state
peerConnections.set(remotePeerId, {
pc,
pendingCandidates,
isRemoteDescSet,
setRemoteDescDone() {
this.isRemoteDescSet = true;
this.pendingCandidates.forEach(c => pc.addIceCandidate(new RTCIceCandidate(c)));
this.pendingCandidates = [];
}
});
return pc;
}
// Join a room
socket.emit('join-room', 'my-room-id');
// When a new peer joins, create an offer
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);
}
});
Dit zorgt voor meerdere peers in een room, buffert ICE candidates correct en ruimt op bij verbreken. Het is het patroon dat we gebruiken bij Social Animal bij het toevoegen van realtime functies aan Next.js applicaties.
Implementatie 2: Supabase Realtime
Als u al Supabase gebruikt (of uw eigen WebSocket-server wilt vermijden), werken Supabase Realtime channels prachtig voor signaling. Deze benadering maakt gebruik van de Broadcast-functie van Supabase -- geen databaseschrijfbewerkingen nodig.
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 } }
});
// Listen for signaling messages
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') {
// Announce presence
await channel.send({
type: 'broadcast',
event: 'peer-joined',
payload: { peerId: myPeerId }
});
}
});
// Send offer
async function sendOffer(targetPeerId, sdp) {
await channel.send({
type: 'broadcast',
event: 'offer',
payload: { sdp, fromPeerId: myPeerId, targetPeerId }
});
}
// Send answer
async function sendAnswer(targetPeerId, sdp) {
await channel.send({
type: 'broadcast',
event: 'answer',
payload: { sdp, fromPeerId: myPeerId, targetPeerId }
});
}
// Send ICE candidate
async function sendIceCandidate(targetPeerId, candidate) {
await channel.send({
type: 'broadcast',
event: 'ice-candidate',
payload: { candidate, fromPeerId: myPeerId, targetPeerId }
});
}
De Supabase-benadering heeft een opvallend voordeel: u krijgt gratis presence tracking met behulp van de Presence-functie van Supabase. U kunt zien wie er in een room is zonder extra code. De afweging is dat broadcast berichten zichtbaar zijn voor alle channel-abonnees -- u filtert op targetPeerId aan de clientzijde, wat prima is voor signaling (het zijn geen gevoelige gegevens; de eigenlijke media zijn versleuteld).
Voor teams die op Supabase bouwen, elimineert dit een hele server uit uw infrastructuur. We hebben dit patroon gebruikt in headless CMS-projecten waar real-time samenwerking een vereiste was.
Production hardening tips
Signaling in development werkend krijgen is eenvoudig. Het betrouwbaar houden in production is waar het echte werk zit.
1. Berichtvolgordevolgorde en deduplicatie
WebSocket-berichten kunnen buiten de volgorde aankomen tijdens herverbindingen. Voeg volgnummers toe aan uw berichten en verwerk herordening op de client:
let messageSeq = 0;
function sendSignalingMessage(type, payload) {
socket.emit(type, { ...payload, seq: ++messageSeq, timestamp: Date.now() });
}
2. Heartbeats en herverbinding
Socket.io verwerkt herverbinding automatisch, maar u moet opnieuw deelnemen aan rooms na herverbinding:
socket.on('connect', () => {
if (currentRoom) {
socket.emit('join-room', currentRoom);
}
});
3. Snelheidsbeperkingen
ICE candidate trickling kan in snelle opeenvolging tientallen berichten genereren. Op een drukke server telt dit op. Implementeer snelheidsbeperkingen per socket:
import { RateLimiterMemory } from 'rate-limiter-flexible';
const rateLimiter = new RateLimiterMemory({
points: 50, // messages
duration: 10, // per 10 seconds
});
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. Authenticatie
Voer nooit een signaling server zonder authenticatie in production uit. Met Socket.io, gebruikt u 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. Horizontale schaling
Een enkel Node.js-proces kan duizenden signaling-verbindingen verwerken (signaling is lichtgewicht -- alleen tekstberichten). Wanneer u verder moet schalen dan één server, gebruikt u @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));
Dit zorgt ervoor dat socket.to(peerId).emit() werkt over meerdere serverinstanties.
TURN-servers: als directe verbindingen mislukken
STUN werkt voor ongeveer 80-85% van verbindingen. Voor de overige 15-20% (symmetrische NAT's, restrictieve firewalls, bedrijfsnetwerken) hebt u een TURN-server nodig om mediaverkeer door te sturen.
In 2026 zijn uw opties voor TURN:
| Provider | Prijzen (ongeveer) | Opmerkingen |
|---|---|---|
| Cloudflare Calls TURN | Gratis tier beschikbaar, op gebruiksbasis | Beste waarde voor de meeste projecten |
| Twilio TURN | $0,40/GB | Betrouwbaar, goed gedocumenteerd |
| Metered.ca | Gratis 500GB/ma, daarna $0,40/GB | Populair voor indie projecten |
| Self-hosted coturn | Alleen serverkosten | Volledige controle, operationele belasting |
| Xirsys | Vanaf $24,99/ma | Globale PoPs |
Zorg altijd voor minstens één TURN-server in uw ICE-configuratie. Gebruikers vertellen "het werkt voor de meeste mensen" is onaanvaardbaar wanneer een VP uw product op hotel-WiFi probeert te demonstreren.
Als u iets bouwt dat betrouwbaar moet werken in bedrijfsnetwerken, neem contact met ons op -- we hebben teams geholpen WebRTC-oplossingen te ontwerpen die de lastige randgevallen aanpakken.
Veelgestelde vragen
Wat is een WebRTC signaling server? Een signaling server is een berichtenstatieven die twee WebRTC peers helpt de informatie uit te wisselen die ze nodig hebben om een directe verbinding tot stand te brengen. Het brengt SDP-aanbiedingen en -antwoorden (die mediahandelingen beschrijven) en ICE candidates (die netwerkpaden beschrijven) over. De signaling server raakt nooit audio- of videogegevens aan -- zodra de peers verbinden, stroomt media direct tussen hen door.
Waarom bevat WebRTC geen standaard signaling protocol? De WebRTC-specificatie laat signaling opzettelijk ongespecificeerd omdat de meeste applicaties al een communicatiekanaal tussen gebruikers hebben. Een chat app, een matchmakingservice of een samenwerkingstool kan zijn bestaande infrastructuur voor signaling hergebruiken. Deze flexibiliteit betekent dat u WebSockets, HTTP, SIP, XMPP of elk ander transport kunt gebruiken dat tekstberichten kan dragen.
Wat is het verschil tussen SDP-aanbod en SDP-antwoord? Een SDP-aanbod wordt gemaakt door de peer die de verbinding initieert. Het beschrijft de mediahandelingen van die peer -- ondersteunde codecs, coderingsmethoden en mediatypes. Het SDP-antwoord wordt gemaakt door de ontvangende peer en bevestigt welke capaciteiten deze ondersteunt uit het aanbod. Samen onderhandelen zij over de gedeelde parameters die beide peers voor de mediasessie zullen gebruiken.
Wat zijn ICE candidates en waarom moeten ze worden uitgewisseld? ICE candidates zijn mogelijke netwerkpaden waarlangs een peer kan worden bereikt. Elke peer ontdekt candidates door zijn lokale netwerkinterfaces te controleren, STUN-servers naar publiek gerichte adressen te bevragen en relaisadressen op TURN-servers toe te wijzen. Deze candidates moeten naar de andere peer worden gestuurd via de signaling server, zodat beide zijden meerdere paden kunnen proberen en er één kunnen vinden die door NAT's en firewalls werkt.
Kan ik Supabase Realtime als WebRTC signaling server gebruiken? Ja. De Broadcast-functie van Supabase Realtime werkt goed voor signaling omdat het laaglatentie berichtlevering tussen verbonden clients biedt zonder databaseschrijfbewerkingen. U maakt een channel voor elke room, broadcast aanbiedingen/antwoorden/ICE-berichten en filtert aan de clientzijde op target peer ID. Het is een solide keuze voor projecten die al Supabase gebruiken en die een afzonderlijke signaling server willen vermijden.
Hoeveel gelijktijdige verbindingen kan een signaling server verwerken? Een enkel Node.js + Socket.io proces kan meestal 10.000 tot 50.000 gelijktijdige signaling verbindingen verwerken, afhankelijk van de berichtfrequentie en serverresources. Signaling verkeer is lichtgewicht -- slechts kleine JSON-berichten. Voor grotere schaal gebruikt u Redis adapter met Socket.io om verbindingen over meerdere serverinstanties te verdelen. De bottleneck in WebRTC is zelden de signaling server; het is meestal de TURN-relaisbandbreedte.
Heb ik een TURN-server in production nodig? Ja. Hoewel STUN ongeveer 80-85% van verbindingen verwerkt, bevinden ongeveer 15-20% van de gebruikers zich achter symmetrische NAT's of restrictieve firewalls die directe verbindingen voorkomen. Zonder een TURN-server kunnen die gebruikers eenvoudigweg niet verbinden. In bedrijfsomgevingen kan het mislukkingtarief zonder TURN nog hoger zijn. Services zoals Cloudflare Calls, Twilio en Metered.ca bieden TURN met gratis tiers.
Wat gebeurt er als de signaling server tijdens een actief gesprek uitvalt? Zodra een WebRTC-verbinding is tot stand gebracht, hangt deze niet meer af van de signaling server. Een bestaand gesprek blijft werken als de signaling server crashes. Echter, nieuwe verbindingen kunnen niet tot stand worden gebracht, en als de bestaande verbinding opnieuw moet worden onderhandeld (bijvoorbeeld het toevoegen van schermsharing), zal dat mislukken. U moet uw signaling server voor hoge beschikbaarheid ontwerpen met behulp van clustering of beheerde services.