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

Połączenia peer-to-peer to część specyfikacji WebRTC, która dotyczy łączenia 2 aplikacji na różnych komputerach w celu komunikowania się za pomocą protokołu peer-to-peer. Komunikacja między urządzeniami może odbywać się za pomocą wideo, dźwięku lub dowolnych danych binarnych (w przypadku klientów obsługujących interfejs RTCDataChannel API). Aby dowiedzieć się, jak dwa urządzenia mogą się połączyć, oba klienty muszą podać konfigurację serwera ICE. Jest to serwer STUN lub TURN, którego zadaniem jest dostarczanie kandydatów ICE do każdego klienta, a następnie przekazywanie ich do zdalnego klienta. Przesyłanie 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 komponent sygnalizacyjny nie jest jej częścią. Sygnalizacja jest potrzebna, aby 2 użytkowników mogło udostępnić informacje o tym, jak powinni się połączyć. Zazwyczaj rozwiązuje się to za pomocą zwykłego interfejsu Web API 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 równorzędnego.

Poniższy fragment kodu pokazuje, jak można używać tej fikcyjnej usługi sygnalizacyjnej do asynchronicznego wysyłania i odbierania wiadomości. W razie 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!');

Sygnalizację można wdrożyć 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 peer-to-peer jest obsługiwane przez obiekt RTCPeerConnection. Konstruktor tej klasy przyjmuje jako parametr pojedynczy obiekt RTCConfiguration. Ten obiekt określa sposób konfiguracji połączenia równorzędnego i powinien zawierać informacje o serwerach ICE, które mają być używane.

Po utworzeniu RTCPeerConnection musimy utworzyć ofertę lub odpowiedź SDP w zależności od tego, czy jesteśmy dzwoniącym, czy odbierającym połączenie. Po utworzeniu oferty lub odpowiedzi SDP należy wysłać ją do zdalnego urządzenia za pomocą innego kanału. Przekazywanie obiektów SDP do zdalnych połączeń równorzędnych nazywa się sygnalizacją i nie jest objęte specyfikacją WebRTC.

Aby zainicjować konfigurację połączenia peer-to-peer po stronie dzwoniącej, tworzymy obiekt RTCPeerConnection, a następnie wywołujemy createOffer(), aby utworzyć obiekt RTCSessionDescription. Opis sesji jest ustawiany jako opis lokalny za pomocą setLocalDescription(), a następnie wysyłany przez nasz kanał sygnalizacyjny do odbiorcy. Ustawiamy też odbiornik na naszym kanale sygnalizacyjnym, aby otrzymywać odpowiedzi na oferowany opis sesji z drugiej strony.

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. Gdy to zrobimy, ustawimy otrzymaną ofertę za pomocą funkcji setRemoteDescription(). Następnie wywołujemy funkcję createAnswer(), aby utworzyć odpowiedź na otrzymaną ofertę. Odpowiedź jest ustawiana jako opis lokalny za pomocą setLocalDescription(), a następnie wysyłana do strony dzwoniącej za pośrednictwem naszego serwera sygnalizacyjnego.

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 oba urządzenia równorzędne ustawią opisy sesji lokalnej i zdalnej, będą znać możliwości urządzenia zdalnego. Nie oznacza to, że połączenie między urządzeniami jest gotowe. Aby to działało, musimy zebrać kandydatów ICE na każdym urządzeniu i przesłać ich (przez kanał sygnalizacyjny) na drugie urządzenie.

Kandydaci ICE

Zanim 2 urządzenia będą mogły komunikować się za pomocą WebRTC, muszą wymienić informacje o połączeniu. Warunki sieciowe mogą się różnić w zależności od wielu czynników, dlatego do odkrywania 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. Jest on zwykle używany pośrednio w większości aplikacji WebRTC.

TURN (Traversal Using Relay NAT) to bardziej zaawansowane rozwiązanie, które obejmuje protokoły STUN. Większość komercyjnych usług opartych na WebRTC używa serwera TURN do nawiązywania połączeń między urządzeniami. Interfejs WebRTC API obsługuje bezpośrednio protokoły STUN i TURN, które są objęte bardziej ogólnym terminem nawiązywania połączenia z internetem. Podczas tworzenia połączenia WebRTC zwykle podajemy w konfiguracji obiektu RTCPeerConnection jeden lub kilka serwerów ICE.

Trickle ICE

Po utworzeniu obiektu RTCPeerConnection podstawowa platforma używa podanych serwerów ICE do zbierania kandydatów do nawiązania połączenia (kandydatów ICE). Zdarzenie icegatheringstatechangeRTCPeerConnection sygnalizuje stan zbierania kandydatów ICE (new, gathering lub complete).

Chociaż węzeł może poczekać na zakończenie zbierania kandydatów ICE, zwykle znacznie wydajniejsze jest użycie techniki „trickle ice” i przesyłanie każdego kandydata ICE do zdalnego węzła w miarę jego wykrywania. Znacznie skróci to czas konfiguracji połączenia peer-to-peer i pozwoli rozpocząć rozmowę wideo z mniejszym opóźnieniem.

Aby zebrać kandydatów ICE, wystarczy dodać detektor zdarzeń dla zdarzenia icecandidate. Wydarzenie RTCPeerConnectionIceEvent wyemitowane w tym odbiorniku będzie zawierać właściwość candidate, która reprezentuje nowego kandydata, którego należy wysłać do zdalnego węzła (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 kandydaci ICE zostaną odebrani, stan połączenia z naszym węzłem powinien w końcu zmienić się na stan połączenia. Aby to wykryć, dodajemy do naszego RTCPeerConnection detektor, który nasłuchuje zdarzeń connectionstatechange.

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

Dokumentacja interfejsu RTCPeerConnection API