Start · Sprachen · JavaScript · Referenz · RTCPeerConnection

RTCPeerConnection

Schnittstelle

Die Schnittstelle <code>RTCPeerConnection</code> stellt eine WebRTC-Verbindung zwischen dem lokalen Rechner und einem Remote-Peer für Audio, Video und Daten dar.

seit JavaScript Baseline: alle modernen Browser (Chrome 56+, Firef Kategorie: webapi

Signatur

interface RTCPeerConnection extends EventTarget

Beschreibung

RTCPeerConnection ist die zentrale Schnittstelle von WebRTC. Sie baut eine direkte Peer-to-Peer-Verbindung zwischen zwei Endpunkten auf, hält sie aufrecht, überwacht sie und beendet sie wieder. Darüber lassen sich Medienspuren (Mikrofon, Kamera, Bildschirm) über addTrack() übertragen, und mit createDataChannel() kann man beliebige Daten mit geringer Latenz austauschen.

Für den Aufbau braucht man einen Signalisierungskanal, den man selbst bereitstellt, z. B. über WebSocket oder HTTP. Über diesen Kanal tauschen die Peers SDP-Beschreibungen (Offer/Answer) und ICE-Kandidaten aus. Der typische Ablauf ist:

  • Der Anrufer ruft createOffer() und danach setLocalDescription() auf und sendet das Offer an die Gegenseite.
  • Der Angerufene ruft setRemoteDescription(), createAnswer() und setLocalDescription() auf und schickt die Answer zurück.
  • Beide Seiten tauschen die ICE-Kandidaten aus dem Event icecandidate aus und übergeben sie der Gegenseite mit addIceCandidate().

Wichtige Eigenschaften sind connectionState, iceConnectionState, signalingState, localDescription und remoteDescription. Wichtige Events sind track, datachannel, icecandidate, negotiationneeded und connectionstatechange. Mit getStats() fragt man Statistiken zur Verbindungsqualität ab. close() beendet die Verbindung endgültig.

Hinter NAT und Firewalls findet man den Weg zur Gegenseite mit STUN-Servern. Wenn keine direkte Verbindung möglich ist, leitet ein TURN-Server den Verkehr weiter. Beide trägt man im Konfigurationsobjekt unter iceServers ein.

Parameter

Name Typ Default Beschreibung
$configuration RTCConfiguration {} Konfigurationsobjekt für den Konstruktor new RTCPeerConnection(configuration). Die wichtigsten Felder sind: iceServers (Array aus { urls, username?, credential? } für STUN/TURN), iceTransportPolicy ("all" | "relay"), bundlePolicy ("balanced" | "max-compat" | "max-bundle"), rtcpMuxPolicy, certificates und iceCandidatePoolSize.

Rückgabewert

Typ
RTCPeerConnection
Beschreibung
Eine neue Verbindung mit signalingState "stable" und connectionState "new".

Beispiele

Verbindung mit STUN/TURN-Konfiguration erzeugen

const pc = new RTCPeerConnection({
  iceServers: [
    { urls: 'stun:stun.l.google.com:19302' },
    {
      urls: 'turn:turn.example.com:3478',
      username: 'user',
      credential: 'geheim'
    }
  ]
});

console.log(pc.signalingState, pc.connectionState);

pc.addEventListener('connectionstatechange', () => {
  console.log('Verbindungsstatus:', pc.connectionState);
});
stable new

Lokaler Loopback-Test mit Datenkanal (zwei Peers im selben Tab)

const pcA = new RTCPeerConnection();
const pcB = new RTCPeerConnection();

// ICE-Kandidaten direkt an die Gegenseite weitergeben (statt über einen Signalisierungsserver)
pcA.onicecandidate = ({ candidate }) => candidate && pcB.addIceCandidate(candidate);
pcB.onicecandidate = ({ candidate }) => candidate && pcA.addIceCandidate(candidate);

const channel = pcA.createDataChannel('chat');
channel.onopen = () => channel.send('Hallo von A!');

pcB.ondatachannel = ({ channel: remote }) => {
  remote.onmessage = (e) => console.log('B empfängt:', e.data);
};

const connect = async () => {
  const offer = await pcA.createOffer();
  await pcA.setLocalDescription(offer);
  await pcB.setRemoteDescription(offer);

  const answer = await pcB.createAnswer();
  await pcB.setLocalDescription(answer);
  await pcA.setRemoteDescription(answer);
};

connect();
B empfängt: Hallo von A!

Kamera und Mikrofon senden, Remote-Stream anzeigen (mit WebSocket-Signalisierung)

const signaling = new WebSocket('wss://signal.example.com');
const pc = new RTCPeerConnection({ iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] });

// Lokale Medienspuren hinzufügen
const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
stream.getTracks().forEach((track) => pc.addTrack(track, stream));

// Empfangene Spuren im Video-Element anzeigen
pc.ontrack = ({ streams: [remoteStream] }) => {
  document.querySelector('#remote').srcObject = remoteStream;
};

pc.onicecandidate = ({ candidate }) => {
  if (candidate) signaling.send(JSON.stringify({ candidate }));
};

// Aushandlung starten, sobald der Browser sie anfordert
pc.onnegotiationneeded = async () => {
  await pc.setLocalDescription(); // erzeugt das Offer automatisch
  signaling.send(JSON.stringify({ description: pc.localDescription }));
};

signaling.onmessage = async ({ data }) => {
  const { description, candidate } = JSON.parse(data);
  if (description) {
    await pc.setRemoteDescription(description);
    if (description.type === 'offer') {
      await pc.setLocalDescription();
      signaling.send(JSON.stringify({ description: pc.localDescription }));
    }
  } else if (candidate) {
    await pc.addIceCandidate(candidate);
  }
};

Verbindungsstatistik abfragen und Verbindung sauber beenden

const logRoundTripTime = async (pc) => {
  const stats = await pc.getStats();
  stats.forEach((report) => {
    if (report.type === 'candidate-pair' && report.state === 'succeeded') {
      console.log('RTT (s):', report.currentRoundTripTime);
    }
  });
};

const hangUp = (pc) => {
  pc.getSenders().forEach((sender) => sender.track?.stop());
  pc.close();
  console.log(pc.connectionState);
};
RTT (s): 0.012 closed

// Wichtig · Fallstricke

  • Sicherer Kontext: getUserMedia() funktioniert nur über HTTPS oder localhost. Ohne Medienspuren oder Datenkanal enthält das Offer keine Medien, und es werden keine ICE-Kandidaten gesammelt.
  • Keine eingebaute Signalisierung: WebRTC legt nicht fest, wie Offer, Answer und Kandidaten zur Gegenseite gelangen. Diesen Kanal muss man selbst bauen und absichern, z. B. mit WSS und Authentifizierung.
  • Reihenfolge beachten: addIceCandidate() erst aufrufen, wenn setRemoteDescription() abgeschlossen ist. Früher eintreffende Kandidaten sollte man zwischenspeichern. Treffen Offers von beiden Seiten gleichzeitig ein (Glare), hilft das Muster „Perfect Negotiation“ mit einer höflichen und einer unhöflichen Seite.
  • TURN ist in der Praxis Pflicht: Hinter symmetrischem NAT oder in Firmennetzen scheitern reine STUN-Verbindungen oft. TURN-Zugangsdaten sollte man nur kurzlebig ausgeben und nicht fest im Frontend-Code hinterlegen.
  • Datenschutz: ICE-Kandidaten können lokale IP-Adressen verraten. Moderne Browser verbergen sie mit mDNS-Hostnamen (*.local). Mit iceTransportPolicy: \"relay\" erzwingt man TURN und verbirgt so die öffentliche IP.
  • Veraltete API: Callback-Varianten von createOffer() und ähnlichen Methoden, addStream() und das Event onaddstream sind veraltet. Stattdessen nutzt man Promises, addTrack() und ontrack. Ältere Safari-Versionen brauchen gegebenenfalls adapter.js.
  • Aufräumen: Nach close() lässt sich die Instanz nicht wiederverwenden. Lokale Tracks muss man zusätzlich mit track.stop() beenden, sonst bleibt die Kamera aktiv.