Erste Schritte mit Peer-Verbindungen

Peer-Verbindungen sind der Teil der WebRTC-Spezifikationen, der sich mit der Verbindung zweier Anwendungen auf verschiedenen Computern zur Kommunikation über ein Peer-to-Peer-Protokoll befasst. Die Kommunikation zwischen Peers kann aus Video-, Audio- oder beliebigen Binärdaten bestehen (für Clients, die die RTCDataChannel API unterstützen). Damit zwei Peers eine Verbindung herstellen können, müssen beide Clients eine ICE-Serverkonfiguration angeben. Dies ist entweder ein STUN- oder ein TURN-Server. Seine Aufgabe besteht darin, jedem Client ICE-Kandidaten zur Verfügung zu stellen, die dann an den Remote-Peer übertragen werden. Diese Übertragung von ICE-Kandidaten wird allgemein als Signalisierung bezeichnet.

Signalisierung

Die WebRTC-Spezifikation enthält APIs für die Kommunikation mit einem ICE-Server (Internet Connectivity Establishment), die Signalkomponente ist jedoch nicht Teil davon. Signalisierung ist erforderlich, damit zwei Peers austauschen können, wie sie eine Verbindung herstellen sollen. Normalerweise wird dies über eine reguläre HTTP-basierte Web-API (d.h. einen REST-Dienst oder einen anderen RPC-Mechanismus) gelöst, bei der Webanwendungen die erforderlichen Informationen weitergeben können, bevor die Peer-Verbindung hergestellt wird.

Das folgende Code-Snippet zeigt, wie mit diesem fiktiven Signalisierungsdienst Nachrichten asynchron gesendet und empfangen werden können. Dieser wird bei Bedarf in den verbleibenden Beispielen in diesem Leitfaden verwendet.

// Set up an asynchronous communication channel that will be
// used during the peer connection setup
const signalingChannel = new SignalingChannel(remoteClientId);
signalingChannel.addEventListener('message', message => {
    // New message from remote client received
});

// Send an asynchronous message to the remote client
signalingChannel.send('Hello!');

Die Signalisierung kann auf viele verschiedene Arten implementiert werden und die WebRTC-Spezifikation bevorzugt keine bestimmte Lösung.

Peer-Verbindungen initiieren

Jede Peer-Verbindung wird von einem RTCPeerConnection-Objekt verwaltet. Der Konstruktor für diese Klasse nimmt ein einzelnes RTCConfiguration-Objekt als Parameter an. Dieses Objekt definiert, wie die Peer-Verbindung eingerichtet wird, und sollte Informationen zu den zu verwendenden ICE-Servern enthalten.

Nachdem die RTCPeerConnection erstellt wurde, müssen wir ein SDP-Angebot oder ‑Antwort erstellen, je nachdem, ob wir der anrufende oder der empfangende Peer sind. Nachdem das SDP-Angebot oder die SDP-Antwort erstellt wurde, muss es über einen anderen Kanal an den Remote-Peer gesendet werden. Das Übergeben von SDP-Objekten an Remote-Peers wird als Signalisierung bezeichnet und ist nicht in der WebRTC-Spezifikation abgedeckt.

Um die Einrichtung der Peer-Verbindung von der rufenden Seite aus zu initiieren, erstellen wir ein RTCPeerConnection-Objekt und rufen dann createOffer() auf, um ein RTCSessionDescription-Objekt zu erstellen. Diese Sitzungsbeschreibung wird mit setLocalDescription() als lokale Beschreibung festgelegt und dann über unseren Signalisierungskanal an die Empfängerseite gesendet. Außerdem richten wir einen Listener für unseren Signalisierungskanal ein, um zu erkennen, wenn eine Antwort auf unsere angebotene Sitzungsbeschreibung von der Empfängerseite empfangen wird.

async function makeCall() {
    const configuration = {'iceServers': [{'urls': 'stun:stun.l.google.com:19302'}]}
    const peerConnection = new RTCPeerConnection(configuration);
    signalingChannel.addEventListener('message', async message => {
        if (message.answer) {
            const remoteDesc = new RTCSessionDescription(message.answer);
            await peerConnection.setRemoteDescription(remoteDesc);
        }
    });
    const offer = await peerConnection.createOffer();
    await peerConnection.setLocalDescription(offer);
    signalingChannel.send({'offer': offer});
}

Auf der Empfängerseite warten wir auf ein eingehendes Angebot, bevor wir unsere RTCPeerConnection-Instanz erstellen. Anschließend setzen wir das empfangene Angebot mit setRemoteDescription(). Als Nächstes rufen wir createAnswer() auf, um eine Antwort auf das empfangene Angebot zu erstellen. Diese Antwort wird mit setLocalDescription() als lokale Beschreibung festgelegt und dann über unseren Signalisierungsserver an die anrufende Seite gesendet.

const peerConnection = new RTCPeerConnection(configuration);
signalingChannel.addEventListener('message', async message => {
    if (message.offer) {
        peerConnection.setRemoteDescription(new RTCSessionDescription(message.offer));
        const answer = await peerConnection.createAnswer();
        await peerConnection.setLocalDescription(answer);
        signalingChannel.send({'answer': answer});
    }
});

Sobald die beiden Peers sowohl die lokale als auch die Remote-Sitzungsbeschreibung festgelegt haben, kennen sie die Funktionen des Remote-Peers. Das bedeutet nicht, dass die Verbindung zwischen den Peers bereit ist. Damit das funktioniert, müssen wir die ICE-Kandidaten bei jedem Peer erfassen und über den Signalisierungskanal an den anderen Peer übertragen.

ICE-Kandidaten

Bevor zwei Peers über WebRTC kommunizieren können, müssen sie Verbindungsinformationen austauschen. Da die Netzwerkbedingungen je nach einer Reihe von Faktoren variieren können, wird in der Regel ein externer Dienst verwendet, um die möglichen Kandidaten für die Verbindung zu einem Peer zu ermitteln. Dieser Dienst wird ICE genannt und verwendet entweder einen STUN- oder einen TURN-Server. STUN steht für Session Traversal Utilities for NAT und wird in den meisten WebRTC-Anwendungen in der Regel indirekt verwendet.

TURN (Traversal Using Relay NAT) ist die fortschrittlichere Lösung, die die STUN-Protokolle umfasst. Die meisten kommerziellen WebRTC-basierten Dienste verwenden einen TURN-Server, um Verbindungen zwischen Peers herzustellen. Die WebRTC API unterstützt sowohl STUN als auch TURN direkt und wird unter dem umfassenderen Begriff „Internet Connectivity Establishment“ zusammengefasst. Beim Erstellen einer WebRTC-Verbindung geben wir in der Regel einen oder mehrere ICE-Server in der Konfiguration für das RTCPeerConnection-Objekt an.

Trickle ICE

Nachdem ein RTCPeerConnection-Objekt erstellt wurde, verwendet das zugrunde liegende Framework die bereitgestellten ICE-Server, um Kandidaten für die Verbindungsherstellung (ICE-Kandidaten) zu erfassen. Das Ereignis icegatheringstatechange am RTCPeerConnection gibt an, in welchem Status sich die ICE-Datenerhebung befindet (new, gathering oder complete).

Es ist zwar möglich, dass ein Peer wartet, bis die ICE-Erfassung abgeschlossen ist, aber es ist in der Regel viel effizienter, das „Trickle-ICE“-Verfahren zu verwenden und jeden ICE-Kandidaten an den Remote-Peer zu übertragen, sobald er gefunden wird. Dadurch wird die Einrichtungszeit für die Peer-Verbindung erheblich verkürzt und ein Videoanruf kann mit weniger Verzögerungen gestartet werden.

Wenn Sie ICE-Kandidaten erfassen möchten, fügen Sie einfach einen Listener für das Ereignis icecandidate hinzu. Die von diesem Listener ausgestrahlte RTCPeerConnectionIceEvent enthält die candidate-Property, die einen neuen Kandidaten darstellt, der an den Remote-Peer gesendet werden soll (siehe Signalisierung).

// Listen for local ICE candidates on the local RTCPeerConnection
peerConnection.addEventListener('icecandidate', event => {
    if (event.candidate) {
        signalingChannel.send({'new-ice-candidate': event.candidate});
    }
});

// Listen for remote ICE candidates and add them to the local RTCPeerConnection
signalingChannel.addEventListener('message', async message => {
    if (message.iceCandidate) {
        try {
            await peerConnection.addIceCandidate(message.iceCandidate);
        } catch (e) {
            console.error('Error adding received ice candidate', e);
        }
    }
});

Verbindung hergestellt

Sobald ICE-Kandidaten empfangen werden, sollte der Status unserer Peer-Verbindung in „Verbunden“ geändert werden. Um dies zu erkennen, fügen wir unserem RTCPeerConnection einen Listener hinzu, der auf connectionstatechange-Ereignisse achtet.

// Listen for connectionstatechange on the local RTCPeerConnection
peerConnection.addEventListener('connectionstatechange', event => {
    if (peerConnection.connectionState === 'connected') {
        // Peers connected!
    }
});

RTCPeerConnection API-Dokumentation