Le WebRTC est un framework qui permet la mise en place d'un système de communication instantané directement dans le navigateur. Il apporte différents composants au sein du navigateur qui permet à chacun d'implémenter sa propre application RTC :

  • MediaStream avec getUserMedia permet l'accès aux périphériques audios et vidéos de l'utilisateur
  • PeerConnection permet d'implémenter la couche réseau et d'assurer la communication réseau entre le navigateur et la cible distante
  • DataChannel permet la mise en place d'un canal réseau qui peut transférer de manière bi-directionnelle des données arbitraires.

Dans la théorie le WebRTC permet donc la mise en place d'un système de communication instantané entre 2 navigateurs en les connectant directement l'un à l'autre directement (peer-to-peer) et en permettant l'échange de données et de flux audio/vidéo.

Comment ça marche ?

Comment se déroule la mise en place d'une communication WebRTC entre 2 utilisateurs A et B ?

  • A et B instancie une RTCPeerConnection
  • A et B obtiennent respectivement leurs offres (au format sdp, Session Description Protocol) et doivent se l'échanger (signaling). L'échange peut se faire par n'importe quel moyen (websocket, ajax, email…)
    • A envoie son offre à B
    • B envoie son offre à A
  • Lorsqu'une offre est reçue elle est ajoutée à l'instance de RTCPeerConnection via la méthode setRemoteDescription

  • A et B active la webcam et/ou le micro via la méthode getUserMedia puis attache le stream à la PeerConnection

  • Le streams reçus sont ajouté en src sur une balise <video>, A et B sont maintenant en mesure de communiquer l'un avec l'autre.

Voici ce que ça donne avec du code (j'ai omis les préfixes des navigateurs pour une meilleure compréhension).

// Connection permettant de communiquer l'offre avec l'autre utilisateur (websocket par exemple)
let signalingChannel = new SignalingChannel()
let pc // RTCPeerConnection

// Appelé lors de la réception d'une SessionDescription
let offerCreated = function (desc) {
  // On l'enregistre sur le point local
  pc.setLocalDescription(desc, function () {
    // On l'envoie l'offre à l'autre utilisateur
    signalingChannel.send(JSON.stringify({
      sdp: pc.localDescription
    }));
  }, logError)
}

// Permet de démarrer une conversation audio / vidéo
let startTalk = function () {
  pc = new RTCPeerConnection({
    iceServers: [{
      url: 'stun:stun.example.org'
    }]
  })

  // Lorsque l'on reçoit une nouvelle "route" possible on l'envois à l'autre utilisateur
  pc.onicecandidate = function (evt) {
    if (evt.candidate)
      signalingChannel.send(JSON.stringify({
        candidate: evt.candidate
      }))
  }

  // Permet de capturer la génération de "l'offre"
  pc.onnegotiationneeded = function () {
    pc.createOffer(offerCreated, logError) // On crée l'offre
  }

  // Quand on reçoit un flux vidéo on l'injecte dans notre <video>
  pc.onaddstream = function (e) {
    $video.src = URL.createObjectURL(e.stream)
  }

  // On utilise l'api media pour obtenir la vidéo / son de l'utilisateur
  navigator.getUserMedia({
    audio: true,
    video: true
  }, function (stream) {
    maVideo.src = URL.createObjectURL(stream);
    pc.addStream(stream);
  }, logError)
}

// Quand on reçoit un message de l'autre utilisateur
signalingChannel.onmessage = function (evt) {
  if (!pc) {
    startTalk()
  }
  let message = JSON.parse(evt.data)
  if (message.sdp)
    pc.setRemoteDescription(new RTCSessionDescription(message.sdp), function () {
      if (pc.remoteDescription.type == 'offer')
        pc.createAnswer(offerCreated, logError)
    }, logError)
  else
    pc.addIceCandidate(new RTCIceCandidate(message.candidate))
}

// L'auteur de l'appel appelera la méthode startTalk() dès le démarrage

Dans la théorie ce système semble plutôt simple mais dans la pratique l'architecture des réseaux complique pas mal les choses.

Traverser les NAT / Parefeu

Crédits html5rocks.com

Dans la pluspart des cas, nos machines ne sont pas directement connectées à internet mais se trouvent derrière plusieurs couches :

  • un NAT permet de router le traffic de plusieurs machines à travers une seule ip externe (l'ip externe obtenue sera donc celle du routeur et non pas de la machine)
  • un parefeu, qui bloque certains protocoles et certains ports
  • un proxy, qui masque l'adresse originale de la machine

Pour contourner toutes ces problématiques le WebRTC permet d'utiliser la technique Interactive Connectivity Establishment (ICE). Cette technique permet de trouver le chemin le plus direct possible pour faire communiquer 2 machines sur le réseau et de contourner les problématiques cités plus haut.

  • Un serveur STUN permet d'obtenir l'adresse externe d'un utilisateur.
  • Un server TURN permet de relayer le traffic si une connexion directe n'est pas possible (un serveur TURN est un serveur STUN avec cette fonction de relai).

Il est possible de spécifier les serveurs TURN et STUN à utiliser en utilisant la configuration iceServers lors de l'instanciation d'une RTCPeerConnection.

{
  'iceServers': [
    {
      'url': 'stun:stun.l.google.com:19302'
    },
    {
      'url': 'turn:192.158.29.39:3478?transport=udp',
      'credential': 'JZEOEt2V3Qb0y27GRntt2u2PAYA=',
      'username': '28224511:1379330808'
    },
    {
      'url': 'turn:192.158.29.39:3478?transport=tcp',
      'credential': 'JZEOEt2V3Qb0y27GRntt2u2PAYA=',
      'username': '28224511:1379330808'
    }
  ]
}

Il n'y a rien de plus à préciser... c'est le navigateur (et par définition RTCPeerConnection) qui se charge d'utiliser l'ICE afin d'obtenir le meilleur chemin entre les 2 machines. C'est ce que l'on retrouve avec l'évènement onicecandidate.

Crédits html5rocks.com

Au final notre architecture "peer-to-peer" ressemblera plutôt à ça dans le pire des cas.