WebRTC Signaling Server: Vollständiger Leitfaden mit Node.js Beispielen
WebRTC ermöglicht es Browsern, Video, Audio und Daten direkt miteinander zu senden. Keine Plugins, kein Flash, keine Intermediär-Server, die deine Medienströme anfassen. Aber hier ist die Sache, die jeden Entwickler beim ersten Mal verwirrt: zwei Browser können sich nicht einfach magisch im Internet finden. Sie brauchen einen Vermittler. Dieser Vermittler ist der Signaling-Server.
Ich habe Signaling-Server für Videokonsultationsplattformen, Multiplayer-Browser-Spiele und kollaborative Bearbeitungstools gebaut. Jedes einzelne Mal ist die Signaling-Ebene der Ort, wo die Verwirrung herrscht – nicht weil es schwer ist, sondern weil WebRTC es absichtlich unspezifiziert lässt. Die Spezifikation sagt dir, welche Informationen ausgetauscht werden müssen, aber nicht, wie sie auszutauschen sind. Das ist sowohl ein Geschenk als auch ein Fluch.
Dieser Leitfaden behandelt alles: was Signaling wirklich unter der Haube tut, das Offer/Answer-Protokoll mit SDP, ICE-Kandidatenaustausch und NAT-Traversal, und dann zwei produktionsreife Implementierungen – eine mit Node.js + Socket.io und eine andere mit Supabase Realtime. Wir schauen uns echten Code an, nicht Pseudocode.
Inhaltsverzeichnis
- Was macht ein Signaling-Server wirklich?
- Das Offer/Answer-Protokoll und SDP
- ICE-Kandidaten und NAT-Traversal
- Vergleich der Signaling-Transport-Optionen
- Implementierung 1: Node.js + Socket.io
- Implementierung 2: Supabase Realtime
- Tipps zur Härtung für den Produktivbetrieb
- TURN-Server: Wenn direkte Verbindungen fehlschlagen
- FAQ

Was macht ein Signaling-Server wirklich?
Lassen Sie uns den Jargon beiseite schaffen. Ein Signaling-Server ist ein Nachrichtenrelais. Das ist alles. Zwei Peers müssen ein paar Kilobytes Text austauschen, bevor sie sich direkt verbinden können. Der Signaling-Server trägt diese Nachrichten hin und her.
Konkret verwaltet ein Signaling-Server:
- Peer-Erkennung – Benutzer A muss Benutzer B sagen „Ich möchte mich mit dir verbinden." Jemand muss diese Nachricht weiterleitend.
- SDP-Austausch – Beide Peers generieren Session Description Protocol-Blobs, die ihre Medienfähigkeiten beschreiben (Codecs, Verschlüsselung usw.). Diese müssen von A zu B und zurück gelangen.
- ICE-Kandidaten-Relais – Jeder Peer entdeckt potenzielle Netzwerkpfade (Kandidaten) und sendet sie über den Signaling-Server an den anderen Peer.
- Session-Lebenszyklus – Starten, Neuaushandeln und Beenden von Verbindungen.
Das macht der Signaling-Server NICHT: Er berührt niemals deine Audio- oder Videodaten. Sobald die beiden Peers eine direkte Verbindung herstellen, ist die Aufgabe des Signaling-Servers im Wesentlichen erledigt. Medien fließen Peer-zu-Peer.
Stellen Sie sich vor, Sie führen zwei Personen auf einer Party zusammen. Sie gehen hin und sagen „Hey Sarah, das ist Mike, ihr mögt beide Klettern." Dann gehen Sie weg. Sarah und Mike machen von da an weiter. Sie sind der Signaling-Server in diesem Szenario.
Warum WebRTC Signaling nicht standardisiert
Das ist eine bewusste Designentscheidung, kein Versehen. Die Autoren der WebRTC-Spezifikation erkannten, dass die meisten Anwendungen bereits einen Kommunikationskanal zwischen Benutzern haben – ein Chat-System, einen Matchmaking-Service, eine Kollaborationsplattform. Ein bestimmtes Signaling-Protokoll zu erzwingen würde bedeuten, dass jede App zwei Kommunikationsebenen implementieren muss, anstatt sich auf das zu stützen, was bereits vorhanden ist.
Eine Dating-App hat bereits Messaging-Infrastruktur. Eine Telemedizin-Plattform hat bereits Terminsysteme. Eine Gaming-Plattform hat bereits Lobby-Server. WebRTC lässt dich alles verwenden, was du bereits hast, um diese SDP- und ICE-Nachrichten herumzuleiten.
Du könntest buchstäblich Brieftauben verwenden, die USB-Sticks tragen, wenn die Latenz keine Rolle spielte. (Sie spielt eine Rolle, aber der Punkt bleibt bestehen.)
Das Offer/Answer-Protokoll und SDP
Der zentrale Signaling-Austausch folgt einem Muster namens Offer/Answer-Modell, das von SIP (Session Initiation Protocol) übernommen wurde. Hier ist der Ablauf:
- Peer A erstellt eine
RTCPeerConnectionund ruftcreateOffer()auf - Peer A setzt das Angebot als seine lokale Beschreibung über
setLocalDescription() - Peer A sendet das Angebot (ein SDP-Blob) über den Signaling-Server an Peer B
- Peer B erhält das Angebot und setzt es als seine Remote-Beschreibung über
setRemoteDescription() - Peer B ruft
createAnswer()auf - Peer B setzt die Antwort als seine lokale Beschreibung
- Peer B sendet die Antwort über den Signaling-Server zurück
- Peer A erhält die Antwort und setzt sie als seine Remote-Beschreibung
Nach diesem Austausch kennen beide Peers die Medienfähigkeiten des anderen. Aber sie können sich immer noch nicht verbinden – sie brauchen Netzwerkpfadinformationen, die von ICE kommen.
Was steckt in einem SDP-Blob?
SDP ist ein Textformat, das so aussieht, als würde es 1996 entworfen. Weil es war. Hier ist ein gekürztes Beispiel:
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
Die wichtigen Teile: Es listet unterstützte Codecs auf (VP8, H264), Verschlüsselungs-Fingerabdrücke, ICE-Anmeldedaten und Medientypen. Dein Signaling-Server muss nichts von alledem parsen. Er leitet einfach den Blob als undurchsichtige Zeichenkette weiter.
Der SDP Offer/Answer im Code
Hier ist das JavaScript für die Client-Seite für den Peer, der das Angebot macht:
const pc = new RTCPeerConnection({
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'turn:your-turn-server.com:3478', username: 'user', credential: 'pass' }
]
});
// Lokale Medienspuren hinzufügen
const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
stream.getTracks().forEach(track => pc.addTrack(track, stream));
// Angebot erstellen und senden
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
// An Signaling-Server senden
signalingServer.send({
type: 'offer',
sdp: pc.localDescription,
targetPeerId: 'peer-b-id'
});
Und der antwortende 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-Kandidaten und NAT-Traversal
Die SDP-Verhandlung verwaltet, was Medien zu senden sind. ICE verwaltet, wie man den anderen Peer im Netzwerk erreicht. Hier wird es interessant – und hier schlagen die meisten WebRTC-Verbindungen in der realen Welt wirklich fehl.
Das NAT-Problem
Die meisten Geräte sitzen hinter einem Router mit NAT (Network Address Translation). Dein Laptop könnte eine lokale IP von 192.168.1.42 haben, aber die Außenwelt sieht die öffentliche IP deines Routers. Wenn zwei Peers beide hinter NATs sitzen, weiß keiner, wie man den anderen direkt erreicht.
ICE (Interactive Connectivity Establishment) löst dies, indem es mehrere Kandidatenpfade sammelt und alle versucht:
| Kandidaten-Typ | Quelle | Erfolgsquote | Latenz |
|---|---|---|---|
| Host | Lokale Netzwerkschnittstelle | Funktioniert nur im gleichen LAN | Niedrigste |
| Server-reflexiv (srflx) | Entdeckt über STUN-Server | ~80-85% der Verbindungen | Niedrig |
| Relay | Zugeordnet über TURN-Server | ~99%+ (Fallback) | Höher (weitergeleitet) |
ICE-Kandidaten-Trickling
Hier ist eine kritische Optimierung: Trickle ICE. Anstatt zu warten, bis alle Kandidaten gesammelt sind, bevor du sie sendest, sendest du jeden Kandidaten an den Remote-Peer, sobald er entdeckt wird. Dies kann Sekunden von der Verbindungserstellung sparen.
// Anrufer-Seite: Kandidaten senden, sobald sie eintreffen
pc.onicecandidate = (event) => {
if (event.candidate) {
signalingServer.send({
type: 'ice-candidate',
candidate: event.candidate,
targetPeerId: remotePeerId
});
}
};
// Empfänger-Seite: Kandidaten hinzufügen, sobald sie eintreffen
signalingServer.on('ice-candidate', async (data) => {
try {
await pc.addIceCandidate(new RTCIceCandidate(data.candidate));
} catch (err) {
// Kandidaten können eintreffen, bevor die Remote-Beschreibung gesetzt ist
// Sie in einen Puffer speichern und später hinzufügen
console.error('Fehler beim Hinzufügen des ICE-Kandidaten:', err);
}
});
Dieser catch-Block deutet auf eine echte Problem hin: ICE-Kandidaten können eintreffen, bevor du setRemoteDescription() aufgerufen hast. Du musst sie puffern. Wir werden dies in den Produktionsimplementierungen unten richtig handhaben.
ICE-Kandidaten-Pufferung
Das ist etwas, das fast alle Tutorials überspringen, und es verursacht Fehler in der Produktion. Hier ist das Muster:
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);
}
});
// Nach dem Setzen der Remote-Beschreibung:
await pc.setRemoteDescription(remoteDesc);
remoteDescriptionSet = true;
for (const candidate of pendingCandidates) {
await pc.addIceCandidate(new RTCIceCandidate(candidate));
}
pendingCandidates = [];

Vergleich der Signaling-Transport-Optionen
Du kannst praktisch jeden bidirektionalen Kommunikationskanal für Signaling verwenden. Hier ist, wie sich die gängigen Optionen vergleichen:
| Transport | Vorteile | Nachteile | Beste Verwendung |
|---|---|---|---|
| WebSocket (roh) | Niedrige Latenz, Vollduplex, leichtgewichtig | Manuelle Wiederverbindungslogik, keine Räume | Einfache 1:1-Apps |
| Socket.io | Automatische Wiederverbindung, Räume, Fallback auf Polling | Größere Bibliotheksgröße (~45KB), kein Standard | Die meisten Web-Apps |
| Supabase Realtime | Verwaltete Infrastruktur, eingebaute Auth, Postgres-Integration | Anbieter-Lock-in, Nachrichtengröße-Limits | Apps bereits auf Supabase |
| Firebase Realtime DB | Verwaltet, gute Dokumentation, Offline-Unterstützung | Anbieter-Lock-in, Preisgestaltung im Maßstab | Firebase-basierte Apps |
| HTTP-Polling | Funktioniert überall, einfach zu implementieren | Hohe Latenz, Serverbelastung | Legacy-Umgebungen |
| SIP über WebSocket | Interop mit Telefonie-Systemen | Komplex, Overkill für die meisten Web-Apps | VoIP-Integration |
Für die meisten Projekte ist Socket.io oder ein verwalteter Echtzeit-Service wie Supabase der richtige Anruf. Lassen Sie uns beide bauen.
Implementierung 1: Node.js + Socket.io
Das ist der häufigste Ansatz und derjenige, den ich Teams empfehlen würde, die volle Kontrolle über ihre Signaling-Infrastruktur wünschen. Wir bauen einen Signaling-Server, der Räume mit mehreren Peers unterstützt.
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']
}
});
// Räume und ihre Teilnehmer nachverfolgen
const rooms = new Map();
io.on('connection', (socket) => {
console.log(`Peer verbunden: ${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);
// Benachrichtige bestehende Peers im Raum
socket.to(roomId).emit('peer-joined', { peerId: socket.id });
// Sende dem neuen Peer eine Liste von bestehenden 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', () => {
// Räume aufräumen
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 läuft auf Port ${PORT}`);
});
Client-seitige Integration
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' },
// TURN-Server für Produktivbetrieb hinzufügen
]
};
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) => {
// Remote-Stream an ein Video-Element anhängen
const remoteVideo = document.getElementById(`video-${remotePeerId}`);
if (remoteVideo) {
remoteVideo.srcObject = event.streams[0];
}
};
pc.onconnectionstatechange = () => {
if (pc.connectionState === 'failed' || pc.connectionState === 'disconnected') {
console.warn(`Verbindung zu ${remotePeerId} ${pc.connectionState}`);
// Wiederverbindungslogik hier implementieren
}
};
// Speichere die Verbindung und seinen Status
peerConnections.set(remotePeerId, {
pc,
pendingCandidates,
isRemoteDescSet,
setRemoteDescDone() {
this.isRemoteDescSet = true;
this.pendingCandidates.forEach(c => pc.addIceCandidate(new RTCIceCandidate(c)));
this.pendingCandidates = [];
}
});
return pc;
}
// Raum beitreten
socket.emit('join-room', 'my-room-id');
// Wenn ein neuer Peer beitritt, erstelle ein Angebot
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);
}
});
Dies verwaltet mehrere Peers in einem Raum, puffert ICE-Kandidaten richtig und räumt bei der Trennung auf. Es ist das Muster, das wir bei der Entwicklung von Echtzeitfunktionen in Next.js-Anwendungen verwenden.
Implementierung 2: Supabase Realtime
Wenn du bereits Supabase verwendest (oder deinen eigenen WebSocket-Server vermeiden möchtest), funktionieren Supabase Realtime-Kanäle wunderbar für Signaling. Dieser Ansatz verwendet die Broadcast-Funktion von Supabase – keine Datenbankschreibvorgänge erforderlich.
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 } }
});
// Signaling-Nachrichten abhören
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') {
// Präsenz ankündigen
await channel.send({
type: 'broadcast',
event: 'peer-joined',
payload: { peerId: myPeerId }
});
}
});
// Angebot senden
async function sendOffer(targetPeerId, sdp) {
await channel.send({
type: 'broadcast',
event: 'offer',
payload: { sdp, fromPeerId: myPeerId, targetPeerId }
});
}
// Antwort senden
async function sendAnswer(targetPeerId, sdp) {
await channel.send({
type: 'broadcast',
event: 'answer',
payload: { sdp, fromPeerId: myPeerId, targetPeerId }
});
}
// ICE-Kandidat senden
async function sendIceCandidate(targetPeerId, candidate) {
await channel.send({
type: 'broadcast',
event: 'ice-candidate',
payload: { candidate, fromPeerId: myPeerId, targetPeerId }
});
}
Der Supabase-Ansatz hat einen bemerkenswerten Vorteil: Du bekommst Präsenzverfolgung kostenlos, indem du die Presence-Funktion von Supabase verwendest. Du kannst sehen, wer sich in einem Raum befindet, ohne zusätzlichen Code. Der Trade-off ist, dass Broadcast-Nachrichten für alle Kanal-Abonnenten sichtbar sind – du filterst nach targetPeerId auf der Client-Seite, was für Signaling in Ordnung ist (es sind keine sensiblen Daten; die tatsächlichen Medien sind verschlüsselt).
Für Teams, die auf Supabase aufbauen, eliminiert dies einen ganzen Server aus deiner Infrastruktur. Wir haben dieses Muster in Projekten mit Headless CMS verwendet, bei denen Echtzeit-Zusammenarbeit eine Anforderung war.
Tipps zur Härtung für den Produktivbetrieb
Signaling in der Entwicklung zum Funktionieren zu bringen, ist unkompliziert. Es zuverlässig in der Produktion zu halten, ist der eigentliche Aufwand.
1. Nachrichtenreihenfolge und Deduplizierung
WebSocket-Nachrichten können während Wiederverbindungen außerhalb der Reihenfolge eintreffen. Füge Sequenznummern zu deinen Nachrichten hinzu und verhandle die Umordnung auf dem Client:
let messageSeq = 0;
function sendSignalingMessage(type, payload) {
socket.emit(type, { ...payload, seq: ++messageSeq, timestamp: Date.now() });
}
2. Heartbeats und Wiederverbindung
Socket.io verwaltet die Wiederverbindung automatisch, aber du musst Räume nach der Wiederverbindung erneut beitreten:
socket.on('connect', () => {
if (currentRoom) {
socket.emit('join-room', currentRoom);
}
});
3. Rate-Limiting
ICE-Kandidaten-Trickling kann Dutzende von Nachrichten in schneller Folge generieren. Auf einem belebten Server addiert sich das. Implementiere Rate-Limiting pro Socket:
import { RateLimiterMemory } from 'rate-limiter-flexible';
const rateLimiter = new RateLimiterMemory({
points: 50, // Nachrichten
duration: 10, // pro 10 Sekunden
});
io.on('connection', (socket) => {
socket.use(async ([event, ...args], next) => {
try {
await rateLimiter.consume(socket.id);
next();
} catch {
next(new Error('Rate-Limit überschritten'));
}
});
});
4. Authentifizierung
Führe niemals einen Signaling-Server ohne Authentifizierung in der Produktion aus. Mit Socket.io verwende 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('Authentifizierung fehlgeschlagen'));
}
});
5. Horizontale Skalierung
Ein einzelner Node.js-Prozess kann Tausende von Signaling-Verbindungen verwalten (Signaling ist leichtgewichtig – nur Textnachrichten). Wenn du über einen Server hinaus skalieren musst, verwende @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));
Dies stellt sicher, dass socket.to(peerId).emit() über mehrere Server-Instanzen funktioniert.
TURN-Server: Wenn direkte Verbindungen fehlschlagen
STUN funktioniert bei etwa 80-85% der Verbindungen. Für die verbleibenden 15-20% (symmetrische NATs, restriktive Firewalls, Unternehmensnetzwerke) benötigst du einen TURN-Server, um Mediendatenverkehr weiterzuleiten.
Im Jahr 2026 sind deine Optionen für TURN:
| Anbieter | Preisgestaltung (ca.) | Anmerkungen |
|---|---|---|
| Cloudflare Calls TURN | Kostenlose Stufe verfügbar, nutzungsabhängig | Bester Wert für die meisten Projekte |
| Twilio TURN | 0,40 $/GB | Zuverlässig, gut dokumentiert |
| Metered.ca | Kostenlos 500GB/Mo, dann 0,40 $/GB | Beliebt für Indie-Projekte |
| Selbst gehostetes coturn | Nur Serverkosten | Volle Kontrolle, operative Last |
| Xirsys | Ab 24,99 $/Mo | Globale PoPs |
Beziehe immer mindestens einen TURN-Server in deine ICE-Konfiguration ein. Benutzern zu sagen „es funktioniert bei den meisten Leuten" ist nicht akzeptabel, wenn ein VP dein Produkt auf einer Hotel-WiFi demonstrieren versucht.
Wenn du etwas baust, das zuverlässig in Unternehmensnetzwerken funktionieren muss, kontaktiere uns – wir haben Teams geholfen, WebRTC-Lösungen zu entwickeln, die die kniffligen Grenzfälle handhaben.
FAQ
Was ist ein WebRTC-Signaling-Server?
Ein Signaling-Server ist ein Nachrichtenrelais, das zwei WebRTC-Peers dabei hilft, die Informationen auszutauschen, die sie benötigen, um eine direkte Verbindung herzustellen. Es trägt SDP-Angebote und -Antworten (die Medienfähigkeiten beschreiben) und ICE-Kandidaten (die Netzwerkpfade beschreiben). Der Signaling-Server berührt niemals Audio- oder Videodaten – sobald sich die Peers verbinden, fließen Medien direkt zwischen ihnen.
Warum schließt WebRTC ein Standardsignaling-Protokoll nicht ein?
Die WebRTC-Spezifikation lässt Signaling absichtlich unspezifiziert, weil die meisten Anwendungen bereits einen Kommunikationskanal zwischen Benutzern haben. Eine Chat-App, ein Matchmaking-Service oder ein Kollaborationstool kann seine bestehende Infrastruktur für Signaling wiederverwenden. Diese Flexibilität bedeutet, dass du WebSockets, HTTP, SIP, XMPP oder einen anderen Transport verwenden kannst, der Textnachrichten tragen kann.
Was ist der Unterschied zwischen SDP-Angebot und SDP-Antwort?
Ein SDP-Angebot wird vom Peer erstellt, der die Verbindung initiiert. Es beschreibt die Medienfähigkeiten dieses Peers – unterstützte Codecs, Verschlüsselungsmethoden und Medientypen. Die SDP-Antwort wird vom empfangenden Peer erstellt und bestätigt, welche Fähigkeiten aus dem Angebot es unterstützt. Zusammen handeln sie die gemeinsamen Parameter aus, die beide Peers für die Mediensitzung verwenden.
Was sind ICE-Kandidaten und warum müssen sie ausgetauscht werden?
ICE-Kandidaten sind potenzielle Netzwerkpfade, über die ein Peer erreichbar ist. Jeder Peer entdeckt Kandidaten, indem er seine lokalen Netzwerkschnittstellen prüft, STUN-Server nach öffentlich zugänglichen Adressen abfragt und Relay-Adressen auf TURN-Servern zuordnet. Diese Kandidaten müssen über den Signaling-Server an den anderen Peer gesendet werden, damit beide Seiten mehrere Pfade versuchen und einen finden können, der durch NATs und Firewalls funktioniert.
Kann ich Supabase Realtime als WebRTC-Signaling-Server verwenden?
Ja. Die Broadcast-Funktion von Supabase Realtime funktioniert gut für Signaling, da sie Nachrichten mit niedriger Latenz zwischen verbundenen Clients liefert, ohne Datenbankschreibvorgänge zu erfordern. Du erstellst einen Kanal für jeden Raum, broadcastest Offer/Answer/ICE-Nachrichten und filterst nach Ziel-Peer-ID auf der Client-Seite. Es ist eine solide Wahl für Projekte, die bereits Supabase verwenden und einen separaten Signaling-Server vermeiden möchten.
Wie viele gleichzeitige Verbindungen kann ein Signaling-Server verwalten?
Ein einzelner Node.js + Socket.io-Prozess kann typischerweise 10.000–50.000 gleichzeitige Signaling-Verbindungen verwalten, abhängig von der Nachrichtenhäufigkeit und den Serverressourcen. Signaling-Datenverkehr ist leichtgewichtig – nur kleine JSON-Nachrichten. Für größere Skalierung verwende Redis-Adapter mit Socket.io, um Verbindungen über mehrere Server-Instanzen zu verteilen. Der Engpass in WebRTC ist selten der Signaling-Server; es ist normalerweise die TURN-Relay-Bandbreite.
Benötige ich einen TURN-Server in der Produktion?
Ja. Während STUN etwa 80-85% der Verbindungen verwaltet, sitzen etwa 15-20% der Benutzer hinter symmetrischen NATs oder restriktiven Firewalls, die direkte Verbindungen verhindern. Ohne einen TURN-Server können sich diese Benutzer einfach nicht verbinden. In Unternehmensumgebungen kann die Ausfallrate ohne TURN noch höher sein. Services wie Cloudflare Calls, Twilio und Metered.ca bieten TURN mit kostenlosen Stufen.
Was passiert, wenn der Signaling-Server während eines aktiven Anrufs ausfällt?
Sobald eine WebRTC-Verbindung hergestellt ist, hängt sie nicht mehr vom Signaling-Server ab. Ein bestehender Anruf funktioniert weiterhin, wenn der Signaling-Server abstürzt. Neue Verbindungen können jedoch nicht hergestellt werden, und wenn die bestehende Verbindung neu ausgehandelt werden muss (z.B. zum Hinzufügen von Bildschirmfreigabe), schlägt dies fehl. Du solltest deinen Signaling-Server für hohe Verfügbarkeit unter Verwendung von Clustering oder verwalteten Services entwerfen.