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 i peer può essere video, audio o dati binari arbitrari (per i client che supportano l'API RTCDataChannel). Per scoprire in che modo due peer possono connettersi, entrambi i client devono fornire una configurazione del server ICE. Si tratta di un server STUN o TURN e il suo ruolo è fornire candidati ICE a ciascun 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 fa parte di essa. L'indicatore è necessario per consentire a due peer di condividere la modalità di connessione. In genere, il problema viene risolto tramite una normale 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 dell'avvio della connessione peer.

Il seguente snippet di codice mostra come questo servizio di segnalazione fittizio può essere utilizzato 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!');

L'indicatore può essere implementato in molti modi diversi e la specifica WebRTC non preferisce alcuna soluzione specifica.

Avvio di connessioni peer

Ogni connessione peer viene gestita da un oggetto RTCPeerConnection. Il costruttore per questa classe prende un singolo oggetto RTCConfiguration come parametro. Questo oggetto definisce la configurazione della connessione tra pari 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 il peer destinatario. Una volta creata, l'offerta o la risposta SDP deve essere inviata al peer remoto tramite un canale diverso. Il passaggio di oggetti SDP ai peer remoti è chiamato signaling e non è coperto dalla specifica WebRTC.

Per avviare la configurazione della connessione tra pari 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 inviata tramite il nostro canale di segnalazione al lato di ricezione. Abbiamo anche configurato un ascoltatore per il nostro canale di segnalazione per quando viene ricevuta una risposta alla descrizione della sessione offerta dal lato di ricezione.

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 di ricezione, attendiamo un'offerta in arrivo prima di creare la nostra istanza RTCPeerConnection. Al termine, 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 al 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 locale e remota, conoscono le funzionalità del peer remoto. Ciò non significa che la connessione tra i peer sia pronta. Affinché ciò funzioni, dobbiamo raccogliere i candidati ICE su ciascun 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 diversi fattori, in genere viene utilizzato un servizio esterno per rilevare i possibili candidati per la connessione a un peer. Questo servizio si chiama ICE e utilizza un server STUN o TURN. STUN è l'acronimo di 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 sia STUN che TURN direttamente e viene raccolta sotto il termine più completo Internet Connectivity Establishment. Quando crei una connessione WebRTC, di solito fornisci uno o più server ICE nella configurazione per l'oggetto RTCPeerConnection.

Trickle ICE

Una volta creato un oggetto RTCPeerConnection, il framework di base utilizza i server ICE forniti per raccogliere candidati per l'instaurazione della connettività (candidati ICE). L'evento icegatheringstatechange su RTCPeerConnection indica in quale stato si trova la raccolta ICE (new, gathering o complete).

Sebbene sia possibile per un peer attendere il completamento della raccolta di ICE, solitamente è molto più efficiente utilizzare una tecnica "trickle ice" e trasmettere ogni candidato ICE al peer remoto man mano che viene rilevato. In questo modo, il tempo di configurazione della connettività tra pari verrà ridotto notevolmente e potrai iniziare una chiamata video con meno ritardi.

Per raccogliere i candidati ICE, aggiungi semplicemente un listener per l'evento icecandidate. L'evento RTCPeerConnectionIceEvent emesso su questo ascoltatore conterrà la proprietà candidate che rappresenta un nuovo candidato da inviare al peer remoto (vedi Segnaletica).

// 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, ci aspettiamo che lo stato della connessione peer alla fine diventi Connesso. Per rilevarlo, aggiungiamo un ascoltatore 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