Start · Sprachen · JavaScript · Referenz · RTCPeerConnection: negotiationneeded

RTCPeerConnection: negotiationneeded

Ereignis

Wird an eine <code>RTCPeerConnection</code> gesendet, wenn sich die Verbindung so geändert hat, dass eine neue SDP-Aushandlung (Offer/Answer) über den Signalisierungskanal nötig ist.

seit JavaScript Baseline: alle modernen Browser (Chrome, Edge, Fir Kategorie: webapi

Signatur

pc.addEventListener("negotiationneeded", handler)

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

Typ
void
Beschreibung
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));
Offer gesendet: offer

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);
Neues Offer: 512 Zeichen SDP (Länge variiert)

// Wichtig · Fallstricke

  • Glare (Offer-Kollision): Wenn beide Peers gleichzeitig auf negotiationneeded reagieren, schicken beide ein Offer. Nutze das Muster „Perfect Negotiation“ mit einer höflichen und einer unhöflichen Rolle, sonst landet die Verbindung in InvalidStateError-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 RTCDataChannel löst eine Aushandlung aus. Weitere Kanäle nutzen den bestehenden SCTP-Transport.
  • Kein Ereignis, wenn signalingState nicht "stable" ist oder die Verbindung geschlossen wurde (pc.close()).
  • Ältere Safari- und Chrome-Versionen kennen setLocalDescription() ohne Argument nicht. Dort await pc.setLocalDescription(await pc.createOffer()) verwenden.
  • Signalisierungsnachrichten enthalten SDP mit IP-Adressen und Fingerprints. Übertrage sie nur über authentifizierte, verschlüsselte Kanäle (wss://).