Guida introduttiva alle connessioni peer

Le connessioni peer sono la parte delle specifiche WebRTC che si occupa di collegare due applicazioni su computer diversi per comunicare utilizzando un protocollo peer-to-peer. La comunicazione tra peer può essere video, audio o dati binari arbitrari (per i client che supportano l'API RTCDataChannel). Per scoprire come possono connettersi due peer, entrambi i client devono fornire una configurazione del server ICE. Si tratta di un server STUN o TURN, il cui ruolo è fornire candidati ICE a ogni client, che vengono poi trasferiti al peer remoto. Questo trasferimento di candidati ICE è comunemente chiamato segnalazione.

Segnalazioni

La specifica WebRTC include API per la comunicazione con un server ICE (Internet Connectivity Establishment), ma il componente di segnalazione non ne fa parte. La segnalazione è necessaria affinché due peer condividano le modalità di connessione. In genere, questo problema viene risolto tramite un'API web basata su HTTP (ad es. un servizio REST o un altro meccanismo RPC) in cui le applicazioni web possono inoltrare le informazioni necessarie prima che venga avviata la connessione peer.

Il seguente snippet di codice mostra come può essere utilizzato questo servizio di segnalazione fittizio per inviare e ricevere messaggi in modo asincrono. Verrà utilizzato negli esempi rimanenti di questa guida, se necessario.

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

La segnalazione può essere implementata in molti modi diversi e la specifica WebRTC non preferisce alcuna soluzione specifica.

Avvio delle connessioni peer

Ogni connessione peer viene gestita da un oggetto RTCPeerConnection. Il costruttore per questa classe accetta un singolo oggetto RTCConfiguration come parametro. Questo oggetto definisce come viene configurata la connessione peer e deve contenere informazioni sui server ICE da utilizzare.

Una volta creato il RTCPeerConnection, dobbiamo creare un'offerta o una risposta SDP, a seconda che siamo il peer chiamante o ricevente. Una volta creata l'offerta o la risposta SDP, deve essere inviata al peer remoto tramite un canale diverso. Il passaggio di oggetti SDP a peer remoti è chiamato segnalazione e non è coperto dalla specifica WebRTC.

Per avviare la configurazione della connessione peer dal lato chiamante, creiamo un oggetto RTCPeerConnection e poi chiamiamo createOffer() per creare un oggetto RTCSessionDescription. Questa descrizione della sessione viene impostata come descrizione locale utilizzando setLocalDescription() e viene quindi inviata tramite il nostro canale di segnalazione al destinatario. Abbiamo anche configurato un listener per il nostro canale di segnalazione per quando viene ricevuta una risposta alla descrizione della sessione offerta dalla parte ricevente.

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

Sul lato ricevente, attendiamo un'offerta in arrivo prima di creare la nostra istanza RTCPeerConnection. Una volta fatto, impostiamo l'offerta ricevuta utilizzando setRemoteDescription(). Successivamente, chiamiamo createAnswer() per creare una risposta all'offerta ricevuta. Questa risposta viene impostata come descrizione locale utilizzando setLocalDescription() e poi inviata alla parte chiamante tramite il nostro server di segnalazione.

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

Una volta che i due peer hanno impostato le descrizioni delle sessioni locali e remote, conoscono le funzionalità del peer remoto. Ciò non significa che la connessione tra i peer sia pronta. Affinché funzioni, dobbiamo raccogliere i candidati ICE in ogni peer e trasferirli (tramite il canale di segnalazione) all'altro peer.

Candidati ICE

Prima che due peer possano comunicare utilizzando WebRTC, devono scambiarsi informazioni sulla connettività. Poiché le condizioni di rete possono variare a seconda di una serie di fattori, di solito viene utilizzato un servizio esterno per scoprire i possibili candidati per la connessione a un peer. Questo servizio si chiama ICE e utilizza un server STUN o TURN. STUN sta per Session Traversal Utilities for NAT e viene solitamente utilizzato indirettamente nella maggior parte delle applicazioni WebRTC.

TURN (Traversal Using Relay NAT) è la soluzione più avanzata che incorpora i protocolli STUN e la maggior parte dei servizi commerciali basati su WebRTC utilizza un server TURN per stabilire connessioni tra peer. L'API WebRTC supporta direttamente STUN e TURN ed è raggruppata nel termine più completo Internet Connectivity Establishment. Quando creiamo una connessione WebRTC, di solito forniamo uno o più server ICE nella configurazione dell'oggetto RTCPeerConnection.

Trickle ICE

Una volta creato un oggetto RTCPeerConnection, il framework sottostante utilizza i server ICE forniti per raccogliere i candidati per la creazione della connettività (candidati ICE). L'evento icegatheringstatechange del giorno RTCPeerConnection indica lo stato della raccolta ICE (new, gathering o complete).

Sebbene sia possibile che un peer attenda il completamento della raccolta ICE, in genere è molto più efficiente utilizzare una tecnica di "trickle ice" e trasmettere ogni candidato ICE al peer remoto man mano che viene scoperto. In questo modo si riduce notevolmente il tempo di configurazione della connettività peer e si consente di avviare una videochiamata con meno ritardi.

Per raccogliere i candidati ICE, aggiungi un listener per l'evento icecandidate. L'evento RTCPeerConnectionIceEvent emesso su questo listener conterrà la proprietà candidate che rappresenta un nuovo candidato da inviare al peer remoto (vedi 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);
        }
    }
});

Connessione stabilita

Una volta ricevuti i candidati ICE, lo stato della connessione peer alla fine cambierà in uno stato connesso. Per rilevarlo, aggiungiamo un listener al nostro RTCPeerConnection in cui ascoltiamo gli eventi connectionstatechange.

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

Documentazione dell'API RTCPeerConnection