Erste Schritte mit Peer-Verbindungen

Peer-Verbindungen sind der Teil der WebRTC-Spezifikationen, der sich mit der Verbindung von zwei Anwendungen auf verschiedenen Computern befasst, um über ein Peer-to-Peer-Protokoll zu kommunizieren. Die Kommunikation zwischen Peers kann Video, Audio oder beliebige Binärdaten (für Clients, die die RTCDataChannel API unterstützen) umfassen. Damit zwei Peers eine Verbindung herstellen können, müssen beide Clients eine ICE-Serverkonfiguration bereitstellen. Dies ist entweder ein STUN- oder ein TURN-Server. Seine Aufgabe ist es, ICE-Kandidaten für jeden Client bereitzustellen, 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 Signalisierungskomponente ist jedoch nicht Teil davon. Signalisierung ist erforderlich, damit zwei Peers mitteilen können, wie sie sich verbinden sollen. Normalerweise wird dies über eine reguläre HTTP-basierte Web-API (d.h. einen REST-Dienst oder einen anderen RPC-Mechanismus) gelöst, über die Webanwendungen die erforderlichen Informationen weiterleiten können, bevor die Peer-Verbindung hergestellt wird.

Das folgende Code-Snippet zeigt, wie dieser fiktive Signalisierungsdienst verwendet werden kann, um Nachrichten asynchron zu senden und zu empfangen. 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 verarbeitet. Der Konstruktor für diese Klasse verwendet ein einzelnes RTCConfiguration-Objekt als Parameter. Dieses Objekt definiert, wie die Peer-Verbindung eingerichtet wird, und sollte Informationen zu den zu verwendenden ICE-Servern enthalten.

Sobald RTCPeerConnection erstellt wurde, müssen wir ein SDP-Angebot oder eine SDP-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 enthalten.

Um die Einrichtung der Peer-Verbindung von der Anruferseite aus zu starten, 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 Signalisierungschannel ein, um zu erkennen, wenn von der Empfängerseite eine Antwort auf unsere angebotene Sitzungsbeschreibung eingeht.

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 legen wir das empfangene Angebot mit setRemoteDescription() fest. 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 aufrufende 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 lokalen als auch die Remote-Sitzungsbeschreibungen festgelegt haben, kennen sie die Funktionen des Remote-Peers. Das bedeutet nicht, dass die Verbindung zwischen den Peers bereit ist. Dazu müssen wir die ICE-Kandidaten an 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 Informationen zur Verbindung 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 als ICE bezeichnet 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. Diese werden unter dem umfassenderen Begriff „Internet Connectivity Establishment“ zusammengefasst. Beim Erstellen einer WebRTC-Verbindung stellen wir in der Regel einen oder mehrere ICE-Server in der Konfiguration für das RTCPeerConnection-Objekt bereit.

Trickle ICE

Sobald ein RTCPeerConnection-Objekt erstellt wurde, verwendet das zugrunde liegende Framework die bereitgestellten ICE-Server, um Kandidaten für die Verbindungsherstellung zu sammeln (ICE-Kandidaten). Das Ereignis icegatheringstatechange auf RTCPeerConnection gibt an, in welchem Zustand sich die ICE-Sammlung befindet (new, gathering oder complete).

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

Um ICE-Kandidaten zu erfassen, fügen Sie einfach einen Listener für das icecandidate-Ereignis hinzu. Das RTCPeerConnectionIceEvent, das für diesen Listener ausgegeben wird, enthält das Attribut candidate, das einen neuen Kandidaten darstellt, der an den Remote-Peer gesendet werden sollte (siehe Signaling).

// 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 sich der Status unserer Peer-Verbindung schließlich in „Verbunden“ ändern. Dazu fügen wir unserem RTCPeerConnection einen Listener hinzu, der auf connectionstatechange-Ereignisse wartet.

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

Dokumentation zur RTCPeerConnection API