Real-Time & P2P

WebRTC mit Claude Code:
Real-Time Video & P2P 2026

RTCPeerConnection, ICE/STUN/TURN, MediaStream, Data Channels und SFU-Skalierung — Claude Code als WebRTC-Experte für echte Peer-to-Peer-Kommunikation im Browser.

📅 6. Mai 2026 📖 12 min Lesezeit 📌 Real-Time & P2P
RTCPeerConnection Signaling WebSocket ICE/STUN TURN MediaStream getUserMedia DataChannel File Transfer mediasoup SFU

Inhaltsverzeichnis

  1. WebRTC Grundlagen: RTCPeerConnection & SDP
  2. Signaling Server: WebSocket + Socket.io Rooms
  3. ICE, STUN & TURN: NAT Traversal mit coturn
  4. MediaStream & Video: getUserMedia & Screen Sharing
  5. Data Channels: Binärdaten & File Transfer
  6. Production: simple-peer, mediasoup SFU, Reconnection

1. WebRTC Grundlagen: RTCPeerConnection & SDP

WebRTC (Web Real-Time Communication) ermöglicht direkte Browser-zu-Browser-Kommunikation ohne Zwischen­server für die eigentlichen Mediadaten. Die W3C-Spezifikation ist seit 2021 ein offizieller Standard und wird von allen modernen Browsern nativ unterstützt — kein Plugin, kein Flash, kein nativer App-Wrapper nötig.

Die zentrale API ist RTCPeerConnection. Sie kapselt die gesamte Verbindungslogik: ICE-Candidate-Sammlung, DTLS-Handshake, SRTP-Verschlüsselung und Mediamultiplexing über BUNDLE. Claude Code generiert die vollständige Boilerplate inklusive korrektem Error-Handling und State-Machine-Logik.

SDP Offer/Answer Modell

WebRTC nutzt das SDP-Protokoll (Session Description Protocol) für den Capability-Austausch. Der Call-Flow folgt immer demselben Muster: Caller erstellt ein Offer-SDP, Callee antwortet mit einem Answer-SDP. Danach werden ICE Candidates ausgetauscht bis eine Verbindung steht.

// Peer A: Offer erstellen und senden
const configuration = {
  iceServers: [
    { urls: 'stun:stun.l.google.com:19302' },
    { urls: 'stun:stun1.l.google.com:19302' }
  ]
};

const pc = new RTCPeerConnection(configuration);

// ICE Candidates lokal sammeln und zum Signaling-Server senden
pc.onicecandidate = (event) => {
  if (event.candidate) {
    socket.emit('ice-candidate', {
      roomId,
      candidate: event.candidate
    });
  }
};

// Verbindungsstatus überwachen
pc.onconnectionstatechange = () => {
  console.log('Connection state:', pc.connectionState);
  if (pc.connectionState === 'failed') {
    pc.restartIce(); // Automatischer Reconnect-Versuch
  }
};

// Offer erstellen
const offer = await pc.createOffer({
  offerToReceiveAudio: true,
  offerToReceiveVideo: true
});

// Local Description setzen (startet ICE-Gathering)
await pc.setLocalDescription(offer);

// Offer über Signaling-Server zu Peer B senden
socket.emit('offer', { roomId, sdp: pc.localDescription });

RTCPeerConnection Lifecycle:

new — Initial, keine Verbindung

connecting — ICE-Candidates werden geprüft, DTLS-Handshake läuft

connected — Mindestens ein ICE-Candidate-Paar funktioniert, DTLS fertig

disconnected — Heartbeats fehlen, aber Reconnect möglich (kurze Unterbrechung)

failed — Alle ICE-Kandidaten gescheitert → restartIce() nötig

closedpc.close() explizit aufgerufen

Answer: Peer B antwortet

// Peer B: Offer empfangen und Answer senden
socket.on('offer', async ({ sdp }) => {
  const pc = new RTCPeerConnection(configuration);

  pc.onicecandidate = (event) => {
    if (event.candidate) {
      socket.emit('ice-candidate', { roomId, candidate: event.candidate });
    }
  };

  // Remote Track eingehend — Video im UI anzeigen
  pc.ontrack = (event) => {
    const [remoteStream] = event.streams;
    remoteVideoElement.srcObject = remoteStream;
  };

  // Remote Description aus dem Offer setzen
  await pc.setRemoteDescription(new RTCSessionDescription(sdp));

  // Answer erstellen und setzen
  const answer = await pc.createAnswer();
  await pc.setLocalDescription(answer);

  // Answer zurück zu Peer A
  socket.emit('answer', { roomId, sdp: pc.localDescription });
});
Claude Code Workflow: Beschreib deinen Use Case auf Deutsch — Claude generiert die vollständige RTCPeerConnection-Logik für beide Seiten, inklusive State-Machine, ICE-Restart-Logic und TypeScript-Interfaces für alle SDP-Objekte. Keine manuelle DTLS- oder SRTP-Konfiguration nötig.

2. Signaling Server: WebSocket + Socket.io Rooms

WebRTC selbst definiert keinen Signaling-Mechanismus — das ist bewusst flexibel gehalten. In der Praxis werden WebSockets (über Socket.io oder native WebSocket-API) als Signaling-Kanal eingesetzt. Der Signaling-Server vermittelt SDP-Offers, SDP-Answers und ICE-Candidates zwischen den Peers, übermittelt aber niemals die eigentlichen Mediadaten.

Node.js Signaling Server mit Socket.io

// server.js — Minimaler Signaling Server
import { createServer } from 'http';
import { Server } from 'socket.io';

const httpServer = createServer();
const io = new Server(httpServer, {
  cors: { origin: 'https://deine-app.com', methods: ['GET', 'POST'] }
});

// Rooms: Schlüssel = roomId, Wert = Set von Socket-IDs
const rooms = new Map();

io.on('connection', (socket) => {
  console.log(`Client verbunden: ${socket.id}`);

  // Room betreten
  socket.on('join-room', (roomId) => {
    const room = rooms.get(roomId) ?? new Set();

    if (room.size >= 2) {
      socket.emit('room-full');
      return;
    }

    room.add(socket.id);
    rooms.set(roomId, room);
    socket.join(roomId);

    // Alle anderen im Room benachrichtigen
    socket.to(roomId).emit('peer-joined', { peerId: socket.id });
    socket.emit('joined-room', {
      roomId,
      peers: [...room].filter(id => id !== socket.id)
    });
  });

  // SDP Offer weiterleiten
  socket.on('offer', ({ roomId, sdp, targetId }) => {
    socket.to(targetId ?? roomId).emit('offer', {
      sdp,
      fromId: socket.id
    });
  });

  // SDP Answer weiterleiten
  socket.on('answer', ({ roomId, sdp, targetId }) => {
    socket.to(targetId ?? roomId).emit('answer', {
      sdp,
      fromId: socket.id
    });
  });

  // ICE Candidates weiterleiten
  socket.on('ice-candidate', ({ roomId, candidate, targetId }) => {
    socket.to(targetId ?? roomId).emit('ice-candidate', {
      candidate,
      fromId: socket.id
    });
  });

  // Disconnect: Room aufräumen
  socket.on('disconnect', () => {
    rooms.forEach((members, roomId) => {
      if (members.has(socket.id)) {
        members.delete(socket.id);
        if (members.size === 0) rooms.delete(roomId);
        io.to(roomId).emit('peer-left', { peerId: socket.id });
      }
    });
  });
});

httpServer.listen(3001, () => console.log('Signaling Server: Port 3001'));

Multi-Peer Rooms (Mesh-Topologie):

Bei mehr als 2 Peers erstellt jeder Teilnehmer eine eigene RTCPeerConnection zu jedem anderen. Bei N Peers = N*(N-1)/2 Verbindungen. Skaliert bis ca. 4-6 Teilnehmer gut, danach SFU empfohlen (siehe Abschnitt 6).

Socket.io Rooms abstrahieren das Routing — jeder Socket in einem Room bekommt Events via socket.to(roomId).emit(), der Sender selbst ist ausgenommen.

Client-seitige Socket.io Integration

// client.js — Socket.io Client-Integration
import { io } from 'socket.io-client';

const socket = io('https://signal.deine-app.com', {
  transports: ['websocket'], // Long-Polling als Fallback vermeiden
  reconnection: true,
  reconnectionDelay: 1000,
  reconnectionAttempts: 5
});

socket.on('connect', () => {
  socket.emit('join-room', roomId);
});

socket.on('peer-joined', async ({ peerId }) => {
  // Wir sind der erste im Room — jetzt Offer erstellen
  await createOffer(peerId);
});

socket.on('offer', async ({ sdp, fromId }) => {
  await handleOffer(sdp, fromId);
});

socket.on('answer', async ({ sdp, fromId }) => {
  const pc = peerConnections.get(fromId);
  await pc.setRemoteDescription(new RTCSessionDescription(sdp));
});

socket.on('ice-candidate', async ({ candidate, fromId }) => {
  const pc = peerConnections.get(fromId);
  if (pc && candidate) {
    await pc.addIceCandidate(new RTCIceCandidate(candidate));
  }
});

3. ICE, STUN & TURN: NAT Traversal mit coturn

Das größte praktische Problem bei WebRTC: Die meisten Geräte befinden sich hinter NAT (Network Address Translation) und haben keine direkt erreichbare öffentliche IP. ICE (Interactive Connectivity Establishment) löst dieses Problem durch systematisches Testen verschiedener Verbindungswege.

ICE Candidate-Typen

Typ Beschreibung Erfolgsrate
host Lokale IP-Adresse des Geräts Nur im gleichen LAN
srflx (STUN) Öffentliche IP via STUN-Server ermittelt ~75% (scheitert bei sym. NAT)
prflx Peer-reflexive, während ICE-Check entdeckt Selten, automatisch
relay (TURN) Traffic über TURN-Server relayed ~100% (Fallback)

coturn Server Setup (Ubuntu)

# coturn installieren
apt-get install coturn -y

# /etc/turnserver.conf
listening-port=3478
tls-listening-port=5349
listening-ip=0.0.0.0
external-ip=DEINE_ÖFFENTLICHE_IP
relay-ip=DEINE_ÖFFENTLICHE_IP

# Auth
fingerprint
lt-cred-mech
user=webrtc:sicheres_passwort
realm=turn.deine-app.com

# TLS (Let's Encrypt)
cert=/etc/letsencrypt/live/turn.deine-app.com/fullchain.pem
pkey=/etc/letsencrypt/live/turn.deine-app.com/privkey.pem

# Logging
log-file=/var/log/coturn/turnserver.log
verbose

# coturn starten
systemctl enable coturn
systemctl start coturn

iceServers Konfiguration im Client:

const configuration = {
  iceServers: [
    // Google STUN — kostenlos, kein Auth
    { urls: 'stun:stun.l.google.com:19302' },
    // Eigener TURN — Fallback für sym. NAT
    {
      urls: [
        'turn:turn.deine-app.com:3478',
        'turn:turn.deine-app.com:3478?transport=tcp',
        'turns:turn.deine-app.com:5349' // TLS
      ],
      username: 'webrtc',
      credential: 'sicheres_passwort'
    }
  ],
  iceTransportPolicy: 'all', // 'relay' = nur TURN erzwingen
  bundlePolicy: 'max-bundle',
  rtcpMuxPolicy: 'require'
};

TURN Credentials dynamisch generieren (Sicherheit)

// server.js — Temporäre TURN-Credentials (HMAC-basiert)
import crypto from 'crypto';

function generateTurnCredentials(secret, ttlSeconds = 3600) {
  const unixTimestamp = Math.floor(Date.now() / 1000) + ttlSeconds;
  const username = `${unixTimestamp}:webrtc-user`;
  const credential = crypto
    .createHmac('sha1', secret)
    .update(username)
    .digest('base64');
  return { username, credential };
}

// REST Endpoint: Client fragt temporäre Credentials an
app.get('/api/turn-credentials', (req, res) => {
  const credentials = generateTurnCredentials(process.env.TURN_SECRET);
  res.json({
    iceServers: [{
      urls: ['turn:turn.deine-app.com:3478'],
      ...credentials
    }]
  });
});
coturn Firewall-Regeln: UDP 3478 (STUN/TURN), TCP 3478 (TURN/TCP), TCP 5349 (TURNS/TLS), UDP 49152-65535 (Relay Ports). Bei Hetzner/DigitalOcean ufw-Regeln entsprechend setzen.

4. MediaStream & Video: getUserMedia & Screen Sharing

Die MediaStream API liefert Audio- und Video-Tracks vom Mikrofon, von der Kamera oder vom Bildschirm. Die Tracks werden der RTCPeerConnection hinzugefügt und dann automatisch verschlüsselt und zum Remote-Peer übertragen.

getUserMedia: Kamera & Mikrofon

// Kamera und Mikrofon anfordern
async function getLocalStream() {
  try {
    const stream = await navigator.mediaDevices.getUserMedia({
      video: {
        width: { ideal: 1280, max: 1920 },
        height: { ideal: 720, max: 1080 },
        frameRate: { ideal: 30 },
        facingMode: 'user'
      },
      audio: {
        echoCancellation: true,
        noiseSuppression: true,
        sampleRate: 48000,
        channelCount: 1
      }
    });

    // Lokales Video ohne Ton (Feedback vermeiden)
    localVideo.srcObject = stream;
    localVideo.muted = true;

    return stream;
  } catch (err) {
    if (err.name === 'NotAllowedError') {
      showError('Kamera-Zugriff verweigert. Bitte Browserberechtigungen prüfen.');
    } else if (err.name === 'NotFoundError') {
      showError('Keine Kamera gefunden.');
    } else {
      showError(`Fehler: ${err.message}`);
    }
    throw err;
  }
}

// Tracks zur PeerConnection hinzufügen
const localStream = await getLocalStream();
localStream.getTracks().forEach(track => {
  pc.addTrack(track, localStream);
});

ontrack: Remote Video empfangen

// Remote Stream im Video-Element anzeigen
pc.ontrack = (event) => {
  const [remoteStream] = event.streams;

  if (remoteVideo.srcObject !== remoteStream) {
    remoteVideo.srcObject = remoteStream;
    console.log('Remote Stream empfangen', remoteStream.id);
  }

  // Track-Statistiken für Debug
  event.track.onmute = () => console.log('Remote Track stumm');
  event.track.onunmute = () => console.log('Remote Track aktiv');
};

Track-Management: Kamera/Mikrofon steuern

// Mikrofon stummschalten (Track bleibt in PeerConnection)
function toggleMute() {
  const audioTrack = localStream.getAudioTracks()[0];
  audioTrack.enabled = !audioTrack.enabled;
  muteBtn.textContent = audioTrack.enabled ? 'Stummschalten' : 'Ton an';
}

// Kamera deaktivieren
function toggleCamera() {
  const videoTrack = localStream.getVideoTracks()[0];
  videoTrack.enabled = !videoTrack.enabled;
}

// Kamera wechseln (z.B. auf Rückkamera)
async function switchCamera(deviceId) {
  const newStream = await navigator.mediaDevices.getUserMedia({
    video: { deviceId: { exact: deviceId } }
  });
  const newTrack = newStream.getVideoTracks()[0];
  const sender = pc.getSenders().find(s => s.track.kind === 'video');
  await sender.replaceTrack(newTrack); // Kein Renegotiation nötig!
}

Screen Sharing: getDisplayMedia

// Bildschirm teilen (Tab, Fenster oder gesamter Bildschirm)
async function startScreenShare() {
  const screenStream = await navigator.mediaDevices.getDisplayMedia({
    video: {
      displaySurface: 'monitor', // 'browser', 'window', 'monitor'
      logicalSurface: true,
      cursor: 'always',
      frameRate: { ideal: 30, max: 60 }
    },
    audio: true // System-Audio mitschneiden (Chrome)
  });

  const screenTrack = screenStream.getVideoTracks()[0];

  // Sender ersetzen: Kamera → Bildschirm (kein Renegotiation)
  const videoSender = pc.getSenders().find(s => s.track?.kind === 'video');
  await videoSender.replaceTrack(screenTrack);

  // Automatisch stoppen wenn User Browser-Button klickt
  screenTrack.onended = async () => {
    const cameraTrack = localStream.getVideoTracks()[0];
    await videoSender.replaceTrack(cameraTrack);
    console.log('Screen Share beendet, zurück zu Kamera');
  };
}

// Verfügbare Eingabegeräte auflisten
const devices = await navigator.mediaDevices.enumerateDevices();
const cameras = devices.filter(d => d.kind === 'videoinput');
const microphones = devices.filter(d => d.kind === 'audioinput');
Bandbreiten-Kontrolle via RTCRtpSender: Claude Code setzt automatisch korrekte Encoding-Parameter für adaptive Bitrate. sender.setParameters() erlaubt Bitrate-Limits ohne Renegotiation — wichtig für mobile Nutzer mit schlechter Verbindung.

5. Data Channels: Binärdaten & File Transfer

RTCDataChannel ermöglicht direkten Datenaustausch zwischen Peers — ohne Server-Umweg. Die Latenz ist deutlich geringer als bei WebSocket-basierten Lösungen, da die Daten direkt von Peer zu Peer fließen (wenn ICE ohne TURN funktioniert). Use Cases: Chat, Spielzüge, kollaborative Editoren, File Transfer, Binary Protocol für IoT.

DataChannel erstellen und verwenden

// Peer A erstellt den DataChannel VOR dem Offer
const dataChannel = pc.createDataChannel('chat', {
  ordered: true,         // TCP-ähnlich: Reihenfolge garantiert
  maxRetransmits: 3,     // Max. Wiederholungen (null = unbegrenzt)
  // maxPacketLifeTime: 3000,  // Alternativ: Timeout in ms
});

dataChannel.onopen = () => {
  console.log('DataChannel offen');
  dataChannel.send('Hallo von Peer A!');
};

dataChannel.onmessage = (event) => {
  const message = typeof event.data === 'string'
    ? event.data
    : '[Binärdaten]';
  console.log('Empfangen:', message);
};

dataChannel.onclose = () => console.log('DataChannel geschlossen');
dataChannel.onerror = (err) => console.error('DataChannel Fehler:', err);

// Peer B empfängt den Channel via ondatachannel
pc.ondatachannel = (event) => {
  const receivedChannel = event.channel;
  receivedChannel.onmessage = (e) => handleMessage(e.data);
  receivedChannel.onopen = () => receivedChannel.send('Hallo von Peer B!');
};

Reliable vs. Unreliable Channels

ordered: true, maxRetransmits: null — Reliable (wie TCP). Für Chat, Dateiübertragung, Spielzüge wo Reihenfolge wichtig ist.

ordered: false, maxRetransmits: 0 — Unreliable (wie UDP). Für Echtzeit-Positions-Updates, Cursor-Bewegungen, wo verlorene Pakete akzeptabel sind.

ordered: false, maxPacketLifeTime: 100 — Time-constrained. Paket wird max. 100ms versucht zu senden — gut für Audio-Jitter-Kontrolle.

priority: 'high' — DSCP-Marking für QoS-fähige Netzwerke.

File Transfer via DataChannel

// Datei-Transfer: Sender-Seite
async function sendFile(file, channel) {
  const CHUNK_SIZE = 16384; // 16 KB pro Chunk
  const buffer = await file.arrayBuffer();

  // Metadaten zuerst senden
  channel.send(JSON.stringify({
    type: 'file-meta',
    name: file.name,
    size: file.size,
    mimeType: file.type,
    chunks: Math.ceil(file.size / CHUNK_SIZE)
  }));

  // Chunks sequenziell senden (Buffer-Level beachten)
  let offset = 0;
  while (offset < buffer.byteLength) {
    // Warten wenn SendBuffer voll (Backpressure)
    if (channel.bufferedAmount > 65536) {
      await new Promise(resolve => {
        channel.onbufferedamountlow = resolve;
      });
    }

    const chunk = buffer.slice(offset, offset + CHUNK_SIZE);
    channel.send(chunk);
    offset += CHUNK_SIZE;

    // Fortschritt melden
    const progress = Math.round((offset / buffer.byteLength) * 100);
    updateProgress(progress);
  }

  channel.send(JSON.stringify({ type: 'file-end' }));
}

// Empfänger-Seite
let incomingFile = null;
const receivedChunks = [];

receivedChannel.onmessage = (event) => {
  if (typeof event.data === 'string') {
    const msg = JSON.parse(event.data);
    if (msg.type === 'file-meta') {
      incomingFile = msg;
      receivedChunks.length = 0;
    } else if (msg.type === 'file-end') {
      const blob = new Blob(receivedChunks, { type: incomingFile.mimeType });
      const url = URL.createObjectURL(blob);
      triggerDownload(url, incomingFile.name);
    }
  } else {
    receivedChunks.push(event.data); // ArrayBuffer Chunk
  }
};
DataChannel Limits 2026: Max Message Size variiert per Browser (Chrome: 256 KB, Firefox: unbegrenzt aber 64 KB empfohlen). Für große Dateien immer Chunking implementieren. bufferedAmountLowThreshold auf 65536 setzen für effiziente Backpressure.

6. Production: simple-peer, mediasoup SFU, Monitoring

Für echte Production-Apps reicht der direkte RTCPeerConnection-Code meist aus — aber es gibt Abstraktions-Libraries und Skalierungsstrategien die den Entwicklungsaufwand erheblich reduzieren.

simple-peer: Abstraktion für schnelle Entwicklung

// npm install simple-peer
import SimplePeer from 'simple-peer';

// Initiator-Seite (erstellt Offer automatisch)
const peer = new SimplePeer({
  initiator: true,
  trickle: true,  // Trickle ICE für schnelleren Connect
  stream: localStream,
  config: {
    iceServers: [
      { urls: 'stun:stun.l.google.com:19302' },
      { urls: 'turn:turn.deine-app.com:3478',
        username: 'webrtc', credential: 'passwort' }
    ]
  }
});

// Signal-Event: SDP + ICE Candidates gebündelt
peer.on('signal', (data) => {
  socket.emit('signal', { roomId, data });
});

peer.on('stream', (remoteStream) => {
  remoteVideo.srcObject = remoteStream;
});

peer.on('data', (data) => {
  console.log('DataChannel Nachricht:', data.toString());
});

peer.on('error', (err) => console.error('simple-peer Fehler:', err));
peer.on('close', () => console.log('Verbindung getrennt'));

// Signal von Remote weitergeben
socket.on('signal', ({ data }) => peer.signal(data));

mediasoup SFU: Skalierung für Gruppen-Calls

Ab 4-6 Teilnehmern wird die Mesh-Topologie zu ineffizient — jeder Peer sendet N-1 Video-Streams. Ein SFU (Selective Forwarding Unit) nimmt einen Stream pro Teilnehmer an und verteilt ihn selektiv weiter. Der Server entschlüsselt die Mediadaten dabei nicht — SRTP bleibt Ende-zu-Ende.

mediasoup v3 Architektur:

Worker — Node.js Worker-Thread, verwaltet Transports und Routers auf CPU-Kern

Router — Media-Routing-Kontext (ein Router = ein "Room" / eine Sitzung)

WebRtcTransport — Client-seitige Verbindung (je ein Transport für Send + Receive)

Producer — Eingehender Media-Stream vom Client zum SFU

Consumer — Ausgehender Media-Stream vom SFU zum Client

// server.js — mediasoup SFU Setup (vereinfacht)
import * as mediasoup from 'mediasoup';

const worker = await mediasoup.createWorker({
  rtcMinPort: 10000,
  rtcMaxPort: 10200,
  logLevel: 'warn'
});

const router = await worker.createRouter({
  mediaCodecs: [
    { kind: 'audio', mimeType: 'audio/opus', clockRate: 48000, channels: 2 },
    { kind: 'video', mimeType: 'video/VP8', clockRate: 90000 },
    { kind: 'video', mimeType: 'video/H264', clockRate: 90000,
      parameters: { 'packetization-mode': 1, 'level-asymmetry-allowed': 1 } }
  ]
});

// Client verbindet sich: Transport erstellen
socket.on('create-transport', async ({ direction }, callback) => {
  const transport = await router.createWebRtcTransport({
    listenIps: [{ ip: '0.0.0.0', announcedIp: process.env.PUBLIC_IP }],
    enableUdp: true,
    enableTcp: true,
    preferUdp: true
  });

  callback({
    id: transport.id,
    iceParameters: transport.iceParameters,
    iceCandidates: transport.iceCandidates,
    dtlsParameters: transport.dtlsParameters
  });
});

Reconnection-Logic: Automatischer Reconnect

// Robuste Reconnection-Logik für Production
class WebRTCConnection {
  constructor(config) {
    this.config = config;
    this.pc = null;
    this.reconnectAttempts = 0;
    this.maxReconnects = 5;
  }

  async connect() {
    this.pc = new RTCPeerConnection(this.config);

    this.pc.onconnectionstatechange = async () => {
      const state = this.pc.connectionState;
      console.log(`Connection: ${state}`);

      if (state === 'disconnected') {
        // Kurz warten — evtl. erholt sich die Verbindung von selbst
        setTimeout(() => {
          if (this.pc.connectionState !== 'connected') {
            this.pc.restartIce();
          }
        }, 3000);
      }

      if (state === 'failed') {
        if (this.reconnectAttempts < this.maxReconnects) {
          this.reconnectAttempts++;
          const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
          console.log(`Reconnect in ${delay}ms (Versuch ${this.reconnectAttempts})`);
          setTimeout(() => this.reconnect(), delay);
        } else {
          this.onFatalError?.();
        }
      }

      if (state === 'connected') {
        this.reconnectAttempts = 0;
      }
    };
  }

  async reconnect() {
    this.pc.close();
    await this.connect();
    // Neues Offer erstellen und über Signaling senden
    await this.createAndSendOffer();
  }
}

WebRTC Stats: Monitoring & QoS

// RTCStatsReport — Verbindungsqualität überwachen
async function getConnectionStats(pc) {
  const stats = await pc.getStats();
  const result = {};

  stats.forEach(report => {
    if (report.type === 'inbound-rtp' && report.kind === 'video') {
      result.video = {
        packetsLost: report.packetsLost,
        packetsReceived: report.packetsReceived,
        bytesReceived: report.bytesReceived,
        framesDecoded: report.framesDecoded,
        jitter: report.jitter
      };
    }
    if (report.type === 'candidate-pair' && report.state === 'succeeded') {
      result.network = {
        rtt: report.currentRoundTripTime * 1000, // ms
        availableOutgoingBitrate: report.availableOutgoingBitrate,
        bytesSent: report.bytesSent
      };
    }
  });

  return result;
}

// Alle 5 Sekunden Statistiken loggen
setInterval(async () => {
  const stats = await getConnectionStats(pc);
  if (stats.network?.rtt > 300) {
    console.warn(`Hohe Latenz: ${stats.network.rtt}ms`);
  }
}, 5000);

Production Checklist 2026:

TURN-Server: Eigener coturn auf eigenem VPS — nie nur Google STUN vertrauen (scheitert bei ~25% der Nutzer)

Signaling-Security: JWT-Auth auf Socket.io, Rate Limiting, Room-Size-Limits

HTTPS/WSS: getUserMedia funktioniert nur über HTTPS (außer localhost)

Mobile: iOS Safari ab 15.4 unterstützt Screen Sharing; Android Chrome voll unterstützt

Codec-Fallback: VP8 als Baseline, VP9/H264/AV1 wo verfügbar

SFU ab 4+ Peers: mediasoup (Node.js), Janus, LiveKit oder Livekit Cloud

Claude Code WebRTC-Workflow: Beschreib deinen Use Case — 1:1 Videocall, Gruppen-Meeting, Screen Sharing, Live-Stream oder P2P-File-Transfer. Claude generiert die vollständige Implementierung: RTCPeerConnection, Signaling-Server in Node.js, TURN-Konfiguration, MediaStream-Handling und Reconnection-Logic — alles mit TypeScript-Types, Error-Boundaries und Production-ready Security.

WebRTC mit Claude Code bauen

Von der minimalen 1:1-Videocall-App bis zum skalierbaren SFU-Setup mit mediasoup — Claude Code generiert production-ready WebRTC-Code auf Deutsch, inkl. TURN-Server-Konfiguration und Reconnection-Logic.

Kostenlos starten