Premiers pas avec la connexion entre pairs

Les connexions peer-to-peer font partie des spécifications WebRTC qui traitent de la connexion de deux applications sur des ordinateurs différents pour communiquer à l'aide d'un protocole peer-to-peer. La communication entre pairs peut être vidéo, audio ou des données binaires arbitraires (pour les clients compatibles avec l'API RTCDataChannel). Pour découvrir comment deux pairs peuvent se connecter, les deux clients doivent fournir une configuration de serveur ICE. Il s'agit d'un serveur STUN ou TURN, dont le rôle est de fournir des candidats ICE à chaque client, qui sont ensuite transférés au pair distant. Ce transfert de candidats ICE est communément appelé signalisation.

Serveur de signalement

La spécification WebRTC inclut des API pour communiquer avec un serveur ICE (Internet Connectivity Establishment), mais le composant de signalisation n'en fait pas partie. La signalisation est nécessaire pour que deux pairs partagent la façon dont ils doivent se connecter. En général, ce problème est résolu à l'aide d'une API Web HTTP standard (c'est-à-dire un service REST ou un autre mécanisme RPC) où les applications Web peuvent relayer les informations nécessaires avant l'établissement de la connexion peer-to-peer.

L'extrait de code suivant montre comment ce service de signalisation fictif peut être utilisé pour envoyer et recevoir des messages de manière asynchrone. Elle sera utilisée dans les exemples restants de ce guide, le cas échéant.

// 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 signalisation peut être implémentée de différentes manières, et la spécification WebRTC ne privilégie aucune solution spécifique.

Lancer des connexions peer-to-peer

Chaque connexion peer-to-peer est gérée par un objet RTCPeerConnection. Le constructeur de cette classe utilise un seul objet RTCConfiguration comme paramètre. Cet objet définit la configuration de la connexion pair à pair et doit contenir des informations sur les serveurs ICE à utiliser.

Une fois le RTCPeerConnection créé, nous devons créer une offre ou une réponse SDP, selon que nous sommes l'homologue appelant ou l'homologue recevant. Une fois l'offre ou la réponse SDP créée, elle doit être envoyée au pair distant via un autre canal. La transmission d'objets SDP à des pairs distants est appelée signalisation et n'est pas couverte par la spécification WebRTC.

Pour lancer la configuration de la connexion peer-to-peer depuis l'extrémité appelante, nous créons un objet RTCPeerConnection, puis nous appelons createOffer() pour créer un objet RTCSessionDescription. Cette description de session est définie comme description locale à l'aide de setLocalDescription(), puis envoyée au destinataire via notre canal de signalisation. Nous configurons également un écouteur sur notre canal de signalisation pour savoir quand une réponse à la description de session proposée est reçue du côté du destinataire.

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

Du côté du destinataire, nous attendons une offre entrante avant de créer notre instance RTCPeerConnection. Une fois cela fait, nous définissons l'offre reçue à l'aide de setRemoteDescription(). Ensuite, nous appelons createAnswer() pour créer une réponse à l'offre reçue. Cette réponse est définie comme description locale à l'aide de setLocalDescription(), puis envoyée au côté appelant via notre serveur de signalisation.

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

Une fois que les deux pairs ont défini les descriptions de session locales et distantes, ils connaissent les capacités du pair distant. Cela ne signifie pas que la connexion entre les pairs est prête. Pour que cela fonctionne, nous devons collecter les candidats ICE au niveau de chaque pair et les transférer (via le canal de signalisation) à l'autre pair.

Candidats ICE

Avant que deux pairs puissent communiquer à l'aide de WebRTC, ils doivent échanger des informations de connectivité. Étant donné que les conditions du réseau peuvent varier en fonction de plusieurs facteurs, un service externe est généralement utilisé pour découvrir les candidats possibles pour la connexion à un pair. Ce service est appelé ICE et utilise un serveur STUN ou TURN. STUN (Session Traversal Utilities for NAT) est généralement utilisé indirectement dans la plupart des applications WebRTC.

TURN (Traversal Using Relay NAT) est une solution plus avancée qui intègre les protocoles STUN. La plupart des services commerciaux basés sur WebRTC utilisent un serveur TURN pour établir des connexions entre pairs. L'API WebRTC est compatible avec STUN et TURN directement, et est regroupée sous le terme plus complet "Internet Connectivity Establishment" (Établissement de la connectivité Internet). Lorsque nous créons une connexion WebRTC, nous fournissons généralement un ou plusieurs serveurs ICE dans la configuration de l'objet RTCPeerConnection.

Trickle ICE

Une fois l'objet RTCPeerConnection créé, le framework sous-jacent utilise les serveurs ICE fournis pour collecter les candidats à l'établissement de la connectivité (candidats ICE). L'événement icegatheringstatechange sur RTCPeerConnection indique l'état de la collecte ICE (new, gathering ou complete).

Bien qu'il soit possible pour un pair d'attendre la fin de la collecte ICE, il est généralement beaucoup plus efficace d'utiliser une technique de "trickle ICE" et de transmettre chaque candidat ICE au pair distant au fur et à mesure de sa découverte. Cela réduira considérablement le temps de configuration de la connectivité peer-to-peer et permettra de démarrer un appel vidéo avec moins de retard.

Pour collecter des candidats ICE, il vous suffit d'ajouter un écouteur pour l'événement icecandidate. L'RTCPeerConnectionIceEvent émis sur cet écouteur contiendra la propriété candidate qui représente un nouveau candidat à envoyer au pair distant (voir la signalisation).

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

Connexion établie

Une fois les candidats ICE reçus, l'état de notre connexion homologue devrait finir par passer à l'état "connecté". Pour détecter cela, nous ajoutons un écouteur à notre RTCPeerConnection où nous écoutons les événements connectionstatechange.

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

Documentation de l'API RTCPeerConnection