Getting Started with WebRTC

HTML5 Rocks
WebRTC est le nouveau front dans la longue guerre pour un web ouvert et décongestionné. Brendan Eich, l'inventeur du JavaScript

Communication en temps réel sans plugins

Imaginez un monde où votre téléphone, votre télé et votre ordinateur pourraient tous communiquer sur une plateforme commune. Imaginez du chat vidéo et de l'échange de données en peer-to-peer ajoutés en toute simplicité à votre application web. Telle est la vision de WebRTC.

Vous voulez essayer ? WebRTC est disponible dès maintenant dans Google Chrome, Opera et Firefox. Une bonne manière de commencer est de lancer cette application minimaliste de chat vidéo sur apprtc.appspot.com:

  1. Ouvrez apprtc.appspot.com dans Chrome, Opera ou Firefox.
  2. Cliquez sur le bouton "Autoriser" pour laisser l'application utiliser votre webcam.
  3. Puis ouvrez l'URL affichée en bas de la page dans un nouvel onglet ou, encore mieux, sur un autre ordinateur.

Il y a une version du code commenté de cette application un peu plus loin dans cet article.

Prise en main

Vous n'avez pas le temps de lire cet article ou vous voulez juste le code?

  1. Suivez une présentation de WebRTC lors de la conférence Google I/O (les supports sont là) :

  2. Si vous n'avez jamais utilisé getUserMedia, jetez un œil à l'article sur HTML5 Rocks sur le sujet et parcourez la source d'un exemple simplifié sur simpl.info/gum.
  3. Attaquez-vous à l'API de RTCPeerConnection en parcourant le petit exemple ci-dessous et la démo sur simpl.info/pc, qui implémente WebRTC sur une seule page web.
  4. Approfondissez vos connaissances sur la manière dont WebRTC utilise des serveurs pour la signalisation, le passage des firewalls et des NAT en lisant le code et les console logs de apprtc.appspot.com.

À défaut, plongez-vous directement dans notre codelab WebRTC : un guide qui explique pas à pas comment construire une application de chat vidéo en utilisant un simple serveur de signalisation.

La petite histoire de WebRTC

Un des derniers grands défis pour le web est de permettre la communication des personnes par la voix et la vidéo : la communication en temps réel, ou Real Time Communication abrégée RTC. Dans une application web, la communication en temps réel devrait être aussi naturelle que de saisir des mots dans un champ texte. Sans elle, nous sommes limités dans notre capacité à innover et développer de nouveaux médiums d'interaction.

Historiquement, faire de la communication en temps réel était complexe et nécessitait des technologies propriétaires et coûteuses ou développées en silos. Du coup, intégrer des technologies RTC avec du contenu préexistant c'était assez galère, spécialement sur le web.

Le chat vidéo de Gmail est devenu populaire en 2008 et en 2011 Google a introduit Hangouts qui utilisait le service Google Talk, comme dans Gmail. Google a racheté GIPS, une entreprise qui avait développé de nombreux composants logiciels requis pour la création de RTC comme des codecs et des dispositifs d'annulation d'écho. Google a rendues open source les technologies développées par GIPS et s'est engagé dans la standardisation d'une norme auprès de l'IETF et au W3C pour s'assurer d'un consensus. En mai 2011, Ericsson a développé la première implémentation de WebRTC.

WebRTC a maintenant des standards ouverts et implémentés pour des dispositifs de communication audio, vidéo et data en temps réel et sans plugin. Il y a un vrai besoin :

  • De nombreux web services utilisent déjà ce genre de technologie mais nécessitent de télécharger une appli ou un plugin. C'est le cas de Skype, Facebook (qui utilise Skype) et Google Hangouts (qui utilise le plugin Google Talk).
  • Télécharger, installer et mettre à jour ses plugins peut être compliqué, générateur d'erreurs et ennyeux.
  • Les plugins peuvent être difficiles à déployer, débuger, corriger, tester et maintenir — et nécessitent souvent un jeu de licenses compliqué et une intégration avec un assortiment de technologies complexes et coûteuses. Et surtout, c'est bien difficile de convaincre les utilisateurs d'installer un plugin sur leur machine !

Le fil rouge du projet WebRTC est que ses APIs doivent être open-source, libres, standardisées, inclues dans les navigateur et plus efficaces que l'état de l'art.

Où en sommes-nous maintenant ?

WebRTC implémente trois APIs:

getUserMedia est disponible dans Chrome, Opera et Firefox. Allez jeter un œil sur les démos cross-browser sur simpl.info/gum et les super exemples de Chris Wilson qui utilisent getUserMedia comme source pour du traitement avec Web Audio.

RTCPeerConnection est disponible dans Chrome (sur ordinateur et sur Android), Opera (sur ordinateur et sur les dernières versions Beta sur Android) et dans Firefox. Un petit point sur le nom : après plusieurs itérations, RTCPeerConnection est actuellement intégré dans Chrome et Opera en tant que webkitRTCPeerConnection et dans Firefox en tant que mozRTCPeerConnection. D'autres noms et implémentations ont été dépréciés. Quand les standards se seront stabilisés, les préfixes seront supprimés. Il y a une démo super simple de l'implémentation de RTCPeerConnection dans Chromium sur simpl.info/pc et une chouette application vidéo sur apprtc.appspot.com. Cette appli utilise adapter.js, un petit fichier JavaScript qui s'occupe de la correspondance entre les différentes terminologies. Maintenu par Google, il permet de s'abstraire des différences entre les navigateurs et les spécifications.

RTCDataChannel est disponible dans Chrome 25, Opera 18 et Firefox 22 et ultérieurs.

Les fonctionnalités de WebRTC sont disponibles dans Internet Explorer via Chrome Frame et Skype (racheté par Microsoft en 2011) prévoit d'utiliser WebRTC. WebRTC a aussi été intégré par les applis natives WebKitGTK+ et Qt.

Avertissement

Certains rapports annoncent que telle ou telle plateforme 'supporte WebRTC' et doivent être traîtés avec scepticisme. En effet, il y a souvent confusion entre WebRTC et getUserMedia, et en effet la plateforme le supporte mais pas les autres composants de WebRTC.

Mon premier WebRTC

Les applications WebRTC nécessitent de faire plusieurs choses :

  • Récupérer des flux audio, vidéo ou autres données.
  • Récupérer les informations du réseau comme les adresses IP et les ports et les échanger avec d'autres clients WebRTC (appelés pairs), pour leur permettre d'établir une connexion, même au travers les NATs et les firewalls.
  • Coordonner la signalisation pour rapporter les erreurs ou clore les sessions.
  • Échanger les informations à propos des médias et de la compatibilité entre les clients en ce qui concerne leur résolution et leurs codecs.
  • Échanger des flux audios, vidéos ou simplement de données.

Pour récupérer et communiquer des flux, WebRTC implémente les APIs suivantes :

  • MediaStream : avoir accès aux flux de données multimédia comme la caméra et le micro de l'utilisateur.
  • RTCPeerConnection: appels audio ou vidéo avec quelques utilitaires pour l'encryption et la gestion de la bande passante.
  • RTCDataChannel: échange en peer-to-peer de données.

(Vous trouverez les détails de la gestion des aspects signalisation et réseau de WebRTC ci-dessous.)

MediaStream (ou getUserMedia)

L'API MediaStream gère la synchronisation de flux de données. Par exemple, parmi les flux venant de la caméra et du micro, les pistes d'image et de son sont synchronisées. (Ne mélangez pas les pistes de MediaStream (tracks) en anglais avec l'élément <track> qui est un tout autre sujet en HTML.)

La meilleure manière de comprendre MediaStream est certainement de voir cette sélection de démos parmi celles qui fleurissent sur le Web :

  1. Dans Chrome ou Opera, ouvrez la démo sur simpl.info/gum.
  2. Ouvrez la console.
  3. Inspectez la variable stream qui est dans le scope global.

Chaque MediaStream a un input (une entrée) qui est un MediaStream généré par navigator.getUserMedia() et un output (une sortie) qui est envoyé vers un élément video ou une connexion RTCPeerConnection.

La méthode getUserMedia() prend 3 arguments :

  • Un objet contrainte.
  • Un callback en cas de succès qui renvoie un MediaStream.
  • Un callback en cas d'échec qui renvoie un objet de type error.

Chaque MediaStream a un label, du grenre 'Xk7EuLhsuHKbnjLWkW4yYGNJJ8ONsgwHBvLQ'. Un tableau de MediaStreamTracks est renvoyé par les méthodes getAudioTracks() et getVideoTracks().

Dans l'exemple simpl.info/gum, stream.getAudioTracks() renvoie un tableau vide (parce qu'il n'y a pas de son) et, si vous disposez d'une webcam qui fonctionne, stream.getVideoTracks() renvoie un tableau contenant un MediaStreamTrack pour le flux vidéo. Chaque MediaStreamTrack a un attribut kind qui renvoie 'audio' ou 'video' et un label qui renvoie quelque chose du genre 'FaceTime HD Camera' et représente un ou plusieurs canaux audio ou vidéo. Dans ce cas, on n'a qu'une piste vidéo sans piste audio, mais on pourrait imaginer une application de chat qui récupèrerait les flux des deux caméras de chaque côté d'un téléphone mobile, du micro et d'une application de partage d'écran.

Dans Chrome ou Opera, la méthode URL.createObjectURL() convertit un MediaStream en Blob URL qui peut être défini comme src d'un élément videoelement. (Dans Firefox et Opera, le src de la vidéo peut appeler le flux lui-même.) Depuis leur version 25, les navigateurs à base de Chromium (Chrome et Opera) autorisent le flux de données audio à être passé depuis getUserMedia vers un élément audio ou video (mais prenez en compte que par défaut, l'élément média ne sera pas en sourdine).

getUserMedia peut aussi être utilisé comme source pour l'API Web Audio :

function gotStream(stream) {
    window.AudioContext = window.AudioContext || window.webkitAudioContext;
    var audioContext = new AudioContext();

    // Crée un AudioNode à partir du flux
    var mediaStreamSource = audioContext.createMediaStreamSource(stream);

    // Connectez le à une destination pour vous entendre
    // ou à n'importe quoi d'autre pour effectuer du traîtement!
    mediaStreamSource.connect(audioContext.destination);
}

navigator.getUserMedia({audio:true}, gotStream);

Les applications basées sur Chromium et leurs extensions peuvent aussi utiliser getUserMedia. Pour autoriser audioCaptureet/ou videoCapture, ajoutez-le au manifest et la permission ne sera demandée qu'une seule fois à l'utilisateur lors de l'installation. Dès lors, l'accès à la caméra et au micro seront automatiques.

Dans le même esprit pour les pages qui utilisent HTTPS : la permission n'est demandée qu'une seule fois pour getUserMedia() (dans Chrome du moins). La première fois, un bouton 'Toujous autoriser' s'affiche dans l'infobar du navigateur.

À terme, MediaStream pourrait capturer des flux de n'importe quelle source de données, pas seulement ceux de la caméra et du micro. On pourrait par exemple capter un flux venant du disque ou d'une toute autre source de données comme des capteurs.

Notez que getUserMedia() doit être utilisé sur un serveur, pas directement à partir du système de fichiers sinon une erreur de type PERMISSION_DENIED: 1 surviendra.

getUserMedia() devient vraiment cool quand on le combine avec d'autres API JavaScrpt :

  • Webcam Toy est une application de type photomaton qui utilise WebGL pour ajouter des effets aux photos qu'on peut partager ou enregistrer sur son ordinateur.
  • FaceKat est un détecteur de visage qui utilise headtrackr.js comme joystick.
  • ASCII Camera utilise l'API de Canvas pour générer des images ASCII images.
ASCII image generated by idevelop.ro/ascii-camera
De l'ASCII art avec GetUserMedia !

Jeux de contraintes

Les jeux de contraintes ont été implémentés dans Chrome depuis la version 24 et Opera 18. Elles définissent une liste de valeurs pour les appels à getUserMedia() et addStream() de RTCPeerConnection. Le but est d'implémenter un support pour d'autres séries de contraintes comme le ratio d'image, le 'facing mode' (caméra avant ou caméra arrière), frame rate (FPS), hauteur et largeur, dans une unique méthode applyConstraints().

Voir le petit exemple sur simpl.info/getusermedia/constraints.

Il y a un hic : le jeu de contraintes de getUserMedia apliqué dans un onglet du navigateur impacte touts les autres onglets. Attribuer un jeu de contrainte non autorisé renvoie un message d'erreur assez incompréhensible du genre :

navigator.getUserMedia error:
NavigatorUserMediaError {code: 1, PERMISSION_DENIED: 1}

Capture d'écran et d'onglet

Il est aussi possible d'utiliser la capture d'écran comme source de MediaStream. C'est actuellement possible dans Chrome en utilisant le jeu de contraintes experimental chromeMediaSource, comme dans cette demo. Cette fonctionnalité n'est pas encore disponible dans Opera. Notez que la capture d'écran nécessite HTTPS.

Les applications basées sur Chrome permettent aussi de partager un flux vidéo d'un onglet du navigateur via l'API encore expérimentale chrome.tabCapture. Pour faire du partage d'écran, veuillez consulter le code et les infos connexes sur cet article de HTML5 Rocks : Screensharing with WebRTC. Cette fonctionnalité n'est pas encore disponible sur Opera.

La signalisation : gestion de session, informations sur réseaux et médias

WebRTC utilise RTCPeerConnection pour communiquer des flux de données entre les navigateurs (pairs). Il a aussi besoin d'un mécanisme pour coordonner la communication et envoyer des messages de contrôle, c'est ce qu'on appelle la signalisation. Les méthodes de signalisation et les protocoles ne sont pas spécifiés par WebRTC : la signalisation n'est pas inclue dans l'API de RTCPeerConnection.

Les développeurs d'applications utilisant WebRTC peuvent donc utiliser la signalisation qu'ils veulent comme SIP ou XMPP, et n'importe quel canal de communication duplex (dans les deux sens). L'exemple apprtc.appspot.com utilise XHR et l'API Channel comme système de signalisation. Dans le codelab, on utilise une instance de Socket.io qui tourne sur un serveur Nodejs.

La signalisation est utilisée pour échanger trois types d'informations.

  • Messages de contrôle de session : pour initialiser ou fermer un lien de communication et rapporter les erreurs.
  • Configuration réseau : quelle est l'adresse IP et sur quel port l'ordinateur est-il connecté ?
  • Compatibilité des médias : quels codecs sont gérés et jusqu'à quelle résolution est-ce que mon navigateur et celui de mon interlocuteur vont pouvoir aller ?

Ces informations doivent avoir été échangées avec succès par la méthode de signalisation avant que le flux de pair à pair ne puisse commencer.

Par exemple, imaginez qu'Alice veuille communiquer avec Bob. Voici un exemple de code provenant du document de travail sur WebRTC auprès du W3C qui montre comment se passe la signalisation. Le code suppose qu'un système de signalisation existe, créé par la méthode createSignalingChannel(). On notera aussi que pour Chrome et Opera, RTCPeerConnection est actuellement préfixé.

var signalingChannel = createSignalingChannel();
var pc;
var configuration = ...;

// start(true) pour commencer l'appel
function start(isCaller) {
    pc = new RTCPeerConnection(configuration);

    // envoie un ice candidate à l'autre pair
    pc.onicecandidate = function (evt) {
        signalingChannel.send(JSON.stringify({ "candidate": evt.candidate }));
    };

    // affiche le flux vidéo
    pc.onaddstream = function (evt) {
        remoteView.src = URL.createObjectURL(evt.stream);
    };

    // récupère le flux local, le montre dans la vidéo de prévisu et l'envoie
    navigator.getUserMedia({ "audio": true, "video": true }, function (stream) {
        selfView.src = URL.createObjectURL(stream);
        pc.addStream(stream);

        if (isCaller)
            pc.createOffer(gotDescription);
        else
            pc.createAnswer(pc.remoteDescription, gotDescription);

        function gotDescription(desc) {
            pc.setLocalDescription(desc);
            signalingChannel.send(JSON.stringify({ "sdp": desc }));
        }
    });
}

signalingChannel.onmessage = function (evt) {
    if (!pc)
        start(false);

    var signal = JSON.parse(evt.data);
    if (signal.sdp)
        pc.setRemoteDescription(new RTCSessionDescription(signal.sdp));
    else
        pc.addIceCandidate(new RTCIceCandidate(signal.candidate));
};

Pour commencer, Alice et Bob échangent des informations sur la configuration de leurs réseaux respectifs. (L'expression 'recherche de candidats' [ou finding candidates en anglais] fait référence à la recherche d'interface réseaux et de ports en utilisant le framework ICE.)

  1. Alice crée un objet RTCPeerConnection avec un gestionnaire (handler) onicecandidate.
  2. Le handler est lancé quand les candidats dont disponibles.
  3. Alice envoie ses candidats à Bob via la méthode de signalisation qu'ils utilisent : WebSocket ou autre mécanisme.
  4. Quand Bob reçoit les candidats d'Alice, il appelle la méthode addIceCandidate pour les ajouter à sa description de pair distant.

Les clients WebRTC (connus comme pairs, dans notre cas Alice et Bob) ont aussi besoin de vérifier et échanger les informations sur les médias locaux et distants comme la résolution et les codecs. La signalisation de ces configurations s'effectue par l'échange d'une offre et d'une réponse en utilisant le protocole de description de session (Session Description Protocol, SDP):

  1. Alice lance la méthode createOffer() de RTCPeerConnection. En callback, elle reçoit un argument de type RTCSessionDescription qui contient la description de la session d'Alice.
  2. Dans le callback, Alice spécifie la description locale en utilisant setLocalDescription() et dès lors envoie la description de cette session à Bob au travers de leur canal de signalisation. Notez que RTCPeerConnection ne va pas commencer à rechercher des candidats tant que setLocalDescription() est appelé: ceci est spécifié dans la proposition sur l'JSEP formulée auprès de l'IETF.
  3. Bob enregistre la description envoyée par Alice comme une description distante en utilisant setRemoteDescription().
  4. Bob lance la méthode createAnswer() de RTCPeerConnection en lui passant la description distante qu'il a reçu d'Alice afin de génerer une session locale qui lui soit compatible. Le callback de createAnswer() lui renvoie une RTCSessionDescription: Bob l'enregistre comme sa description locale de session et l'envoie à Alice.
  5. Quand Alice reçoit la description de session de Bob, elle l'enregistre comme la description de la session distante avec setRemoteDescription.
  6. Hop!

Les objets RTCSessionDescription se conforment à la description de protocoles de session (Session Description Protocol), SDP. Sérialisé, un objet SDP ressemble à ça :

v=0
o=- 3883943731 1 IN IP4 127.0.0.1
s=
t=0 0
a=group:BUNDLE audio video
m=audio 1 RTP/SAVPF 103 104 0 8 106 105 13 126

// ...

a=ssrc:2223794119 label:H4fjnMzxy3dPIgQ7HxuCTLb4wLLLeRHnFxh810

L'acquisition et l'échange des informations de médias et de réseaux peuvent être effectués de manière simultanée, mais doivent être achevés avant que les pairs puissent commencer à échanger leurs flux.

L'architecture offre/réponse décrite ci-dessus est appelée JSEP pour JavaScript Session Establishment Protocol.

(Il y a une excellente animation qui explique le procédé de signalisation et d'échange de flux dans la vidéo de démo d'Ericsson pour sa première implémentation de WebRTC.)

JSEP architecture diagram
L'architecture JSEP

Une fois que le processus de signalisation s'est correctement déroulé, les données peuvent être transférées directement de pair à pair entre l'appelant et l'appelé — ou si ça a échoué par l'intermédiaire d'un serveur de relais (on en parle plus bas). La diffusion est effectuée par RTCPeerConnection.

RTCPeerConnection

RTCPeerConnection est le composant de WebRTC qui gère l'efficacité et la stabilité des échanges de flux entre les pairs.

Le schéma ci-dessous montre le rôle de RTCPeerConnection. Comme vous pouvez le voir, les zones en vert sont assez complexes !

WebRTC architecture diagram
architecture de WebRTC (de webrtc.org)

En ce qui concerne le JavaScript, ce qu'il faut comprendre, c'est que RTCPeerConnection protège les développeurs d'une myriade de complexités qui se cachent sous la spécification. Les codecs et les protocoles utilisés par WebRTC fournissent un travail considérable pour rendre possible la communication en temps réel, même sur des réseaux instables :

  • Masquage des effets de la perte de paquets IP
  • Annulation de l'écho
  • Adaptation de la bande passante
  • Gestion dynamique du buffer pour éviter les effets de sautillement de l'image
  • Contrôle automatique du gain
  • Réduction et suppression du bruit
  • 'Nettoyage' de l'image.

Le code du W3C ci-dessus montre un exemple simplifié de WebRTC d'un point de vue signal. Vous trouverez ci-dessous un guide pas à pas de deux applications WebRTC : le premier est une simple démonstration de RTCPeerConnection; le second est un client de chat vidéo complet.

RTCPeerConnection sans serveurs

Le code ci-dessous vient de la démo de WebRTC sur une seule page consultable sur webrtc-demos.appspot.com. Il y a une connexion RTCPeerConnection locale et une distante (une vidéo est locale, l'autre distante) sur une seule page. Ça ne sert à rien d'avoir l'appelé et l'appelant sur la même page, mais ça permet de mieux comprendre les objets RTCPeerConnection en s'affranchissant des mécanismes de signalisation.

Seul hic : le deuxième jeu de 'contraintes' (optionnel) du constructeur RTCPeerConnection() est différent des jeux de contraintes utilisés lors du getUserMedia() : voir w3.org/TR/webrtc/#constraints pour plus d'infos.

Dans cet exemple, pc1 représente le pair local (l'appelant) et pc2 représente le pair distant (appelé).

L'appelant

  1. Crée une nouvelle RTCPeerConnection et attache le flux de getUserMedia() :

    // 'servers' est une config optionnelle (voir la note sur TURN et STUN plus bas)
    pc1 = new webkitRTCPeerConnection(servers);
    // ...
    pc1.addStream(localstream); 
  2. Crée une offre et l'associe à la description locale pour pc1 et à la description distante pour pc2. Dans ce cas, c'est fait directement dans le code sans utiliser de signalisation puisque l'appelant et l'appelé sont dans la même page.

    pc1.createOffer(gotDescription1);
    //...
    function gotDescription1(desc){
      pc1.setLocalDescription(desc);
      trace("Offer from pc1 \n" + desc.sdp);
      pc2.setRemoteDescription(desc);
      pc2.createAnswer(gotDescription2);
    }
    

L'appelé

  1. Crée pc2 et, quand le flux arrive de pc1, l'affiche dans un élément vidéo :

    pc2 = new webkitRTCPeerConnection(servers);
    pc2.onaddstream = gotRemoteStream;
    //...
    function gotRemoteStream(e){
      vid2.src = URL.createObjectURL(e.stream);
    }
    

RTCPeerConnection avec serveurs

Dans la vraie vie, WebRTC a besoin de serveurs pour permettre différentes choses :

  • Les utilisateurs doivent se découvrir et échanger leurs détails respectifs, comme leurs noms.
  • Les clients WebRTC (pairs) doivent échanger leurs configurations réseau.
  • Les pairs doivent échanger des infos sur leurs capacités comme leurs formats vidéo et résolution.
  • Les clients WebRTC doivent passer les barrières des NAT et des firewalls (pare-feux).

En d'autres termes, WebRTC a besoin de quatre types de fonctionnalités fournies par les serveurs :

  • Découverte d'utilisateurs et communication.
  • Signalisation.
  • Traversée des NAT/firewall.
  • Serveurs de relais au cas où la communication de pair à pair échoue.

Nous ne traiterons dans cet article ni la traversée des NAT, ni les réseaux de pair à pair, ni les moyens de faire se rencontrer les utilisateurs, ni encore les différents moyens de signalisation, parce qu'on s'écarterait trop du sujet. Disons pour faire simple que le protocole STUN et ses extensions TURN sont utilisés dans le framework ICE pour permettre à RTCPeerConnection de gérer la traversée des NAT et autres aléas liés aux réseaux.

ICE est un framework pour connecter les pairs comme deux personnes qui feraient du chat vidéo. Pour commencer, ICE tente de connecter les pairs directement, avec le moins de latence possible, via UDP. Dans ce processus, des serveurs STUN ne font qu'une chose : permettre au pair derrière son NAT de trouver son adresse IP publique et son port. (Google a quelques serveurs STUN, l'un d'entre eux est utilisé pour l'exemple sur apprtc.appspot.com.)

Finding connection candidates
À la recheche de candidats de connexion

Si UDP échoue, ICE essaye TCP: d'abord HTTP puis HTTPS. Si la communication en direct échoue — souvent à cause des NAT ou des firewalls — ICE utilise un serveur TURN relais. En d'autres termes, ICE va d'abord essayer STUN en UDP pour connecter les pairs entre eux, et si ça échoue, il se rabattra sur un serveur de relais TURN. L'expression «recherche de candidats» (en anglais «finding candidates») fait référence au procédé de découverte des interfaces réseaux et des ports.

WebRTC data pathways
le chemin des données dans WebRTC

Justin Uberti, un des concepteurs de WebRTC explique plus en détails ICE, STUN et TURN lors de la présentation de WebRTC lors da la Google I/O de 2013. (Les supports de présentation donnent des exemples d'implémentation de serveurs TURN et STUN.)

Un simple client de chat vidéo

Les explications ci-dessous décrivent les mécanismes de signalisation utilisés par apprtc.appspot.com.

Si vous êtes un peu perdu, essayez le WebRTC codelab. Ce guide qui explique pas à pas comment monter une application de chat vidéo, il y a même un petit serveur de signalisation basé sur Socket.io sur un serveur Nodejs.

Testez un environnement WebRTC complet avec signalisation et traversée de NAT/firewall par STUN sur la démo apprtc.appspot.com. Cette application utilise adapter.js pour gérer les différentes implémentations de RTCPeerConnection et getUserMedia().

Le code est délibérément verbeux dans ses logs : gardez un œil sur la console pour suivre l'enchaînement des événements. Nous donnerons ci-dessous des détails du code pas à pas.

Que se passe-t-il ?

La démo commence en lançant la fonction initalize() :

function initialize() {
    console.log("Initializing; room=99688636.");
    card = document.getElementById("card");
    localVideo = document.getElementById("localVideo");
    miniVideo = document.getElementById("miniVideo");
    remoteVideo = document.getElementById("remoteVideo");
    resetStatus();
    openChannel('AHRlWrqvgCpvbd9B-Gl5vZ2F1BlpwFv0xBUwRgLF/* ...*/');
    doGetUserMedia();
  }

Vous remarquerez que des valeurs comme celles de la variable room et le token utilisé par openChannel() sont fournis au niveau du serveur par le Google App Engine : jetez un œil au template de index.html dans le repository pour voir les valeurs qui sont ajoutées.

Ce code initialise les variables pour l'élément HTML video qui va afficher le flux venant de la caméra locale (localVideo) et de la caméra distante (remoteVideo). resetStatus() quant à lui définit juste un message avertissant sur l'état.

La fonction openChannel() met en place l'échange de messages entre les clients WebRTC :

function openChannel(channelToken) {
  console.log("Ouverture du canal.");
  var channel = new goog.appengine.Channel(channelToken);
  var handler = {
    'onopen': onChannelOpened,
    'onmessage': onChannelMessage,
    'onerror': onChannelError,
    'onclose': onChannelClosed
  };
  socket = channel.open(handler);
}

Pour la signalisation, cette démo utilise le Channel API de Google App Engine, qui transmet les messages entre les clients JavaScript sans polling. (La signalisation dans WebRTC est traitée plus en détails ci-dessous).

Architecture of the apprtc video chat application
Architecture de l'application apprtc de chat vidéo

Pour établir un canal avec la Channel API, ça marche comme ça :

  1. Le client A génère un identifiant (ID) unique.
  2. Le client A demande un accès auprès de l'application en lui envoyant son ID.
  3. L'App Engine crée un canal (channel) et génère un token pour y accéder.
  4. L'App envoie le token au client A.
  5. Le client A ouvre un socket et se connecte au channel créé sur le serveur.
The Google Channel API: establishing a channel
La Channel API de Google : création d'un channel

L'envoi de messages fonctionne comme ceci :

  1. Le client B fait une requête POST à l'application.
  2. L'App passe la requête au channel.
  3. Le client A reçoit le message par le channel.
  4. Chez le client A, un callback "onmessage" est lancé.
The Google Channel API: sending a message
La Channel API de Google : envoi d'un message

Petit rappel : les messages de signalisation peuvent être communiqués par n'importe quel mécanisme choisi par le développeur, il n'est pas spécifié par WebRTC. Dans cette démo, on utilise la Channel API, mais d'autres méthodes (comme les WebSockets) peuvent être utilisées à la place.

Après avoir appelé OpenChannel(), la fonction getUserMedia() appelée par initialize() vérifie si le navigateur supporte l'API getUserMedia. (Il y a plein d'articles à propos de getUserMedia sur HTML5 Rocks.) Si tout se passe bien, onUserMediaSuccess est appelé :

function onUserMediaSuccess(stream) {
  console.log("L'utilisateur a autorisé l'accès à la caméra et au micro.");
  // Ce polyfill permet d'attacher le flux de médias à cet élément.
  attachMediaStream(localVideo, stream);
  localVideo.style.opacity = 1;
  localStream = stream;
  // L'appelant créé une PeerConnection.
  if (initiator) maybeStart();
}

Là, on a la vidéo de la caméra locale qui s'affiche dans l'élément localVideo, un objet (Blob) URL fait référence aux données créées par le flux vidéo qui est envoyé vers l'attribut src de l'élément. (createObjectURL est utilisé pour accéder à l'URI "en mémoire" de la ressource binaire, c'est à dire le LocalDataStream de la vidéo.) le flux de données est aussi envoté dans localStream, pour le rendre disponible pour l'interlocuteur.

Dans ce cas, initiator a pour valeur 1 (et restera à cette valeur jusqu'à la fin de l'appel) donc maybeStart() est appelé :

function maybeStart() {
  if (!started && localStream && channelReady) {
    // ...
    createPeerConnection();
    // ...
    pc.addStream(localStream);
    started = true;
    // L'appelant lance l'offre à l'appelé
    if (initiator)
      doCall();
  }
}

On utilise ici maybeStart() qui peut être appelé d'un peu n'importe où dans le code mais qui ne s'execute que quand localStream a été défini et channelReady a pour valeur true et que l'appel n'a pas encore commencé. Donc, si l'appel n'a pas encore commencé, et qu'un flux média local est disponible et qu'en plus le canal de communication est prêt à assurer la signalisation, alors on lance l'appel en passant le flux vidéo local. Une fois que cet appel a eu lieu, started prend pour valeur true pour éviter de relancer un deuxième appel.

RTCPeerConnection : l'appel

C'est dans createPeerConnection(), appelé par maybeStart(), que l'appel commence vraiment :

function createPeerConnection() {
  var pc_config = {"iceServers": [{"url": "stun:stun.l.google.com:19302"}]};
  try {
    // Création d'une RTCPeerConnection via adapter.js
    pc = new RTCPeerConnection(pc_config);
    pc.onicecandidate = onIceCandidate;
    console.log("Created RTCPeerConnnection with config:\n" + "  \"" +
      JSON.stringify(pc_config) + "\".");
  } catch (e) {
    console.log("Impossible de créer PeerConnection, erreur: " + e.message);
    alert("L'objet RTCPeerConnection ne peut pas être créé; WebRTC n'est pas supporté par ce navigateur.");
      return;
  }

  pc.onconnecting = onSessionConnecting;
  pc.onopen = onSessionOpened;
  pc.onaddstream = onRemoteStreamAdded;
  pc.onremovestream = onRemoteStreamRemoved;
}

L'objectif sous-jacent est de mettre en place une connexion en utilisant un serveur STUN qui appelle onIceCandidate() (voir ci-dessus l'explication de ICE, STUN et la recherche de candidats). Chaque événement RTCPeerConnection, quand une session se connecte ou est ouverte et quand un flux distant est ajouté ou enlevé, doit être écouté. Dans cet exemple, tous ces événements sont logués — sauf onRemoteStreamAdded() qui va être orienté vers l'élément remoteVideo :

function onRemoteStreamAdded(event) {
  // ...
  miniVideo.src = localVideo.src;
  attachMediaStream(remoteVideo, event.stream);
  remoteStream = event.stream;
  waitForRemoteVideo();
}

Une fois que createPeerConnection() a été invoqué par maybeStart(), un appel est créé par l'établissement d'une offre et son envoi à l'appelé :

function doCall() {
  console.log("Sending offer to peer.");
  pc.createOffer(setLocalAndSendMessage, null, mediaConstraints);
}

Le processus de création de l'offre est ici similaire à l'exemple sans signalisation ci-dessus, mais, en plus, un message est envoyé au pair distant, en donnant une SessionDescription (description de session) sérialisée pour l'offre. Ce procédé est géré par setLocalAndSendMessage() :

function setLocalAndSendMessage(sessionDescription) {
  // Envoie "Opus" comme codec préféré dans le SDP si Opus est dispo sur la machine.
  sessionDescription.sdp = preferOpus(sessionDescription.sdp);
  pc.setLocalDescription(sessionDescription);
  sendMessage(sessionDescription);
}

Signalisation avec la Channel API

Le callback onIceCandidate() invoqué quand la RTCPeerConnection a été bien créée dans createPeerConnection() envoie des informations à propos des candidats au fur et à mesure qu'ils se "rencontrent" :

function onIceCandidate(event) {
    if (event.candidate) {
      sendMessage({type: 'candidate',
        label: event.candidate.sdpMLineIndex,
        id: event.candidate.sdpMid,
        candidate: event.candidate.candidate});
    } else {
      console.log("Tous les jeux de candidats ont été envoyés.");
    }
  }

L'envoi de messages du client vers le serveur est pris en charge par sendMessage() qui fait des requêtes AJAX :

function sendMessage(message) {
  var msgString = JSON.stringify(message);
  console.log('C->S: ' + msgString);
  path = '/message?r=99688636' + '&u=92246248';
  var xhr = new XMLHttpRequest();
  xhr.open('POST', path, true);
  xhr.send(msgString);
}

AJAX (XmlHttpRequest) fonctionne bien pour envoyer la signalisation du client vers le serveur, mais pas pour la signalisation de serveur à client. Cette application utilise le Google App Engine Channel API. Les messages de l'API (ceux du serveur, donc) sont gérés par processSignalingMessage() :

function processSignalingMessage(message) {
  var msg = JSON.parse(message);

  if (msg.type === 'offer') {
    // L'appelé crée une PeerConnection
    if (!initiator && !started)
      maybeStart();

    pc.setRemoteDescription(new RTCSessionDescription(msg));
    doAnswer();
  } else if (msg.type === 'answer' && started) {
    pc.setRemoteDescription(new RTCSessionDescription(msg));
  } else if (msg.type === 'candidate' && started) {
    var candidate = new RTCIceCandidate({sdpMLineIndex:msg.label,
                                         candidate:msg.candidate});
    pc.addIceCandidate(candidate);
  } else if (msg.type === 'bye' && started) {
    onRemoteHangup();
  }
}

Si le message est une réponse d'un pair (une réponse à une offre), RTCPeerConnection le place dans sa SessionDescription distante et l'appel peut commencer. Si le message est une offre (un message de l'appelé) RTCPeerConnection enregistre la SessionDescription distante, prépare une réponse et l'envoie en tant que réponse à l'appelé, et commence à invoquer la méthode startIce() de RTCPeerConnection :

function doAnswer() {
  console.log("Sending answer to peer.");
  pc.createAnswer(setLocalAndSendMessage, null, mediaConstraints);
}

Et c'est tout ! L'appelant et l'appelé sont en contact et ont échangé les informations sur leurs capacités, une session d'appel a été lancée et de l'échange de données en temps réel peut commencer.

Topologies de réseau

WebRTC tel qu'il est actuellement implémenté ne supporte que les communications 1 à 1 mais peut être utilisé dans des scenarii plus complexes, par exemple 3 utilisateurs pourraient communiquer les uns avec les autres directement de pair à pair ou en passant par une MCU (Multipoint Control Unit), un serveur qui peut gérer un grand nombre de participants et fait du transfert de flux, les mixe ou encore les enregistre :

Multipoint Control Unit topology diagram
exemple de Multipoint Control Unit

La plupart des applications WebRTC servent à communiquer entre différents navigateurs, mais une appli serveur peut permettre l'intéraction de cette technologie avec le Réseau téléphonique commuté (ou bon vieux bigophone) et avec les systèmes de Voix sur IP. En mai 2012, Doubango Télécom a rendu open-source le client SIP sipml5, basé sur WebRTC et WebSocket qui permet (entre autres) des appels vidéos entre navigateurs et des apps sur iOS ou Android. À la Google I/O, Tethr et Tropo ont fait une démo d'un dispositif de télécommunication d'urgence dans une valise utilisant un appareil OpenBTS pour permettre la communication entre ordinateurs et téléphones via WebRTC... autrement dit, de la communication téléphonique sans opérateur!

Tethr/Tropo demo at Google I/O 2012
Tethr/Tropo: dispositif de télécommunication d'urgence dans une valise

RTCDataChannel

En plus de l'audio et de la vidéo, WebRTC supporte un canal temps-réel pour d'autres types de données.

L'API RTCDataChannel permet de faire des échanges de pair à pair de n'importe quelle donnée avec peu de latence et un bon débit. Il y a une page de démo sur simpl.info/dc.

Cette fonctionnalité ouvre de nouvelles portes à cette API comme par exemple :

  • Les jeux vidéos
  • Les applications de bureau à distance
  • Le chat
  • L'échange de fichiers
  • Les réseaux décentralisés en tout genre

L'API a quelques fonctionalités qui permettent de tirer le meilleur de RTCPeerConnection pour mettre en place une communication de qualité de pair à pair :

  • Réutilisation des sessions RTCPeerConnection.
  • Multiples canaux simultanés avec prioritisation.
  • Gestion de la fiabilité de livraison.
  • Gestion intégrée de la sécurité (DTLS) et gestion de la congestion.
  • Possibilité d'utilisation avec ou sans audio et vidéo.

La syntaxe est volontairement similaire au WebSocket, avec une méthode send()et une gestion des événements message :

var pc = new webkitRTCPeerConnection(servers,
  {optional: [{RtpDataChannels: true}]});

pc.ondatachannel = function(event) {
  receiveChannel = event.channel;
  receiveChannel.onmessage = function(event){
    document.querySelector("div#receive").innerHTML = event.data;
  };
};

sendChannel = pc.createDataChannel("sendDataChannel", {reliable: false});

document.querySelector("button#send").onclick = function (){
  var data = document.querySelector("textarea#send").value;
  sendChannel.send(data);
};

La communication se fait directement entre les navigateurs si bien que RTCDataChannel peut être beaucoup plus rapide que WebSocket même si un serveur de relais (TURN) est requis pour la traversée des firewalls et des NAT.

RTCDataChannel est disponible dans Chrome, Opera et Firefox. Le superbe jeu Cube Slam utilise cette API pour gérer la couche communication. Sharefest permet de faire du partage de fichiers en P2P via RTCDataChannel et peerCDN propose une approche de ce que serait la distribution de contenu en P2P.

Pour plus d'infos sur RTCDataChannel, jetez un œil au draft de la spécification par l'IETF.

Sécurité

Les solutions de communication en temps réel présentent plusieurs vulnérabilités potentielles comme par exemple :

  • Les médias non cryptés pourraient être interceptés en route entre les navigateurs ou entre un navigateur et un serveur.
  • Une application pourrait enregistrer le son et/ou la vidéo à l'insu de l'utilisateur.
  • Des malwares ou des virus pourraient être installés cachés dans les plugins ou les applications.

WebRTC apporte plusieurs solutions pour se prémunir contre ces problèmes :

  • l'implémentation de WebRTC utilise des couches de sécurisation comme DTLS et SRTP.
  • Le cryptage est obligatoire pour tous les composants de WebRTC y compris la signalisation.
  • WebRTC n'est pas un plugin : il tourne dans une sandbox du navigateur dans un processus séparé et il ne requiert aucune installation de logiciel tiers. En plus il se met à jour en même temps que le navigateur.
  • Les accès à la caméra et au micro doivent être autorisés explicitement et quand ils sont actifs, c'est indiqué de manière claire dans l'interface de l'utilisateur.

Pour une approche plus approfondie sur la sécurisation des flux de médias, vous pouvez consulter le document WebRTC Security Architecture proposé par l'IETF.

En conclusion

La bibliothèque logicielle et les standards de WebRTC peuvent démocratiser et décentraliser des outils pour la création de contenu et la communication — pour la téléphonie, les jeux vidéos, la production audiovisuelle, la (co)création musicale, la diffusion de messages et de nombreuses autres applications.

Difficile de trouver une technologie plus en rupture que celle-ci.

Nous avons hâte de voir ce que les développeurs JavaScript feront de WebRTC au fur et à mesure de son implémentation. Comme l'écrit le blogueur Phil Edholm , «Potentiellement, WebRTC et HTML5 pourraient rendre possible le même genre d'avancées pour la communication en temps réel que le navigateur a apporté pour la communication figée.»

Outils de développement

  • La page de Chrome chrome://webrtc-internals (ou pour Opéra opera://webrtc-internals) fournit des informations détaillées et des statistiques sur les sessions WebRTC en cours :
    chrome://webrtc-internals page
    Copie d'écran de chrome://webrtc-internals
  • Notes d'interopérabilité entre les navigateurs
  • adapter.js est un adaptateur maintenu par Google pour assurer le maximum de compatibilité entre les navigateurs au fur et à mesure de leur implémentation se WebRTC
  • Pour en savoir plus sur la signalisation de WebRTC, regardez les console.log de apprtc.appspot.com
  • Si tout ceci est un peu trop touffu, vous pouvez préférer un framework WebRTC ou carrément un service WebRTC
  • La remontée de bugs et la demande de nouvelles fonctionalités sont toujours appréciés : crbug.com/new pour Chrome, bugs.opera.com/wizard/ pour Opera et bugzilla.mozilla.org pour Firefox

Pour en savoir plus

Standards et protocoles

Résumé du support de WebRTC

MediaStream et getUserMedia

  • Chrome desktop 18.0.1008+; Chrome pour Android 29+
  • Opera 18+; Opera pour Android 20+
  • Opera 12, Opera Mobile 12 (basé sur le moteur Presto)
  • Firefox 17+

RTCPeerConnection

  • Chrome desktop 20+ ; Chrome pour Android 29+
  • Opera 18+ ; Opera for Android 20+
  • Firefox 22+

RTCDataChannel

  • Version expérimentale dans Chrome 25, plus stable (et avec l'interopérabilité Firefox) dans Chrome 26+; Chrome pour Android 29+
  • Verison stable (et avec la compatibilité Firefox) dans Opera 18+; Opera pour Android 20+
  • Firefox 22+

WebRTC est dispo pour Internet Explorer via Chrome Frame : vidéo de démo et lien dans la description.

Les API natives pour RTCPeerConnection sont aussi disponibles dans la documentation sur webrtc.org.

Pour plus d'informations sur le support de WebRTC sur les différentes plateformes, veuillez vous référer à caniuse.com.

Comments

0

Next steps

Except as otherwise noted, the content of this page is licensed under the Creative Commons Attribution 3.0 License, and code samples are licensed under the Apache 2.0 License.