Définir les phasers sur STUN/TURN avec WebRTC, Node.js, Socket.io et Twilio
Temps de lecture: 13 minutes
Les dernières semaines ont été passionnantes en matière de lancements pour Twilio. Mon favori a été le lancement de notre service Network Traversal. Bien que cela puisse paraître un peu ennuyant, c'est un service important pour les applications WebRTC, car il supprime la surcharge que constitue le déploiement de votre propre réseau de serveurs STUN et TURN. Je mourais d'envie de trouver une excuse pour tester WebRTC, j'avais là une bonne raison de le faire.
Bien évidemment, ce serait manquer à mes devoirs que de garder le code et le processus de mise en place d'une application WebRTC pour moi-même. Tout au long de ce post, je vais vous expliquer comment j'ai commencé à créer une application de chat vidéo avec WebRTC. Vous passerez ainsi moins de nuits blanches à vous demander quel rappel vous avez manqué ou quel message vous n'avez pas encore mis en œuvre et plus de temps à saluer vos amis et à penser à des applications sympas pour cette technologie.
Donnons vie à du WebRTC !
WebRTC, qu'est-ce que c'est ?
Commençons par quelques définitions pour nous assurer que nous savons tous de quoi nous parlons.
WebRTC est un ensemble d'API JavaScript qui permettent la communication de données, audio et vidéo P2P, en temps réel et sans plug-in entre deux navigateurs. Simple, non ? Nous verrons les API JavaScript dans le code plus tard.
WebRTC, qu'est-ce que ça n'est pas ?
Il est également important de parler de ce que WebRTC ne fait pas pour nous, car c'est la partie de l'application que nous avons réellement besoin de construire. Bien qu'une connexion WebRTC entre deux navigateurs soit de pair-à-pair, nous exigeons toujours que les serveurs travaillent pour nous. Les trois parties de l'application requises sont les suivantes :
Configuration réseau
Il s'agit d'informations sur l'adresse IP publique et le numéro de port sur lesquels un navigateur peut être atteint. C'est là que le service Network Traversal de Twilio intervient. Comme expliqué dans la présentation, lorsqu'un pare-feu et un NAT sont impliqués, il n'est pas futile de découvrir comment accéder publiquement à un point de terminaison. Les serveurs STUN et TURN peuvent être utilisés pour découvrir ces informations. Le navigateur fait beaucoup de travail ici, mais nous verrons comment le configurer avec l'accès au service de Twilio plus tard.
Présence
Les navigateurs vivent généralement une vie solitaire, sans aucune prise en compte des autres navigateurs susceptibles de les contacter. Afin de connecter un navigateur à un autre, nous allons devoir découvrir la présence d'un deuxième navigateur d'une quelconque façon. C'est à nous de construire un moyen pour les navigateurs de découvrir d'autres navigateurs qui sont prêts à prendre un appel vidéo.
Signalisation
Enfin, une fois qu'un navigateur décide de contacter un autre homologue, il doit envoyer et recevoir à la fois les informations réseau reçues des serveurs STUN/TURN, ainsi que des informations sur ses propres capacités multimédias. C'est ce que l'on appelle la signalisation, et c'est la majorité du travail que nous devons faire dans cette application.
Pour une vue beaucoup plus approfondie sur WebRTC et les technologies environnantes, je recommande vivement l'introduction de HTML5 Rocks à WebRTC et leur article plus détaillé sur STUN, TURN et la signalisation.
Outils
Pour construire notre WebRTC « Hello World ! » (ce qui, étonnamment, est une application de chat vidéo), nous avons besoin de quelques outils. Puisque nous parlons de JavaScript sur le front-end, j'ai décidé d'utiliser JavaScript pour le back-end aussi. Nous allons donc utiliser Node.js. Nous avons également besoin de quelque chose pour servir notre application et pour ce projet, j'ai choisi Express. Pour la présence et la signalisation, tout canal de communication bidirectionnel peut être utilisé. J'ai choisi WebSockets avec Socket.io pour la simplicité de l'API.
Tout ce dont nous avons besoin pour commencer, c'est d'un compte Twilio, d'un ordinateur avec une webcam et Node.js installé. Oh, et d'un navigateur qui prend en charge WebRTC. En ce moment, c'est le cas de la plupart d'entre eux. Vous avez tout cela ? Bon, écrivons un peu de code.
Mise en route
Sur la ligne de commande, préparez votre application :
Entrez les informations demandées par npm init (vous pouvez appuyer sur Entrée ici pour la plupart d'entre elles). Maintenant, installez vos dépendances :
Créez les fichiers et les répertoires dont vous allez avoir besoin.
Ouvrez ensuite public/index.html et entrez la page HTML basique suivante :
Comme vous pouvez le voir, cela inclut deux éléments <video> vides, quelques boutons que nous allons utiliser pour contrôler nos appels et les fichiers JavaScript que nous avons définis précédemment avec la bibliothèque client Socket.io.
Enfin, nous allons configurer notre serveur. Ouvrez index.js et saisissez les informations suivantes :
Il s'agit d'une configuration de base pour Express, nous ne faisons rien de spécial ici, si ce n'est relier le processus Socket.io à l'objet serveur Express.
Nous avons également chargé la bibliothèque de nœuds Twilio ici, et vous pouvez voir que j'ai inclus les identifiants API de l'environnement. Avant d'exécuter le serveur, vous devez vous assurer que vous disposez de ces identifiants dans l'environnement.
Maintenant, exécutez le serveur et assurez-vous que tout fonctionne.
Ouvrez http://localhost:3000 et vérifiez que vous avez un titre, des éléments vidéo vides et deux boutons. Est-ce que tout est là ? Continuons.
Flux vidéo et audio
Nous sommes prêts. La première chose que nous devons faire pour démarrer le processus d'appel vidéo est de prendre en main les flux vidéo et audio de l'utilisateur. Pour cela, nous utiliserons l'API Navigator.getUserMedia().
Nous allons écouter pour un clic sur le premier élément <button>
que nous avons ajouté à la page et demander les flux de la webcam et du microphone de l'utilisateur. Ouvrez public/app.js et saisissez les informations suivantes :
Le code ci-dessus réalise quelques actions, alors parlons-en. J'ai d'abord configuré un objet VideoChat
, pour stocker quelques objets et fonctions que nous allons définir tout au long du processus. Le premier objet que nous saisissons est le bouton vidéo, auquel nous attachons un écouteur d'événement de clic (ce n'est malheureusement pas jQuery ici, seulement des API DOM vanilla). Lorsque nous cliquons sur le bouton, nous faisons la demande d'accès aux flux vidéo et audio via la fonction getUserMedia
.
Suite à l'appel à getUserMedia
, le navigateur invite l'utilisateur à accepter ou à refuser la demande d'utilisation des éléments multimédias par la page. Dans Firefox, cela se présente sous cette forme :
Dans Chrome, cela se présente sous cette forme :
Si vous acceptez, la promesse getUserMedia
est résolue et la fonction onMediaStream()
est appelée. Si vous refusez les autorisations, la promesse est rejetée et la fonction noMediaStream()
est appelée. Lorsque nous recevrons le flux, nous l'enregistrerons dans notre objet VideoChat
et l'ajouterons à l'élément vidéo pour que vous puissiez vous voir (nous diminuons également le volume à 0 pour éviter les échos). Pour ce faire, nous devons affecter le flux renvoyé par getUserMedia
à la propriété srcObject de l'élément vidéo. Nous désactivons également le bouton « Get Video » (Obtenir la vidéo), car nous n'en avons plus besoin.
Enregistrez cela, rechargez la page, cliquez sur « Get Video » (Obtenir la vidéo) et vous devriez voir la fenêtre contextuelle des autorisations. Acceptez et vous devriez vous voir !
Présence de l'utilisateur
Ensuite, nous devons établir un moyen de savoir que nous avons un autre utilisateur à l'autre bout prêt à passer un appel. À la fin de cette section, nous aurons activé le bouton « Call » (Appeler) lorsque nous saurons qu'il y a quelqu'un à l'autre bout.
Afin de commencer à transmettre des messages entre les navigateurs dans le cadre de notre signalisation, nous devons commencer à utiliser nos WebSockets. Ouvrez à nouveau index.js et copiez et collez le code suivant avant la fonction server.start.
C'est une idée très basique d'une salle et d'une présence. Seuls deux utilisateurs peuvent rejoindre la salle à la fois. Lorsqu'un client tente de rejoindre une salle, nous comptabilisons le nombre de clients actuellement présents dans la salle. Si le nombre est égal à zéro, le client peut s'y joindre. S'il est égal à un, il peut s'y joindre et le socket indique aux deux clients qu'ils sont prêts. S'il y a déjà deux clients dans la salle, elle a atteint sa capacité maximale et aucun autre client ne peut s'y joindre pour le moment.
Nous devons maintenant rejoindre la salle du client. Nous avons besoin de démarrer une connexion au serveur socket. Nous pouvons le faire en appelant simplement io()
. Affectez-le à notre objet VideoChat
pour que nous puissions l'utiliser ultérieurement. Ensuite, à la fin de la fonction onMediaStream
, ajoutez deux lignes supplémentaires, l'une pour rejoindre la salle et l'autre pour écouter l'événement. Nous avons alors besoin d'une fonction de rappel une fois que nous avons entendu que la salle est prête. Dans ce rappel, nous allons activer le bouton « Call » (Appeler).
Il vaut mieux également contrôler ce bouton « Call » (Appeler). Au bas du fichier où nous avons saisi le bouton « Get Video » (Obtenir la vidéo), nous allons faire de même pour le bouton « Call » (Appeler).
Créons une méthode startCall factice dans l'objet VideoChat pour nous assurer que les choses se déroulent comme prévu.
Redémarrez maintenant le serveur de nœud (Ctrl + C pour arrêter le processus et $ node index.js
pour le redémarrer), ouvrez deux fenêtres de navigateur sur http://localhost:3000 et cliquez sur « Get Video » (Obtenir la vidéo) dans les deux. Une fois que les deux vidéos sont en cours de lecture, les boutons « Call » (Appeler) de chaque fenêtre doivent être actifs, et un clic sur le bouton « Call » (Appeler) devrait enregistrer un message adéquat sur la console de votre navigateur.
Démarrage de la signalisation
Notre bouton « Call » (Appeler) est très important, car il va lancer le reste du processus WebRTC. Ce sont les dernières interactions que l'utilisateur doit exécuter pour démarrer l'appel.
Le bouton « Call » (Appeler) va configurer un certain nombre de processus. Il va créer l'objet RTCPeerConnection qui gérera la création de la connexion entre les deux navigateurs. Cela consiste à produire des informations sur les capacités multimédias du navigateur et la configuration réseau. C'est notre travail de les envoyer à l'autre navigateur.
Signalisation de la configuration réseau
Pour configurer l'objet RTCPeerConnection, nous devons lui donner des détails sur les serveurs STUN et TURN qu'il utilisera pour découvrir la configuration réseau. Pour cela, nous allons utiliser les nouveaux serveurs Twilio STUN/TURN. La méthode la plus simple consiste à utiliser simplement les serveurs STUN. Ils sont libres et ne nécessitent aucune autorisation. Les iceServers (et les iceCandidates que vous verrez plus tard) font référence au protocole global d'établissement de connectivité interactive qui utilise les serveurs STUN et TURN.
Afin d'obtenir les meilleures chances possibles d'une connexion, nous souhaiterons également utiliser les serveurs TURN. Pour ce faire, nous aurons besoin de demander un token éphémère à Twilio en utilisant le nouveau point de terminaison de tokens qui nous donnera accès aux serveurs TURN à partir de notre front-end JavaScript. Nous allons devoir demander ce token à notre serveur et remettre les résultats au navigateur. Comme nous avons déjà une connexion WebSocket configurée, nous allons l'utiliser. Voici le flux que nous allons utiliser dans la section suivante :
Retournez sur index.js et, dans le rappel de l'événement de connexion du socket, placez le code suivant :
Ici, lorsque le socket reçoit un message de token, il envoie une requête à l'API REST Twilio. Lorsqu'il reçoit le token dans le rappel de la demande, il le retransmet au front-end. Construisons maintenant la partie front-end.
Notre fonction startCall doit maintenant utiliser le socket pour obtenir un token. Nous configurons donc simplement pour écouter un message de token du serveur et en émettre un nous-mêmes.
À présent, nous devons définir la méthode onToken pour initialiser notre RTCPeerConnection avec les iceServers renvoyés par l'API. Cela lance le processus d'obtention de la configuration réseau. Nous devons donc ajouter une fonction de rappel à peerConnection
pour traiter les résultats de cette opération. Il s'agit du rappel onicecandidate
, et il est appelé chaque fois que peerConnection
génère un moyen potentiel de s'y connecter depuis le monde extérieur. En tant que développeur, notre travail consiste à partager ce candidat avec l'autre navigateur. Nous allons donc l'envoyer par la connexion WebSocket.
Sur le serveur, nous devons envoyer ce candidat directement à l'autre navigateur :
Ensuite, nous devons être en mesure de recevoir ces messages dans le front-end, cette fois au nom de l'autre navigateur. Nous avons configuré l'écouteur pour le socket dans la fonction onToken, car c'est à ce moment que nous créons peerConnection
et nous serons prêts à traiter les candidats.
La méthode onCandidate
reçoit le candidat stringify sur le socket, le transforme en RTCIceCandidate
et l'ajoute au peerConnection
du navigateur. Vous vous demandez peut-être où le deuxième navigateur a obtenu un objet peerConnection
puisque nous avons créé cet objet uniquement lorsque l'utilisateur a cliqué sur le bouton « Call » (Appeler) dans le premier navigateur. Vous avez raison de vous poser la question, mais ne vous inquiétez pas, nous allons y répondre très bientôt.
Nous ne pouvons pas encore procéder à un test, car l'objet peerConnection ne commence pas à générer des candidats tant que la partie suivante n'est pas terminée. Nous avançons bien, mais nous devons partager davantage d'informations entre les navigateurs.
Partage de la configuration des éléments multimédias
Dans la dernière section, nous avons défini comment l'initiateur d'appel commence à partager sa configuration réseau. Nous devons maintenant régler le partage des informations multimédias. Les objets peerConnection
de chaque navigateur devront générer des descriptions de leurs capacités multimédia. L'appelant va créer une offre détaillant ces fonctionnalités et l'envoyer via la connexion WebSocket. L'autre navigateur prend cette offre et crée une réponse contenant ses propres capacités et la renvoie à l'appelant. Nous allons mettre en œuvre ce processus ci-dessous, mais voici un diagramme pour illustrer ce qui devrait se passer.
Envoi de l'offre
Commençons ce processus par l'offre. Une fois que nous avons créé l'objet peerConnection
, nous y ajoutons notre localStream
. Cela fait, nous appelons createOffer
sur peerConnection
. Ceci génère la configuration des éléments multimédia et rappelle la fonction transmise. Dans le rappel, nous appelons setLocalDescription
avec l'offre sur peerConnection et nous envoyons l'offre sur le socket à l'autre navigateur. Nous avons également besoin d'un rappel pour les erreurs si createOffer
n'aboutit pas.
Sur le serveur, nous devons à nouveau transmettre ce message.
Réception de l'offre
Ensuite, dans le front-end, nous devons recevoir l'offre. Cette fois-ci, nous allons configurer l'écouteur dans onMediaStream
, car il déclenchera la création de peerConnection
dans l'autre navigateur.
Exécutez cela pour vous assurer que vous êtes sur la bonne voie jusqu'à présent. Redémarrez le serveur et revenez aux fenêtres de navigateur. Actualisez-les, cliquez sur « Get Video » (Obtenir la vidéo) dans les deux et acceptez la demande d'autorisation. Ouvrez une console de développeur dans une fenêtre et cliquez sur « Call » (Appeler) dans l'autre navigateur. Vous devriez voir « Got an offer » (Offre reçue) affiché sur la console, suivi d'une chaîne JSON de l'offre envoyée. L'un des côtés de notre signalisation fonctionne !
Il y a beaucoup d'informations dans l'offre, mais heureusement, nous n'avons pas besoin d'examiner cela en profondeur pour le moment. Elles doivent juste être passées entre les objets peerConnection
dans chaque navigateur. Poursuivez la construction.
À ce stade, nous pourrions passer un appel à VideoChat.startCall
, mais en fin de compte, cela créerait une offre envoyée par le biais du socket au premier navigateur qui reprendrait ce processus en boucle. Ce que nous voulons vraiment faire ici, c'est créer une réponse et la retourner au premier navigateur. Je pense que nous avons besoin d'une refactorisation à ce stade.
Refactorisation
Il nous faut un moyen de créer un objet peerConnection
pour nous-mêmes et de configurer les écouteurs, mais il s'agit de décider si nous créons une offre ou une réponse à envoyer à l'autre navigateur.
Pour ce faire, je vais mettre à jour la fonction onToken
pour prendre une fonction de rappel qui nous permettra de décrire ce qui se passe une fois peerConnection
configuré. Étant donné que onToken
est également utilisé comme rappel, la définition de fonction renvoie désormais une fonction qui deviendra le rappel :
Ainsi, la fonction de rappel remplace notre méthode originale de création de l'offre, pour laquelle nous aurons besoin d'une nouvelle fonction :
Ensuite, nous changeons startCall
pour configurer les rappels comme suit :
Nous pouvons maintenant commencer à définir les fonctions de création d'une réponse.
Dans ce cas, nous voulons utiliser createAnswer
comme rappel pour la création de peerConnection
, mais nous devons également utiliser l'offre pour définir la description distante sur peerConnection
. Cette fois, nous allons créer une clôture en appelant la fonction avec l'offre et renvoyer une fonction à utiliser comme rappel. À présent, lorsque peerConnection
est créé, nous retournons à la fonction interne et transformons l'offre que nous avons reçue via le socket en un objet RTCSessionDescription
et la définissions comme description distante. Nous créons ensuite la réponse sur l'objet peerConnection
, ce qui est à peu près la même chose que lorsque nous avons créé l'offre en premier lieu, et nous l'envoyons à nouveau sur le socket.
Voici comment nous configurons notre fonction onOffer
à présent :
Établissement de la connexion
Maintenant que nous envoyons une réponse sur le socket, il nous suffit de la transmettre à l'appelant d'origine, puis d'attendre que le navigateur fasse son tour de magie.
Dans index.js, nous allons configurer le relais pour la réponse.
Ensuite, nous devons configurer la réception de la réponse dans le navigateur. Nous allons ajouter un autre écouteur au socket lors de la création de peerConnection
et construire la fonction de rappel pour enregistrer la réponse en tant que description distante de peerConnection
.
Les navigateurs transmettent désormais des fonctionnalités multimédia et des informations de connexion entre eux. Il ne reste donc plus qu'une autre chose à faire. Lorsqu'une connexion est établie, peerConnection reçoit un événement onaddstream
avec le flux du média du pair. Il nous suffit de le connecter à notre autre élément <video>
et le chat vidéo sera activé. Nous allons ajouter le rappel onaddstream
dans lequel nous créerons l'élément peerConnection
.
Chargez deux navigateurs l'un à côté de l'autre, ouvrez votre URL de développement sur localhost:3000
sur les deux navigateurs, obtenez le flux vidéo dans les deux navigateurs et cliquez sur « Call » (Appeler) à partir de l'un d'eux. Vous devriez vous voir vous-même. Quatre fois !
La touche finale
Avant de mettre en œuvre le dernier code, résumons ce qui se passe lors de la sélection du bouton « Call » (Appeler) :
- Le client A (le client qui passe l'appel) demande et obtient un token auprès du serveur
- Le client A utilise les informations contenues dans le token pour ajouter les serveurs Twilio ICE à une nouvelle
RTCPeerConnection
- Le client A envoie une offre sur le serveur pour qu'elle soit envoyée à l'autre pair
- Le client A peut maintenant commencer à recevoir des candidats ICE et à les envoyer à l'autre pair via le serveur
- Le client B (le client qui reçoit l'appel), lorsqu'il reçoit une offre, demande et obtient un token auprès du serveur
- Une fois que le client B reçoit un token, il crée une nouvelle
RTCPeerConnection
et une réponse, puis les envoie au client A via le serveur - Le client B peut maintenant enregistrer un rappel pour les candidats des serveurs ICE locaux (
onIceCandidate
) et distants (onCandidate
)
Voyez-vous le problème ici ? À l'étape 4, le client A peut potentiellement envoyer des candidats ICE avant que le client B ne soit prêt à les recevoir (après l'étape 7). Lors de vos tests, cela a fonctionné car les deux clients étaient sur le même localhost
. Cependant, si vous essayez sur deux périphériques se trouvant sur deux réseaux différents, vous verrez que le client B ne peut pas atteindre le client A.
Pour résoudre ce problème (et vous permettre de passer des appels vidéo avec vos amis), nous allons mettre en œuvre un tampon des candidats ICE sur le Client A.
Nous vérifions ici si VideoChat
est connecté avant d'envoyer le candidat au serveur. Si VideoChat
n'est pas connecté, nous l'ajoutons à un tampon ou buffer local (le tableau localICECandidates
).
Mais comment pouvons-nous détecter si VideoChat
est connecté ? Comment le client A sait-il que le client B est prêt à recevoir des candidats ? Sur le client A, cela se produit lorsqu'il reçoit une réponse du client B.
Sur le client B, nous pouvons définir l'état connecté, juste avant de renvoyer la réponse à l'autre pair.
Si vous souhaitez le tester avec une personne sur un autre réseau, vous pouvez exposer votre localhost
sur une adresse publique à l'aide d'un service gratuit tel que ngrok. N'oubliez pas d'utiliser l'URL HTTPS pour vous connecter à l'adresse publique, sinon le navigateur ne pourra pas acquérir les entrées vidéo et audio (ce n'est pas un problème lorsque vous vous connectez au localhost
)
Ce n'est que le début
Il ne s'agit que de la première étape du développement de toutes sortes d'applications WebRTC potentielles. Une fois que vous avez pris le temps de configurer la connexion entre deux navigateurs, libre à vous de décider ce que vous allez en faire. Dans cet exemple, la création d'un moyen pour les utilisateurs de raccrocher, ou la création d'une salle d'attente avec des contrôles de présence beaucoup plus efficaces, peuvent être un début.
Et puis, il y a davantage de choses amusantes que vous pourriez essayer. Vous pourriez transmettre les flux vidéo à une toile pour les modifier, utiliser l'API WebAudio pour changer le son ou encore, utiliser le canal de données (que je n'ai pas abordé dans ce post) pour transmettre toutes les données de votre choix entre les pairs.
Tout le code de ce post, entièrement commenté, est disponible sur GitHub.
J'aimerais beaucoup voir ce que vous allez faire ou ce que vous avez déjà fait avec WebRTC. Contactez-moi sur Twitter ou envoyez-moi un e-mail à philnash@twilio.com.
Articles associés
Ressources connexes
Twilio Docs
Des API aux SDK en passant par les exemples d'applications
Documentation de référence sur l'API, SDK, bibliothèques d'assistance, démarrages rapides et didacticiels pour votre langage et votre plateforme.
Centre de ressources
Les derniers ebooks, rapports de l'industrie et webinaires
Apprenez des experts en engagement client pour améliorer votre propre communication.
Ahoy
Le hub de la communauté des développeurs de Twilio
Meilleures pratiques, exemples de code et inspiration pour créer des expériences de communication et d'engagement numérique.