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éthodesetRemoteDescription
-
A et B active la webcam et/ou le micro via la méthode
getUserMedia
puis attache le stream à laPeerConnection
-
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
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
.
Au final notre architecture "peer-to-peer" ressemblera plutôt à ça dans le pire des cas.