Guida introduttiva alle connessioni peer

Le connessioni peer sono la parte delle specifiche WebRTC che si occupa del collegamento di due applicazioni su computer diversi per comunicare tramite un protocollo peer-to-peer. La comunicazione tra peer può essere basata su dati binari, audio o video arbitrari (per i client che supportano l'API RTCDataChannel). Per scoprire come due peer possono connettersi, entrambi i client devono fornire una configurazione server ICE. Può essere uno STUN o un server TURN e il loro ruolo consiste nel fornire i candidati ICE a ciascun client che viene poi trasferito al peer remoto. Questo trasferimento di candidati ICE è comunemente chiamato segnale.

Segnalazione

La specifica WebRTC include le API per la comunicazione con un server ICE (Internet Connectivity tendement,) ma il componente di segnalazione non ne fa parte. Perché due colleghi possano condividere le modalità di connessione, è necessaria la segnalazione. In genere, questo viene risolto tramite una normale API web basata su HTTP, ovvero 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 in che modo questo servizio di segnalazione fittizio può essere utilizzato per inviare e ricevere messaggi in modo asincrono. Se necessario, verrà utilizzato negli esempi rimanenti in questa guida.

// 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 una soluzione specifica.

Avvio di connessioni tra peer

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

Dopo aver creato l'elemento RTCPeerConnection, dobbiamo creare un'offerta o una risposta dell'SDP, a seconda che si tratti dell'app peer o del peer ricevente. Dopo aver creato l'offerta o la risposta SDP, questa deve essere inviata al peer remoto tramite un canale diverso. Il passaggio di oggetti SDP a peer remoti è detto segnale e non è coperto dalla specifica WebRTC.

Per avviare la configurazione della connessione peer dal lato chiamante, creiamo un oggetto RTCPeerConnection, quindi chiamiamo createOffer() per creare un oggetto RTCSessionDescription. La descrizione della sessione viene impostata come descrizione locale tramite setLocalDescription() e viene inviata al nostro canale di segnalazione al lato di destinazione. Inoltre, configuriamo un listener per il nostro canale di segnalazione per la ricezione di una risposta alla descrizione della sessione offerta.

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

Per quanto riguarda la ricezione, attendiamo un'offerta in entrata prima di creare la nostra istanza RTCPeerConnection. Una volta fatto, impostiamo l'offerta ricevuta utilizzando setRemoteDescription(). In seguito, chiamiamo createAnswer() per creare una risposta all'offerta ricevuta. Questa risposta viene impostata come descrizione locale utilizzando setLocalDescription() e inviata al lato 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});
    }
});

Dopo aver impostato le descrizioni sia della sessione locale che della sessione remota, i due peer conoscono le funzionalità del peer remoto. Ciò non significa che la connessione tra peer è pronta. Per far sì che questo funzioni, è necessario raccogliere i candidati ICE per ogni peer e trasferirli (tramite il canale di segnalazione) all'altro peer.

Candidati di ICE

Prima che due peer possano comunicare utilizzando WebRTC, devono scambiarsi informazioni sulla connettività. Poiché le condizioni di rete possono variare in base a una serie di fattori, un servizio esterno viene solitamente utilizzato per individuare i possibili candidati per la connessione a un peer. Questo servizio è chiamato ICE e utilizza un server STUN o TURN. STUN sta per Session Traversal Utility per NAT e di solito viene 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 e viene raccolta con il termine più completo di definizione della connettività Internet. Quando crei una connessione WebRTC, di solito forniamo uno o più server ICE nella configurazione per l'oggetto RTCPeerConnection.

Trickle ice

Una volta creato un oggetto RTCPeerConnection, il framework sottostante utilizza i server ICE in dotazione per raccogliere i candidati per la creazione della connettività (i candidati ICE). L'evento icegatheringstatechange sugli indicatori di RTCPeerConnection in stato stato della raccolta ICE (new, gathering o complete).

Anche se è possibile attendere che un compagno ICE attenda il completamento della raccolta ICE, in genere è molto più efficiente utilizzare una tecnica "Isolotto" e trasmettere ogni candidato ICE al peer remoto quando viene scoperto. In questo modo ridurrai notevolmente i tempi di configurazione della connettività peer e potrete iniziare una videochiamata con meno ritardi.

Per raccogliere i candidati ICE, è sufficiente aggiungere un listener per l'evento icecandidate. Il valore di RTCPeerConnectionIceEvent emesso per l'ascoltatore conterrà la proprietà candidate, che rappresenta un nuovo candidato che deve essere inviato al peer remoto (vedi Segnalazione).

// 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

Dopo aver ricevuto i candidati ICE, prevediamo che lo stato della connessione tra compagni diventerà uno stato connesso. Per rilevare questo evento, 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