Premiers pas avec la connexion entre pairs

Les connexions d'appairage font partie des spécifications WebRTC qui concernent la connexion de deux applications sur différents ordinateurs pour communiquer à l'aide d'un protocole peer-to-peer. La communication entre les pairs peut être une vidéo, audio ou binaire arbitraire (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, et leur rôle consiste à fournir des candidats ICE à chaque client, qui sont ensuite transférés au pair distant. Ce transfert de candidats ICE est communément appelé "signalement".

Serveur de signalement

La spécification WebRTC inclut des API permettant de communiquer avec un serveur ICE (Internet Connect Establishment), mais le composant de signalisation ne fait pas partie de ce protocole. Le signal est nécessaire pour que deux pairs puissent partager leur façon de se connecter. Ce problème est généralement résolu via une API Web standard basée sur HTTP (un service REST ou un autre mécanisme RPC, par exemple), qui permet aux applications Web de transmettre les informations nécessaires avant le démarrage de la connexion au pair.

L'extrait de code suivant montre comment ce service de signalement 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, si nécessaire.

// 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 signature peut être mise en œuvre de différentes manières, et la spécification WebRTC ne préfère aucune solution spécifique.

Lancer des connexions pair

Chaque connexion de 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 au pair et doit contenir des informations sur les serveurs ICE à utiliser.

Une fois RTCPeerConnection créé, nous devons créer une offre ou une réponse SDP, selon que nous sommes le pair appelant ou le pair 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 à distance est appelée signalisation et n'est pas couverte par la spécification WebRTC.

Pour initier la configuration de la connexion du côté pair à l'appel, nous créons un objet RTCPeerConnection, puis appelez createOffer() pour créer un objet RTCSessionDescription. Cette description de session est définie en tant que description locale à l'aide de setLocalDescription(), puis est envoyée via notre canal de signalisation au côté de la réception. Nous configurons également un écouteur sur notre canal de signalisation afin de recevoir une réponse à la description de session proposée du côté de la réception.

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é de la réception, nous attendons une offre entrante avant de créer notre instance RTCPeerConnection. Une fois cette opération effectuée, nous avons défini 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 en tant que description locale à l'aide de setLocalDescription(), puis est envoyée au côté de l'appel 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 locale et distante, 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 auprès du système ICE à chaque pair, puis le transférer (sur le canal de signalisation) à l'autre pair.

Candidats au système ICE

Pour que deux pairs puissent communiquer à l'aide de WebRTC, ils doivent échanger des informations de connectivité. Comme 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 potentiels pour la connexion à un pair. Ce service s'appelle ICE et utilise un serveur STUN ou TURN. STUN signifie Session Traversal Utility pour NAT et 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 incorpore les protocoles STUN et la plupart des services basés sur WebRTC utilisent un serveur TURN pour établir des connexions entre pairs. L'API WebRTC est compatible à la fois avec STUN et TURN, et est collectée sous le statut d'établissement de connectivité Internet plus complet. Lors de la création d'une connexion WebRTC, nous fournissons généralement un ou plusieurs serveurs ICE dans la configuration de l'objet RTCPeerConnection.

ICE Trickle

Une fois qu'un objet RTCPeerConnection a été 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 réunion ICE (new, gathering ou complete).

Bien qu'il soit possible pour un pair d'attendre la fin du recueil ICE, il est généralement bien plus efficace d'utiliser une technique de type "glaçage" en glace et de transmettre chaque candidat ICE au pair distant dès qu'il est découvert. Cela permet de réduire considérablement le temps de configuration de la connectivité au pair et de démarrer les appels vidéo plus rapidement.

Pour rassembler des candidats ICE, ajoutez simplement un écouteur pour l'événement icecandidate. Le RTCPeerConnectionIceEvent émis sur cet écouteur contient une propriété candidate qui représente un nouveau candidat à envoyer au pair distant (voir la section "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);
        }
    }
});

Connexion établie

Une fois les candidats reçus, ils devraient être associés à un état connecté. Pour détecter cela, nous ajoutons un écouteur à notre RTCPeerConnection, où nous écouteons 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