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

WebRTC Signaling Server: Complete Guide with Node.js Examples

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:

  1. Peer discovery -- Gebruiker A moet Gebruiker B vertellen "Ik wil met je verbinden." Iemand moet dat bericht routeren.
  2. SDP-uitwisseling -- Beide peers genereren Session Description Protocol blobs die hun mediahandelingen beschrijven (codecs, codering, enz.). Deze moeten van A naar B en terug.
  3. ICE candidate relay -- Elke peer ontdekt mogelijke netwerkpaden (candidates) en stuurt deze naar de andere peer via de signaling server.
  4. 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:

  1. Peer A maakt een RTCPeerConnection en roept createOffer() aan
  2. Peer A stelt het aanbod in als zijn lokale beschrijving via setLocalDescription()
  3. Peer A stuurt het aanbod (een SDP blob) naar Peer B via de signaling server
  4. Peer B ontvangt het aanbod en stelt het in als zijn externe beschrijving via setRemoteDescription()
  5. Peer B roept createAnswer() aan
  6. Peer B stelt het antwoord in als zijn lokale beschrijving
  7. Peer B stuurt het antwoord via de signaling server terug
  8. 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 = [];

WebRTC Signaling Server: Complete Guide with Node.js Examples - architecture

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.