Erste Schritte mit Peer-Verbindungen

Peer-Verbindungen sind Teil der WebRTC-Spezifikationen, die sich mit der Verbindung von zwei Anwendungen auf verschiedenen Computern zur Kommunikation über ein Peer-to-Peer-Protokoll befassen. Die Kommunikation zwischen Peers kann Video-, Audio- oder beliebige Binärdaten sein (für Clients, die die RTCDataChannel API unterstützen). Damit ermittelt werden kann, wie zwei Peers eine Verbindung herstellen können, müssen beide Clients eine ICE-Serverkonfiguration angeben. Dies ist entweder ein STUN oder ein TURN-Server und 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 Signaling bezeichnet.

Signalisierung

Die WebRTC-Spezifikation enthält APIs für die Kommunikation mit einem ICE-Server (Internet Connectivity Einrichten). Die Signalkomponente ist jedoch nicht Teil dieser Spezifikation. Es ist Signaling erforderlich, damit zwei Peers teilen können, wie sie miteinander verbunden sind. Normalerweise wird dies durch eine reguläre HTTP-basierte Web API (d.h. einen REST-Dienst oder einen anderen RPC-Mechanismus) gelöst, bei dem Webanwendungen die erforderlichen Informationen weiterleiten können, bevor die Peer-Verbindung initiiert wird.

Das folgende Code-Snippet zeigt, wie dieser fiktive Signaling-Dienst zum Senden und Empfangen von Nachrichten asynchron verwendet werden kann. Er wird gegebenenfalls in den restlichen Beispielen in dieser Anleitung 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!');

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

Peer-Verbindungen werden gestartet

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 die Peer-Verbindung. Es sollte Informationen zu den zu verwendenden ICE-Servern enthalten.

Sobald RTCPeerConnection erstellt ist, müssen wir ein SDP-Angebot oder eine Antwort erstellen, je nachdem, ob wir der aufrufende Peer oder der Peer-Empfänger sind. Sobald das SDP-Angebot oder die Antwort erstellt wurde, muss es über einen anderen Kanal an den Remote-Peer gesendet werden. Die Übergabe von SDP-Objekten an Remote-Peers wird Signaling genannt und ist nicht durch die WebRTC-Spezifikation abgedeckt.

Zum Einrichten der Peer-Verbindungseinrichtung von der Aufrufseite erstellen wir ein RTCPeerConnection-Objekt und rufen dann createOffer() auf, um ein RTCSessionDescription-Objekt zu erstellen. Diese Sitzungsbeschreibung wird mithilfe von setLocalDescription() als lokale Beschreibung festgelegt und dann über unseren Signalkanal an die Empfängerseite gesendet. Außerdem richten wir einen Listener für unseren Signalisierungskanal ein, wenn eine Antwort auf die 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 legen wir das empfangene Angebot mit setRemoteDescription() fest. Als Nächstes rufen wir createAnswer() auf, um eine Antwort auf das erhaltene Angebot zu erstellen. Diese Antwort wird mit setLocalDescription() als lokale Beschreibung festgelegt und dann über unseren Signalserver an die Aufrufseite 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 definiert haben, kennen sie die Funktionen des Remote-Peers. Dies bedeutet nicht, dass die Verbindung zwischen den Peers bereit ist. Damit das funktioniert, müssen wir die ICE-Kandidaten an jedem Peer erfassen und über den Signalisierungskanal an den anderen Peer übertragen.

ICE-Kandidaten

Bevor zwei Peers mit WebRTC zusammenarbeiten können, müssen sie Konnektivitätsinformationen austauschen. Da die Netzwerkbedingungen abhängig von einer Reihe von Faktoren variieren können, wird normalerweise ein externer Dienst verwendet, um die möglichen Kandidaten für die Verbindung mit 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 Utilitys“ für NAT und wird in der Regel in den meisten WebRTC-Anwendungen indirekt verwendet.

TURN (Traversal Using Relay NAT) ist eine leistungsfähigere Lösung, die die STUN-Protokolle umfasst und die meisten kommerziellen WebRTC-Dienste einen TURN-Server zum Herstellen von Verbindungen zwischen Peers verwenden. Die WebRTC API unterstützt sowohl STUN als auch TURN direkt und wird unter dem umfassenderen Begriff „Internet Connectivity Setup“ 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.

Rieselfilter

Sobald ein RTCPeerConnection-Objekt erstellt ist, nutzt das zugrunde liegende Framework die bereitgestellten ICE-Server, um Kandidaten für die Konnektivitätseinrichtung (ICE-Kandidaten) zu ermitteln. Das Ereignis icegatheringstatechange in RTCPeerConnection signalisiert, in welchem Zustand die ICE-Erfüllung lautet (new, gathering oder complete).

Es ist zwar möglich, dass ein Peer wartet, bis die ICE-Erfassung abgeschlossen ist, es ist jedoch in der Regel wesentlich effizienter, eine Technik vom Typ „Trickle ice“ zu verwenden und jeden ICE-Kandidaten an den Remote-Peer zu senden, sobald er gefunden wird. Dadurch wird die Einrichtungszeit der Peer-Konnektivität erheblich verringert und ein Videoanruf startet mit weniger Verzögerungen.

Um ICE-Kandidaten zu erfassen, fügen Sie einfach einen Listener für das icecandidate-Ereignis hinzu. Das Element RTCPeerConnectionIceEvent, das an diesen Listener ausgegeben wird, enthält die Property candidate und steht für einen neuen Kandidaten, der an den Remote-Peer gesendet werden soll (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 wir ICE-Kandidaten erhalten, sollten wir davon ausgehen, dass sich der Status unserer Peer-Verbindung irgendwann in einen verbundenen Status ändert. Um dies zu erkennen, fügen wir unserem RTCPeerConnection-Element 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!
    }
});

RTCPeerConnection API-Dokumentation