Réalisez votre PortalGun avec WebRTC
Réalisez votre PortalGun avec WebRTC
Vous avez toujours rêvé d’avoir votre “Portal Gun”* afin de pouvoir imiter Homer Simpson et ainsi pouvoir attraper les bières de votre frigo depuis votre canapé.
Et bien c’est dors et déjà possible ! Enfin presque… vous pourrez surveiller votre frigo mais vous ne pourrez pas attraper ces précieuses bières.
Ce que je vous propose c’est de réaliser des Portails similaires au jeux Portal basés uniquement sur les technologies du Web !
Le projet Portal WebRTC
Voici le rendu de ce qu’on allons réaliser : Le portail bleu voit ce qui se passe dans le portail orange et inversement :
Avant d’attaquer le code, regardons un peu de quoi nous avons besoin :
- Nous devons être capables de voir ce qu’il se passe de l’autre côté du portail
- Un “mur de flammes” entoure notre image
- Nous voulons nous télé-porter de l’autre côté du portail
Voyons maintenant comment nous pouvons répondre à ces différents besoins à travers des technologies du Web :
- WebRTC :il s’agit d’une technologie de visio (mais pas que) et donc c’est idéal pour voir ce qu’il se passe à l’autre côté du portail
- Les canvas nous permettront de jouer efficacement pour simuler de façon performante notre “mur de flammes”.
- La teleportationAPI…. euh non rien ne permet de répondre à ce besoin…
/!\ Ce projet ne marchera que sous Chrome ou Firefox
Architecture du projet
- Chaque ordinateur se trouve sur le réseau et se connecte à un serveur Web uniquement pour afficher le contenu de la page. Le serveur est un serveur NodeJS.
- Ce Serveur expose aussi une WebSocket dont le rôle sera expliqué plus tard dans l’article
- L’échange des données vidéos se fait en “direct” entre les 2 ordinateurs via la technologie WebRTC
WebRTC What ?
WebRTC pour Real Time Communication est une des technologies les plus importantes du projet. Grâce à cette API, on peut faire plusieurs choses :
- Obtenir l’audio et la vidéo.
- Etablir une connexion entre 2 hôtes.
- Communiquer de la vidéo et de l’audio.
- Communiquer d’autres types de données.
Une des forces du webRTC est que les données s’échangent directement entre les 2 ordinateurs et que ces dernières ne passent pas par un serveur ! Pour réussir cet exploit, la technologie WebRTC repose sur 3 APIS web :
- getUserMedia : cette API permet de récupérer les flux vidéos et audios d’un ordinateur
- RTCPeerConnection : cette API permet de faire communiquer des données entre 2 hôtes en tenant compte de tout un ensemble de contraintes telles que l’adresse IP d’une machine, ses codecs, sa connectivité, …
- RTCDataChannel : cette API permet de faire transiter sur une RTCPeerConnection des données textuelles ou binaires.
Pour notre projet, nous n’allons utiliser que les API getUserMedia et RTCPeerConnection.
GetUserMedia
Il s’agit d’une API qui permet de récupérer un ensemble de stream de médias syncrhonisés. Chaque stream peut être vidéo / audio.
var constraints = {video: true}; |
Dans l’exemple ci dessus, nous ne récupérons que la vidéo et nous injectons le résultat de l’appel de getUserMedia dans une balise vidéo.
RTCPeerConnection
La RTCPeerConnection permet de gérer le transport des données. Pour initialiser une RTCPeerConnection, on répond au principe de l’offre et de la demande. Il y a d’une part, une notion d’offre et de demande pour communiquer mais aussi une notion de chemin à emprunter ! Ces 2 notions s’appellent le “Signaling”. Le Signaling a pour objectif de répondre à ces questions :
- Quel type de média et format je supporte ?
- Que puis-je envoyer ?
- Quel est mon type d’infrastructure réseau ?
Pour faire cette étape, il suffit juste de trouver un moyen de passer ces informations à l’hôte distant. Une des technologies préconisées pour faire le signaling est “les WebSockets”. C’est donc ici qu’interviendra notre serveur de websockets
Voici comment se déroule le signaling :
Gestion de l’offre
- Alice appelle la méthode createOffer()
- Dans le callback, Alice appelle setLocalDesctiption()
- Alice sérialise l’offre et l’envoie à Eve
- Eve appelle la méthode setRemoteDescription() avec l’offre
- Eve appelle la méthode createAnswer()
- Eve appelle la méthode setLocalDescription() avec la réponse envoyée à Alice
- Alice reçoit la réponse et appelle setRemoteDescription()
Gestion du chemin Ice Candidate (ICE pour Interactive Connectivity Establishement)
- Alice & Eve ont leur RTCPeerConnection
- En cas de succès de chaque côté les IceCanditates sont envoyées
- Alice sérialise ses IceCandidates et les envoie à Eve
- Eve reçoit les IceCandidates d’Alice et appelle addIceCandidate()
- Eve sérialise ses IceCandidates et les envoie à Alice
- Alice reçoit les IceCandidates d’Eve et appelle addIceCandidate()
- Les 2 savent comment communiquer.
Plus d’infos
Si vous souhaitez plus d’information sur le WebRTC :
- http://www.html5rocks.com/en/tutorials/webrtc/basics/
- http://www.html5rocks.com/en/tutorials/webrtc/datachannels/
Retour au Projet Portal
Après cette rapide introduction sur la technologie WebRTC. Nous allons maintenant nous intéresser à notre projet et nous allons voir comment réaliser notre “Portal”.
Comme tout bon projet, je me suis inspiré de ce que je trouvais sur le net afin de gagner du temps. Ainsi, plutôt que de vous noyer sous des montagnes de code compréhensible et/ou incompréhensible. Je vous donnerais plutôt les deltas que j’ai effectué et pourquoi je les ai fait.
Etape 1 : cloner les projets références
La base du projet WebRTC repose sur le codelab initialisé par Sam Dutton : ingénieur chez Google et travaillant sur l’implémentation de WebRTC dans Chrome : https://bitbucket.org/webrtc/codelab. De façon plus précise notre point de départ sera le step7 de ce codelab.
La base graphique des flammes repose sur le projet de Chris Longo : https://github.com/chrislongo/html5-canvas-demo
Nous allons donc commencer par cloner les 2 projets afin de récupérer une base de code propre et fonctionnelle que nous allons nettoyer petit à petit pour coller avec notre besoin.
Etape 2 : création du squelette de l’application
L’application est structurée comme suit :
- assets/ : fichiers externes
- fonts/ : les fonts spéciales utilisées pour le projet
- images/ : les ressources graphiques utilisées pour le projet
- css/ : le style de notre page
- js/ : les fichiers javascripts utilisés par le projet
- package.json : fichier des dépendances node utilisées pour le serveur node
- server.js : le serveur nodeJS
- index.html : notre application
Téléchargement des ressources annexes
- Vous trouverez la font ici : http://fontmeme.com/freefonts/34868/portal.font
- Les images utilisées dans le projet sont disponible aux urls suivantes :
- Image du BugDroid Portal : https://goo.gl/vT6svL
- Image du footer : https://goo.gl/J6CknB
- Image du header : https://goo.gl/AQnIoo
- Image du logo WebRTC : https://goo.gl/XeimoA
Etape 3 : Ecriture du Serveur
Comme expliqué précédemment, nous allons baser notre travail sur le step7 du codelab.
./package.json
{ |
./server.js
Nous reprenons le serveur tel qu’il est dans le codelab
Ce serveur fait donc 2 choses :
- Dans un premier temps, on va définir un serveur http pour servir notre contenu html
- On créé un serveur de webSockets afin d’assurer la partie “Signaling”. Un message transféré au serveur sera automatiquement partagé à l’autre client.
Il ne nous reste plus à qu’à récupérer les modules node avec l’instruction :
npm install |
De cette manière, les dépendances nodes seront téléchargées dans notre projet
Etape 4 : Ecriture du projet WebRTC
Nous allons poser le style graphique de notre application :
./assets/fonts/stylesheet.css
@font-face { |
./css/main.css
body{ |
index.html
|
./js/lib/adapter.js
Ce fichier doit être copié tel quel depuis le codelab car il s’agit de la classe Polyfill qui permet d’uniformiser l’API WebRTC entre Chrome & Firefox
./js/canvasFire.js
Initialiser ce fichier à vide afin d’avoir l’import depuis le fichier html qui fonctionne
./js/app.js
Nous allons partir du fichier issu du step7 : ./js/main.js.
Copiez l’intégralité du fichier et nous allons retirer ce qui ne nous intéresse pas.
LocalVideo
Dans notre projet, nous ne sommes pas intéressé pour afficher le retour de notre webCam à l’écran, nous allons donc supprimer toutes les références à cet élément.
var localVideo = document.querySelector('#localVideo'); |
et
attachMediaStream(localVideo, stream); |
dans la fonction handleUserMedia(stream)
DataChannel
De la même façon tout ce qui concerne le DataChannel ne nous sert pas
var sendChannel; |
Au début du projet. Ensuite
var pc_constraints = { |
est à remplacer par :
var pc_constraints = { |
Et aussi il faut supprimer tout ce qui suit qui se situe au niveau de la fonction createPeerConnection
if (isInitiator) { |
Et enfin pour finir
var constraints = {'optional': [], 'mandatory': {'MozDontOfferDataChannel': true}}; |
de la fonction doCall() est à remplacer par :
var constraints = {'optional': [], 'mandatory': {}}; |
Tester
Nous pouvons à présent tester notre application pour vérifier que la vidéo passe bien à travers l’API WebRTC. Pour ce faire, il suffit simplement de lancer notre serveur à l’aide de la commande :
node server.js |
Notre serveur tourne sur le port 2013. Il faut donc entrer dans notre navigateur l’url : http://localhost:2013. Il est très important d’accepter le partage de vidéo sinon cela ne pourra pas fonctionner.
A ce moment là, vous devriez avoir un écran noir. En effet, comme nous ne faisons pas de retour visuel de notre propre caméra, nous devons ouvrir un deuxième onglet sur la même url pour vérifier le bon fonctionnement. Cependant, il y a une deuxième raison pour laquelle nous ne voyons rien, la balise video remoteVideo a un style ‘display:none’. Il faudra supprimer ce display: none le temps du test.
Si tout se passe bien, vous devriez avoir une vidéo sur les 2 onglets correspondant au rendu de votre webcam. Pour chaque tests futurs, je vous conseille de fermer les 2 onglets à chaque fois car le serveur Node stocke le nombre de clients connectés et la limite a été fixée à 2 clients maximum !
Etape 5 : Ajout du mur de flammes
Maintenant que nous nous sommes occupés de la partie WebRTC, nous allons ajouter un peu de graphisme à tout ça. Pour le moment, notre flux WebRTC arrive directement dans une balise vidéo, mais il se trouve que les canvas et les vidéos marchent très bien ensemble ! En effet, nous allons faire des snapshots de notre balise vidéo que nous allons injecter dans un canvas et ainsi pouvoir commencer à jouer plus sérieusement avec des effets graphiques.
Nous allons donc avoir
- 1 balise vidéo en “display:none”
- 1 canvas restituant la vidéo mais avec un masque
- 1 canvas affichant le mur de flamme.
display:none
Pour ce faire, il suffit simplement de faire en sorte que dans notre html, nous ayons le code suivant :
<video id='remoteVideo' autoplay muted style="display:none;"></video> |
canvas avec la vidéo
Nous allons maintenant ajouter à notre application (app.js) l’affichage du canvas qui recevra la vidéo.
var canvasRemoteElement = document.querySelector('#canvasRemoteVideo'); |
Nous utilisons simplement la possibilité de dessiner dans un canvas une image d’une vidéo
Mur de flamme
Vous devez copier le contenu du fichier canvas.js du projet html5-canvas-demo dans notre fichier ./js/canvasFire.js
Nous allons maintenant afficher le mur dans un canvas. Nous devons donc éditer notre fichier app.js
var canvasFireElement = document.querySelector('#canvasFireLocalVideo'); |
Nous allons aussi modifier la méthode snapshot afin d’y intégrer toute la partie flammes
var init = false; |
Le code ajouté nous permet d’initialiser graphiquement le canvas et de demander de piloter les rafraîchissements du mur de flammes à partir de notre application. Nous allons donc devoir faire une modification dans le fichier canvasFire.js : Nous ajoutons une méthode rafraîchissement et nous supprimons l’appel au requestAnimationFrame :
this.refresh = function(){ |
Etape 6 : Ajouter un cercle de feux
Principe
Le principe pour le mur de flamme est simple : Le projet html5-canvas-demo nous fournit un seul mur de flammes, hors nous, nous voulons 1 cercle. Nous allons nous y prendre en plusieurs étapes.
- Nous allons créer un mur de flammes sur chacun des axes cardinaux. De cette façon nous aurons des flammes partout autour de notre image
- Nous allons ensuite mettre en place un masque ovale afin de restreindre la zone affichant les flammes
- Nous allons faire tourner ces flammes pour leur donner plus de mouvement et se rapprocher du rendu du jeu Portal.
- Enfin, nous allons autoriser une deuxième couleur car chaque portail possède sa propre couleur (bleu et orange)
Pour rappel, le projet initial fonctionne de la façon suivante : A chaque fois qu’il peut dessiner (window.requestAnimationFrame), on dessine une image de particules de flammes avec la méthode drawImage du context du canvas.
Flammes selon les axes cardinaux
Afin d’afficher les flammes selon les axes nous allons créer une fonction qui nous permet d’afficher pour un angle donné une image de flamme dans le fichier canvasFire.js.
var drawAngle = function(angleDegree){ |
Cette méthode fait donc une rotation du context du canvas avant de dessiner la flamme. Une des choses importante dans cette méthode est le retour à la position initiale ! C’est très important pour pouvoir traiter un nouvel angle !
Nous devons maintenant appeler cette méthode là où le drawImage d’origine était effectué à savoir dans la méthode “draw”. Le contenu de cette fonction sera donc maintenant le suivant :
// draw colormap->palette values to screen |
Il faut aussi ajouter les dimensions de l’image destination afin de calculer les bons angles.
var dims = {}; |
Il faut enfin modifier la façon d’afficher les pixels afin de forcer un affichage de pixels transparents ! En effet, actuellement seul le dernier canvas est affiché. Il faut donc modifier la fonction drawPixel comme suit :
// set pixels in imageData |
Comme nous avons changé la méthode init de canvasFire.js. Nous devons donc changer l’appel à cette méthode dans app.js. Ainsi dans la méthode snapshot, il faut remplacer :
canvasDemo.init(); |
par
canvasDemo.init({width :minVideoWidth, height : idealHeight + 100}); |
On constate qu’on va bien afficher 4 murs de flammes autour de notre vidéo
Mise en place des masques
Dans l’application finale, la vidéo et les flammes sont dans un ovale. Nous allons utiliser pour ce faire une fonction des canvas : clip. Cette dernière nous permettra de définir une zone ovale dans le canvas et nous y dessinerons notre vidéo et nos flammes. Plus précisément, nous allons dessiner un ovale de flammes et par dessus, nous dessinerons un ovale contenant l’image de la vidéo. Voici donc la version presque finale de la méthode snaphot dans le fichier app.js
function snapshot(){ |
et il faut donc ajouter la fonction
function drawEllipse(ctx, x, y, w, h) { |
On peut donc voir que l’on dessine des Ellipses dans lesquelles nous faisons nos affichages de flammes suivit de l’affichage de la vidéo
Rotation des flammes
La gestion de la rotation des flammes se fait dans le fichier canvasFire.js. Tout à l’heure nous avons définit l’affichage du mur de flammes selon 4 axes cardinaux. Nous allons donc simplement faire évoluer ces angles au fil du temps pour mettre en place la rotation.
Commençons par ajouter de nouvelles variables globales :
var timeOut = 7; |
et nous allons simplement mettre à jour le contenu de la fonction draw pour faire évoluer les angles :
// draw colormap->palette values to screen |
Maintenant notre cercle de flamme évolue en tournant
Ajout d’une autre couleur
Afin de compléter comme il se doit notre portal, nous devons donc ajouter une deuxième couleur ! Dans le fichier canvasFire.js la définition de la couleur utilisée se fait dans la méthode initPalette();
// init palette from warm to white hot colors |
Et nous allons donc modifier son appel dans la méthode init et ajouter la variable globale color.
var color = 'red'; |
Comme vous le constatez, nous avons fait évolué la signature de la méthode init. Ceci veut dire que nous allons faire notre ultime modification dans le fichier app.js dans la méthode snapshot(). Nous allons remplacer :
canvasDemo.init({width :minVideoWidth, height : idealHeight + 100}); |
par :
canvasDemo.init(isInitiator ? 'red' : 'blue', {width :minVideoWidth, height : idealHeight + 100}); |
Nous en avons fini avec le code !
Etape 7 : Fin
Il ne vous reste plus qu’à projeter le résultat de l’application sur 2 murs différents avec une webcam de chaque côté pour filmer le tout !
Crédits :
Tout le code source est disponible ici : https://github.com/GDG-Nantes/portal-devfest-2013