Signatur
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 danachsetLocalDescription()auf und sendet das Offer an die Gegenseite. - Der Angerufene ruft
setRemoteDescription(),createAnswer()undsetLocalDescription()auf und schickt die Answer zurück. - Beide Seiten tauschen die ICE-Kandidaten aus dem Event
icecandidateaus und übergeben sie der Gegenseite mitaddIceCandidate().
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
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);
});
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();
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);
};
// Wichtig · Fallstricke
- Sicherer Kontext:
getUserMedia()funktioniert nur über HTTPS oderlocalhost. 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, wennsetRemoteDescription()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). MiticeTransportPolicy: \"relay\"erzwingt man TURN und verbirgt so die öffentliche IP. - Veraltete API: Callback-Varianten von
createOffer()und ähnlichen Methoden,addStream()und das Eventonaddstreamsind veraltet. Stattdessen nutzt man Promises,addTrack()undontrack. Ältere Safari-Versionen brauchen gegebenenfalls adapter.js. - Aufräumen: Nach
close()lässt sich die Instanz nicht wiederverwenden. Lokale Tracks muss man zusätzlich mittrack.stop()beenden, sonst bleibt die Kamera aktiv.