Pierwsze kroki z połączeniami równorzędnymi

Połączenia równorzędne to część specyfikacji WebRTC, która dotyczy łączenia 2 aplikacji na różnych komputerach w celu komunikacji za pomocą protokołu peer-to-peer. Komunikacja między punktami końcowymi może polegać na przesyłaniu filmów, dźwięku lub dowolnych danych binarnych (w przypadku klientów obsługujących interfejs API RTCDataChannel). Aby dowiedzieć się, jak dwa urządzenia mogą się połączyć, oba muszą podać konfigurację serwera ICE. Może to być serwer STUN lub TURN, którego zadaniem jest dostarczanie kandydatów ICE do każdego klienta, który następnie przekazuje je do odległego peera. Przenoszenie kandydatów ICE jest powszechnie nazywane sygnalizacją.

Wysyłanie sygnałów

Specyfikacja WebRTC zawiera interfejsy API do komunikacji z serwerem ICE (Internet Connectivity Establishment), ale nie obejmuje komponentu sygnalizacji. Sygnalizacja jest potrzebna, aby dwa urządzenia mogły się połączyć. Zwykle jest to realizowane za pomocą zwykłego interfejsu API internetowego opartego na protokole HTTP (np. usługi REST lub innego mechanizmu RPC), w którym aplikacje internetowe mogą przekazywać niezbędne informacje przed zainicjowaniem połączenia peer-to-peer.

Poniższy fragment kodu pokazuje, jak za pomocą tej fikcyjnej usługi sygnalizacji można wysyłać i odbierać wiadomości asynchronicznie. W przypadku potrzeby będzie on używany w pozostałych przykładach w tym przewodniku.

// 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!');

Sygnalizacja może być implementowana na wiele różnych sposobów, a specyfikacja WebRTC nie preferuje żadnego konkretnego rozwiązania.

Inicjowanie połączeń równorzędnych

Każde połączenie z innym węzłem jest obsługiwane przez obiekt RTCPeerConnection. Konstruktor tej klasy przyjmuje jako parametr obiekt RTCConfiguration. Obiekt ten określa sposób konfiguracji połączenia peer-to-peer i powinien zawierać informacje o serwerach ICE do użycia.

Po utworzeniu RTCPeerConnection musimy utworzyć ofertę SDP lub odpowiedź, w zależności od tego, czy jesteśmy dzwoniącym czy odbierającym. Po utworzeniu oferty lub odpowiedzi SDP należy ją wysłać do odległego peera przez inny kanał. Przekazywanie obiektów SDP do urządzeń zdalnych nazywa się sygnalizacją i nie jest objęte specyfikacją WebRTC.

Aby zainicjować konfigurację połączenia peer-to-peer po stronie wywołującej, tworzymy obiekt RTCPeerConnection, a następnie wywołujemy createOffer(), aby utworzyć obiekt RTCSessionDescription. Ten opis sesji jest ustawiany jako lokalny opis za pomocą setLocalDescription(), a następnie wysyłany przez nasz kanał sygnalizacji do strony odbierającej. Skonfigurowaliśmy też odsłuchiwanie naszego kanału sygnalizacyjnego, aby otrzymywać od strony odbierającej odpowiedź na oferowany opis sesji.

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});
}

Po stronie odbiorcy czekamy na ofertę przychodzącą, zanim utworzymy instancję RTCPeerConnection. Następnie ustawiamy otrzymaną ofertę za pomocą funkcji setRemoteDescription(). Następnie wywołujemy funkcję createAnswer(), aby utworzyć odpowiedź na otrzymaną ofertę. Ta odpowiedź jest ustawiana jako lokalny opis za pomocą funkcji setLocalDescription(), a następnie wysyłana do strony dzwoniącej przez nasz serwer sygnalizacji.

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});
    }
});

Gdy obie strony określą opisy sesji lokalnej i zdalnej, będą znać możliwości urządzenia zdalnego. Nie oznacza to, że połączenie między peerami jest gotowe. Aby to zadziałało, musimy zebrać kandydatów ICE na każdym z peerów i przekazać (przez kanał sygnalizacyjny) do drugiego peera.

Kandydaci ICE

Zanim 2 urządzenia mogą komunikować się za pomocą WebRTC, muszą wymienić się informacjami o połączeniu. Warunki w sieci mogą się zmieniać w zależności od wielu czynników, dlatego do wykrywania możliwych kandydatów do połączenia z peerem zwykle używa się usługi zewnętrznej. Ta usługa nazywa się ICE i korzysta z serwera STUN lub TURN. STUN to skrót od Session Traversal Utilities for NAT i zwykle jest używany pośrednio w większości aplikacji WebRTC.

TURN (Traversal Using Relay NAT) to bardziej zaawansowane rozwiązanie, które wykorzystuje protokoły STUN. Większość komercyjnych usług opartych na WebRTC korzysta z serwera TURN do nawiązywania połączeń między peerami. Interfejs WebRTC API obsługuje bezpośrednio protokoły STUN i TURN, a także bardziej kompleksowy protokół Internet Connectivity Establishment. Podczas tworzenia połączenia WebRTC zwykle podajemy 1 lub kilka serwerów ICE w konfiguracji obiektu RTCPeerConnection.

Struga ICE

Po utworzeniu obiektu RTCPeerConnection framework korzysta z dostawionych serwerów ICE do zbierania kandydatów na potrzeby nawiązywania połączeń (kandydaci ICE). Zdarzenie icegatheringstatechange na RTCPeerConnection sygnalizuje, w jakim stanie jest zbieranie danych przez ICE (new, gathering lub complete).

Chociaż jest możliwe, że peer zaczeka, aż zakończy się zbieranie informacji ICE, zazwyczaj znacznie wydajniej jest użyć techniki „trickle ice” i przesłać każdego kandydata ICE do odległego peera, gdy zostanie on wykryty. Dzięki temu czas konfiguracji połączenia z innym urządzeniem będzie znacznie krótszy, a rozmowa wideo rozpocznie się z mniejszym opóźnieniem.

Aby zebrać kandydatów ICE, po prostu dodaj słuchacza do zdarzenia icecandidate. Wyemitowany na tym odbiorcy RTCPeerConnectionIceEvent będzie zawierać właściwość candidate, która reprezentuje nowego kandydata, który powinien zostać wysłany do odległego peera (patrz sygnalizacja).

// 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);
        }
    }
});

Połączenie zostało nawiązane

Gdy otrzymamy kandydatów ICE, stan połączenia peer-to-peer powinien się zmienić na „Połączono”. Aby wykryć takie zdarzenie, dodajemy do RTCPeerConnection listenera, który nasłuchuje zdarzeń connectionstatechange.

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

RTCPeerConnection API: dokumentacja