Signatur
Beschreibung
Das Ereignis negotiationneeded löst der Browser aus, wenn sich an einer RTCPeerConnection etwas ändert, das die Gegenseite erfahren muss. Beispiele: Hinzufügen oder Entfernen eines Tracks mit addTrack()/removeTrack(), Anlegen eines ersten RTCDataChannel, Ändern der Richtung eines Transceivers oder ein ICE-Neustart mit restartIce(). Das Ereignis ist ein einfaches Event ohne zusätzliche Eigenschaften, bubbelt nicht und ist nicht abbrechbar.
Im Handler erzeugst du normalerweise ein neues Angebot: setLocalDescription() ohne Argument (erzeugt implizit ein Offer) aufrufen und danach pc.localDescription über deinen Signalisierungskanal (z. B. WebSocket) an die Gegenseite schicken. Das ist der zentrale Einstiegspunkt für das Muster der „Perfect Negotiation“, bei dem beide Peers denselben Code benutzen und Kollisionen von Angeboten („Glare“) sauber auflösen.
Den Handler kannst du mit addEventListener("negotiationneeded", handler) oder über die Eigenschaft pc.onnegotiationneeded = handler registrieren. Der Browser löst das Ereignis nur aus, wenn signalingState gleich "stable" ist. Gibt es währenddessen weitere Änderungen, bündelt er sie und feuert erst nach Abschluss der laufenden Aushandlung erneut, falls dann noch etwas offen ist.
Parameter
| Name | Typ | Default | Beschreibung |
|---|---|---|---|
| $type Pflicht | string | Der Ereignisname "negotiationneeded". |
|
| $handler Pflicht | (event: Event) => void | Promise<void> | Callback, der ein einfaches Event erhält. Er ist meist async, weil hier setLocalDescription() aufgerufen wird. |
|
| $options | boolean | AddEventListenerOptions | false | Optionale Listener-Optionen wie once oder signal (zum Abmelden per AbortController). |
Rückgabewert
addEventListener() gibt nichts zurück. Der Handler bekommt ein Event ohne spezielle Eigenschaften.Beispiele
Angebot erzeugen und über WebSocket senden (addEventListener)
const signaling = new WebSocket('wss://example.com/signal');
const pc = new RTCPeerConnection({
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
});
pc.addEventListener('negotiationneeded', async () => {
try {
// Ohne Argument wird automatisch ein passendes Offer erzeugt
await pc.setLocalDescription();
signaling.send(JSON.stringify({ description: pc.localDescription }));
console.log('Offer gesendet:', pc.localDescription.type);
} catch (err) {
console.error('Aushandlung fehlgeschlagen:', err);
}
});
// Ein Track löst negotiationneeded aus
const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
stream.getTracks().forEach((track) => pc.addTrack(track, stream));
Perfect Negotiation mit onnegotiationneeded
const pc = new RTCPeerConnection();
const signaling = new BroadcastChannel('webrtc-demo'); // Demo-Signalisierung
const polite = location.hash === '#polite';
let makingOffer = false;
let ignoreOffer = false;
pc.onnegotiationneeded = async () => {
try {
makingOffer = true;
await pc.setLocalDescription();
signaling.postMessage({ description: pc.localDescription.toJSON() });
} catch (err) {
console.error(err);
} finally {
makingOffer = false;
}
};
pc.onicecandidate = ({ candidate }) => {
if (candidate) signaling.postMessage({ candidate: candidate.toJSON() });
};
signaling.onmessage = async ({ data: { description, candidate } }) => {
if (description) {
const collision = description.type === 'offer' &&
(makingOffer || pc.signalingState !== 'stable');
ignoreOffer = !polite && collision;
if (ignoreOffer) return;
await pc.setRemoteDescription(description);
if (description.type === 'offer') {
await pc.setLocalDescription();
signaling.postMessage({ description: pc.localDescription.toJSON() });
}
} else if (candidate) {
try {
await pc.addIceCandidate(candidate);
} catch (err) {
if (!ignoreOffer) throw err;
}
}
};
pc.createDataChannel('chat'); // löst negotiationneeded aus
Listener per AbortController wieder entfernen
const pc = new RTCPeerConnection();
const controller = new AbortController();
const onNegotiation = async () => {
await pc.setLocalDescription();
console.log('Neues Offer:', pc.localDescription.sdp.length, 'Zeichen SDP');
};
pc.addEventListener('negotiationneeded', onNegotiation, { signal: controller.signal });
pc.createDataChannel('daten');
// Später, z. B. beim Beenden des Anrufs:
setTimeout(() => {
controller.abort(); // entspricht removeEventListener('negotiationneeded', onNegotiation)
pc.onnegotiationneeded = null;
pc.close();
}, 5000);
// Wichtig · Fallstricke
- Glare (Offer-Kollision): Wenn beide Peers gleichzeitig auf
negotiationneededreagieren, schicken beide ein Offer. Nutze das Muster „Perfect Negotiation“ mit einer höflichen und einer unhöflichen Rolle, sonst landet die Verbindung inInvalidStateError-Fehlern. - Nicht manuell
createOffer()vor dem Ereignis aufrufen: Wenn du zusätzlich selbst verhandelst, entstehen doppelte Aushandlungen. Überlass dem Ereignis die Steuerung. - Das Ereignis feuert asynchron, also erst nachdem der aktuelle Code durchgelaufen ist. Mehrere
addTrack()-Aufrufe hintereinander ergeben deshalb meist nur ein Ereignis. - Nur der erste
RTCDataChannellöst eine Aushandlung aus. Weitere Kanäle nutzen den bestehenden SCTP-Transport. - Kein Ereignis, wenn
signalingStatenicht"stable"ist oder die Verbindung geschlossen wurde (pc.close()). - Ältere Safari- und Chrome-Versionen kennen
setLocalDescription()ohne Argument nicht. Dortawait pc.setLocalDescription(await pc.createOffer())verwenden. - Signalisierungsnachrichten enthalten SDP mit IP-Adressen und Fingerprints. Übertrage sie nur über authentifizierte, verschlüsselte Kanäle (
wss://).