Premiers pas avec la connexion entre pairs

Les connexions entre pairs constituent la partie des spécifications WebRTC qui permet de connecter deux applications sur différents ordinateurs pour qu'elles communiquent à l'aide d'un protocole peer-to-peer. La communication entre les 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 permettant de 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 puissent partager la manière dont ils doivent se connecter. En général, cela est résolu à l'aide d'une API Web basée sur 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 entre pairs.

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. Il sera utilisé dans les autres exemples 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 favorise aucune solution spécifique.

Lancer des connexions d'appairage

Chaque connexion entre pairs 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 entre pairs 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'appelant ou le destinataire. 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 entre pairs du côté de l'appelant, nous créons un objet RTCPeerConnection, puis 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 via notre canal de signalisation à l'autre extrémité. Nous avons également configuré un écouteur pour notre canal de signalisation lorsque la partie réceptrice reçoit une réponse à la description de session proposée.

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 cette opération effectuée, 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 fonctionnalité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 à chaque paire et les transférer (via le canal de signalisation) à l'autre paire.

Candidats à l'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 d'un certain nombre de facteurs, un service externe est généralement utilisé pour découvrir les candidats possibles à la connexion à un pair. Ce service est appelé ICE et utilise un serveur STUN ou TURN. STUN signifie "Session Traversal Utilities for NAT" (Utilitaires de traversée de session pour NAT). Il est généralement utilisé indirectement dans la plupart des applications WebRTC.

TURN (Traversal Using Relay NAT) est la solution la 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". Lorsque vous créez une connexion WebRTC, vous fournissez généralement un ou plusieurs serveurs ICE dans la configuration de l'objet RTCPeerConnection.

ICE en goutte

Une fois un objet RTCPeerConnection créé, le framework sous-jacent utilise les serveurs ICE fournis pour rassembler 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'un pair puisse attendre que la collecte ICE soit terminée, il est généralement beaucoup plus efficace d'utiliser une technique de "goutte à goutte ICE" et de transmettre chaque candidat ICE au pair distant à mesure qu'il est découvert. Cela réduit considérablement le temps de configuration de la connectivité entre pairs et permet de démarrer un appel vidéo avec moins de retards.

Pour collecter des candidats ICE, ajoutez simplement un écouteur pour l'événement icecandidate. Le RTCPeerConnectionIceEvent émis sur cet écouteur contiendra la propriété candidate qui représente un nouveau candidat à envoyer au pair distant (voir "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 que les candidats ICE sont reçus, nous nous attendons à ce que l'état de notre connexion peer passe à un é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