Signatur
Beschreibung
Das Ereignis selectedcandidatepairchange wird an ein RTCIceTransport-Objekt gesendet, wenn der ICE-Agent ein neues Kandidatenpaar auswählt. Ein Kandidatenpaar besteht aus einem lokalen und einem entfernten ICE-Kandidaten. Zusammen beschreiben die beiden die Endpunkte, über die die Medien- oder Datenpakete tatsächlich laufen. Das Ereignis tritt zum ersten Mal auf, wenn eine funktionierende Verbindung zustande kommt. Danach kommt es immer dann, wenn der Agent den Weg wechselt, etwa nach einem ICE-Neustart, beim Wechsel zwischen WLAN und Mobilfunk oder beim Umstieg von einem TURN-Relais auf eine direkte Verbindung.
Das Ereignis ist ein einfaches Event ohne zusätzliche Eigenschaften. Es steigt nicht auf (bubbles) und lässt sich nicht abbrechen. Das neue Paar liest man im Handler mit RTCIceTransport.getSelectedCandidatePair() aus. Die Methode gibt ein Objekt mit den Eigenschaften local und remote zurück, beide vom Typ RTCIceCandidate.
An den RTCIceTransport kommt man über den DTLS-Transport eines Senders oder Empfängers (sender.transport.iceTransport) oder über den SCTP-Transport eines Datenkanals (pc.sctp.transport.iceTransport). Diese Objekte gibt es erst, nachdem die Beschreibungen per setLocalDescription() bzw. setRemoteDescription() gesetzt wurden.
Typische Einsatzzwecke sind Diagnose und Überwachung: Man erkennt damit, ob eine Verbindung direkt (host/srflx) oder über ein TURN-Relais (relay) läuft. Man kann Netzwerkwechsel protokollieren oder dem Nutzer die Verbindungsqualität anzeigen. Den Handler registriert man mit addEventListener() oder über die Eigenschaft onselectedcandidatepairchange.
Parameter
| Name | Typ | Default | Beschreibung |
|---|---|---|---|
| $type Pflicht | string | Der Ereignisname "selectedcandidatepairchange". |
|
| $handler Pflicht | (event: Event) => void | Callback, der beim Wechsel des Kandidatenpaars aufgerufen wird. event.target ist der betroffene RTCIceTransport. |
|
| $options | boolean | AddEventListenerOptions | false | Optionale Listener-Optionen wie once oder signal, zum Beispiel um den Listener über einen AbortController zu entfernen. |
Rückgabewert
addEventListener() gibt nichts zurück. Die Informationen zum neuen Paar liefert getSelectedCandidatePair() im Handler.Beispiele
Gewähltes Kandidatenpaar per addEventListener protokollieren
const pc = new RTCPeerConnection({
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
});
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
const sender = pc.addTrack(stream.getAudioTracks()[0], stream);
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
// ... Offer per Signalisierung senden, Answer empfangen und setzen ...
const iceTransport = sender.transport?.iceTransport;
iceTransport?.addEventListener('selectedcandidatepairchange', (event) => {
const pair = event.target.getSelectedCandidatePair();
if (!pair) return;
const { local, remote } = pair;
console.log(`Lokal: ${local.type} ${local.address}:${local.port} (${local.protocol})`);
console.log(`Entfernt: ${remote.type} ${remote.address}:${remote.port} (${remote.protocol})`);
});
TURN-Relais erkennen über onselectedcandidatepairchange
const pc = new RTCPeerConnection();
const channel = pc.createDataChannel('chat');
const showRelayHint = (isRelay) => {
document.querySelector('#verbindung').textContent = isRelay
? 'Verbindung läuft über TURN-Relais (höhere Latenz möglich)'
: 'Direkte Verbindung aktiv';
};
channel.addEventListener('open', () => {
const iceTransport = pc.sctp?.transport.iceTransport;
if (!iceTransport) return;
iceTransport.onselectedcandidatepairchange = () => {
const pair = iceTransport.getSelectedCandidatePair();
const isRelay = pair?.local.type === 'relay' || pair?.remote.type === 'relay';
showRelayHint(isRelay);
};
});
Listener mit AbortController wieder entfernen
const controller = new AbortController();
const changes = [];
const watchPairChanges = (iceTransport) => {
iceTransport.addEventListener('selectedcandidatepairchange', () => {
const pair = iceTransport.getSelectedCandidatePair();
changes.push({ zeit: Date.now(), lokal: pair?.local.type, entfernt: pair?.remote.type });
console.log(`Paarwechsel Nr. ${changes.length}`);
}, { signal: controller.signal });
};
// Beim Beenden des Anrufs aufräumen
const hangUp = (pc) => {
controller.abort(); // entfernt den Listener
pc.close();
console.log('Protokoll:', changes);
};
Browserübergreifender Ersatz über getStats()
const logSelectedPairViaStats = async (pc) => {
const stats = await pc.getStats();
for (const report of stats.values()) {
if (report.type === 'transport' && report.selectedCandidatePairId) {
const pair = stats.get(report.selectedCandidatePairId);
const local = stats.get(pair.localCandidateId);
const remote = stats.get(pair.remoteCandidateId);
console.log(`Aktives Paar: ${local.candidateType} ↔ ${remote.candidateType}`);
}
}
};
// Fallback für Browser ohne RTCIceTransport-Ereignis (z. B. Firefox)
const pc = new RTCPeerConnection();
pc.addEventListener('iceconnectionstatechange', () => {
if (pc.iceConnectionState === 'connected') logSelectedPairViaStats(pc);
});