L’API a changé depuis, j’ai donc mis à jour l’article pour refléter ces changements
Vu que dernièrement je vous ai bien gavé avec WAMP, ça mérite un tuto non ?
Il se trouve que l’équipe derrière WAMP a publié plus tôt que prévu une version de leurs libs contenant l’API flaskesque sur laquelle on bosse. L’idée est que même si on n’a pas encore les tests unitaires, on peut déjà jouer avec.
Maintenant il me fallait un projet sexy, histoire de donner envie. Donc j’ai fouillé dans ce qui se faisait côté temps réel (essentiellement du NodeJS et du Tornado, mais pas que) pour trouver l’inspiration.
Et j’ai trouvé un truc très sympa : un player vidéo piloté à distance.
En effet, n’est-il pas chiant de regarder une vidéo en ligne sur son ordi posé sur la commode pendant qu’on est enfoncé dans le canap ? Si on veut faire pause ou changer le son, il faut se lever, arg.
Les problèmes du tiers monde, c’est du pipi de chat à côté. Ils ont de la chance, eux, ils ne connaissent pas le streaming.
Voici donc le projet :
Une page avec un player HTML 5 et un QR code.
Si on scanne le QR code avec son téléphone, il vous envoie sur une page avec une télécommande pour contrôler le player sans bouger votre cul :
Et vous allez voir, c’est même pas dur à faire.
Démo en ligne:
Vous pouvez télécharger le code ici.
Pour comprendre ce qui va suivre, il va vous falloir les bases en prog Javascript et Python, ainsi que bien comprendre la notion de callback. Être à l’aise avec promises peut aider.
Et pour bien digérer ce paté, rien ne vaut un peu de son :
Le Chteumeuleu
Il va nous falloir deux pages Web, une pour le player vidéo, et une pour la télécommande.
Le player :
<!DOCTYPE html> <html> <head> <title>Video</title> <meta charset='utf-8'> <!-- Chargement des dépendances : autobahn pour WAMP et qrcode pour générer le QR code. Bien entendu, je vous invite à ne pas les hotlinker dans vos projets, mais pour la démo c'est plus simple. --> <script src="https://autobahn.s3.amazonaws.com/autobahnjs/latest/autobahn.min.jgz" type="text/javascript"></script> <script src="http://davidshimjs.github.com/qrcodejs/qrcode.min.js" type="text/javascript"></script> <style type="text/css"> #vid { /* Taille de la video */ width:427px; height:240px; } /* Centrage avec la méthode Rache */ #container { width:427px; margin:auto; } #ctrllink { display:block; width:256px; margin:auto; } </style> </head> <body> <div id="container"> <p> <!-- Pareil, je hotlink la video, mais ne faites pas ça à la maison les enfants. Surtout que les perfs du serveur du W3C sont merdiques et ça bufferise à mort. --> <video id="vid" class="video-js vjs-default-skin" controls preload="auto" poster="http://media.w3.org/2010/05/sintel/poster.png" > <source id='ogv' src="http://media.w3.org/2010/05/sintel/trailer.ogv" type='video/ogg'> <source id='mp4' src="http://media.w3.org/2010/05/sintel/trailer.mp4" type='video/mp4'> <source id='webm' src="http://media.w3.org/2010/05/sintel/trailer.webm" type='video/webm'> </video> </p> <p> <a id="ctrllink" href="#" target="_blank"> <span id="qrcode"></span> </a> </p> </div> </body> |
Et la télécommande :
<!DOCTYPE html> <html> <head> <title>Télécommande</title> <meta charset='utf-8'> <script src="https://autobahn.s3.amazonaws.com/autobahnjs/latest/autobahn.min.jgz" type="text/javascript"></script> <!-- Zoom du viewport sur mobile pour éviter d'avoir à le faire à la main. --> <meta name="viewport" content="width=device-width, initial-scale=1"> <style type="text/css"> #controls { width:350px; margin:auto; } #controls button { font-size: 1em; } #controls input { vertical-align:middle; width: 200px; height:20px; } </style> </head> <body> <p id="controls"> <!-- Marrant de se dire qu'en 2000, le JS inline était considéré comme démoniaque, et maintenant avec angularjs et cie, c'est exactement ce que tout le monde fait... Bref, on attache le clic sur nos contrôles à des méthodes de notre objet qui va se charger de la logique. --> <button id="play" onclick="control.togglePlay()">Play</button> <input id="volume" onchange="control.volume(this.value)" type="range"> </p> </body> |
Rien d’incroyable. C’est du HTML, un peu de CSS, on charge les dépendances en JS. Classique.
Vu qu’on utilise des ressources hotlinkées par souci de simplicité, il vous faudra être connecté à Internet.
Setup du routeur
On va travailler avec Python 2.7 puisque Crossbar.io est uniquement en 2.7 et que je n’ai pas envie de vous faire installer deux versions de Python juste pour le tuto.
Il nous faut avant tout un serveur HTTP pour servir les fichiers HTML et un routeur WAMP. On installe donc Crossbar.io :
pip install crossbar |
Ca va aussi installer autobahn, twisted et tout le bordel.
On va ensuite dans le dossier qui contient ses fichiers HTML, et on créé le fichier de config de Crossbar.io avec un petit :
crossbar init |
Vous noterez la création d’un dossier .crossbar
qui contient un fichier config.json
. C’est la config de crossbar. Videz moi ce fichier, on va le remplir avec notre config :
{ "workers": [ { "type": "router", "realms": [ { "name": "realm1", "roles": [ { "name": "anonymous", "permissions": [ { "uri": "*", "publish": true, "subscribe": true, "call": true, "register": true } ] } ] } ], "transports": [ { "type": "web", "endpoint": { "type": "tcp", "port": 8080, "interface": "0.0.0.0" }, "paths": { "/": { "type": "static", "directory": ".." }, "ws": { "type": "websocket", } } } ] } ] } |
Crossbar est en effet un gestionnaire de processus : il ne gère vraiment rien lui même. Il démarre d’autres processus, appelés workers, à qui il délègue le travail.
On définit dans ce fichier de config quels processus (les workers) lancer quand Crossbar.io démarre. Les valeurs qu’on utilise disent de créer un seul worker de type “router”, c’est à dire un worker capable de gérer les entrées et les sorties WAMP. Hey oui, le routeur n’est qu’un worker comme les autres :)
Il y a d’autres sortes de workers, mais aujourd’hui on s’en branle.
Dans notre config du worker router, on crée d’abord un realm, qui est juste un namespace avec des permissions. Si un client WAMP se connecte à ce routeur, il doit choisir un realm (qui est juste un nom), et il ne peut parler qu’avec les clients du même realm. C’est une cloture quoi.
Dans un realm, on définit des roles qui déclarent quelles opérations PUB/SUB et RPC on a le droit de faire. Ici on dit que tout le monde (anonymous) a le droit de tout faire sur toutes les urls (“uri”: ‘*”) histoire de pas se faire chier. Si on met en prod, évidement on va se pencher sur la sécurité et faire ça plus proprement.
"realms": [ { "name": "realm1", "roles": [ { "name": "anonymous", "permissions": [ { "uri": "*", "publish": true, "subscribe": true, "call": true, "register": true } ] } ] } ], |
Puis on définit les transports, c’est à dire sur quoi notre worker va ouvrir ses oreilles pour écouter les messages entrant :
"transports": [ { "type": "web", "endpoint": { "type": "tcp", "port": 8080, "interface": "0.0.0.0" }, "paths": { "/": { "type": "static", "directory": ".." }, "ws": { "type": "websocket", } } } ] |
Encore une fois on en déclare un seul, de type “web”. Ce transport peut écouter HTTP et Websocket sur le même port. On lui dit d’écouter sur “0.0.0.0:8080″ :
“endpoint”: {
“type”: “tcp”,
“port”: 8080,
“interface”: “0.0.0.0”
},
Ensuite on dit que si quelqu’un arrive sur “/”, on sert en HTTP les fichiers statiques histoire que nos pages Web soient servies :
"/": { "type": "static", "directory": ".." }, |
Si on arrive sur “/ws”, on route les requêtes WAMP via Websocket :
"ws": { "type": "websocket", } |
Le routeur est prêt, on lance Crossbar.io :
$ crossbar start 2015-01-07 20:02:55+0700 [Controller 26914] Log opened. 2015-01-07 20:02:55+0700 [Controller 26914] ============================== Crossbar.io ============================== 2015-01-07 20:02:55+0700 [Controller 26914] Crossbar.io 0.9.12-2 starting 2015-01-07 20:02:55+0700 [Controller 26914] Running on CPython using EPollReactor reactor 2015-01-07 20:02:55+0700 [Controller 26914] Starting from node directory /home/sam/Work/sametmax/code_des_articles/2014/juin/video_remote/.crossbar 2015-01-07 20:02:55+0700 [Controller 26914] Starting from local configuration '/home/sam/Work/sametmax/code_des_articles/2014/juin/video_remote/.crossbar/config.json' 2015-01-07 20:02:55+0700 [Controller 26914] Warning, could not set process title (setproctitle not installed) 2015-01-07 20:02:55+0700 [Controller 26914] Warning: process utilities not available 2015-01-07 20:02:55+0700 [Controller 26914] No WAMPlets detected in enviroment. 2015-01-07 20:02:55+0700 [Controller 26914] Starting Router with ID 'worker1' .. 2015-01-07 20:02:55+0700 [Controller 26914] Entering reactor event loop ... 2015-01-07 20:02:55+0700 [Router 26917] Log opened. 2015-01-07 20:02:55+0700 [Router 26917] Warning: could not set worker process title (setproctitle not installed) 2015-01-07 20:02:55+0700 [Router 26917] Running under CPython using EPollReactor reactor 2015-01-07 20:02:56+0700 [Router 26917] Entering event loop .. 2015-01-07 20:02:56+0700 [Router 26917] Warning: process utilities not available 2015-01-07 20:02:56+0700 [Controller 26914] Router with ID 'worker1' and PID 26917 started 2015-01-07 20:02:56+0700 [Controller 26914] Router 'worker1': realm 'realm1' started 2015-01-07 20:02:56+0700 [Controller 26914] Router 'worker1': role 'role1' started on realm 'realm1' 2015-01-07 20:02:56+0700 [Router 26917] Site starting on 8080 2015-01-07 20:02:56+0700 [Controller 26914] Router 'worker1': transport 'transport1' started |
Setup du client
Pour cette démo, le serveur n’a pas grand chose à faire. On pourrait en fait la faire sans aucun code Python, mais ça va nous simplifier la vie et donner un peut de grain à moudre pour le tuto.
En effet, on a deux problématiques que le serveur va résoudre facilement pour nous : créer un ID unique pour le player et récupérer l’IP sur le réseau local.
L’ID, c’est simplement que si plusieurs personnes lancent en même temps un player, on ne veut pas que les télécommandes puissent lancer un ordre à un autre player que le sien. On pourrait utiliser un timestamp, mais ils sont contigus, n’importe quel script kiddies pourrait faire un script pour foutre la merde. On va donc créer un ID unique qui ne soit pas facilement prévisible. Javascript n’a rien pour faire ça en natif, et c’est un peu con de charger une lib de plus pour ça alors que Python peut le faire pour nous.
L’IP, c’est parce qu’il faut donner l’adresse de notre machine contient notre routeur. Et le téléphone qui sert de télécommande doit se connecter à ce routeur. Il faut donc qu’il connaisse l’adresse de celui-ci, donc on va la mettre dans notre QR code.
Cela veut dire aussi que le téléphone doit être sur le même réseau local pour que ça fonctionne. Donc mettez votre téléphone en Wifi, pas en 3G.
Voilà ce que donne notre code WAMP côté serveur :
# -*- coding: utf-8 -*- from autobahn.twisted.wamp import Application import socket import uuid # Comme pour flask, l'objet app # est ce qui lie tous les éléments # de notre code ensemble. On lui donne # un nom, ici "demo" app = Application('demo') # Bien que l'app va démarrer un serveur # pour nous, l'app est bien un CLIENT # du serveur WAMP. Le serveur démarré # automatiquement n'est qu'une facilité # pour le dev. En prod on utiliserait # crossbar. # Juste un conteneur pour y mettre notre IP app._data = {} # On déclare que cette fonction sera appelée # quand l'app se sera connectée au serveur WAMP. # Ceci permet de lancer du code juste après # le app.run() que l'on voit en bas du fichier. # '_' est une convention en Python pour dire # "ce nom n'a aucune importance, c'est du code # jetable qu'on utilisera une seule fois". @app.signal('onjoined') def _(): # On récupère notre adresse IP sur le réseau local # C'est une astuce qui demande de se connecter et donc # à une IP externe, on a besoin d'une connexion internet. s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.connect(("8.8.8.8", 80)) # On stocke l'adresse IP locale dans un conteneur # qui sera accessible partout ailleur. app._data['LOCAL_IP'] = s.getsockname()[0] s.close() # On déclare que la fonction "ip()" est appelable # via RCP. Ce qui veut dire que tout autre client # WAMP peut obtenir le résultat de cette fonction. # Donc on va pouvoir l'appeler depuis notre navigateur. # Comme notre app s'appelle "demo" et notre fonction # s'appelle "ip", un client pourra l'appeler en faisant # "demo.ip". @app.register() def ip(): # On ne fait que retourner l'IP locale. Rien de fou. return app._data['LOCAL_IP'] # Je voulais appeler cette fonction distante "uuid", mais ça # override le module Python uuid. Ce n'est pas une bonne # idée. Je l'appelle donc 'get_uuid' mais je déclare le # namespace complet dans register(). Un client WAMP pourra donc # bien l'appeler via "demo.uuid". # Notez que ce namespace doit toujours s'écrire # truc.machine.bidule. Pas truc/machin ou truc:machin. # ou truc et bidule.MACHIN. @app.register('demo.uuid') def get_uuid(): # Retourne un UUID, sans les tirets. # ex: b27f7e9360c04efabfae5ac21a8f4e3c return str(uuid.uuid4()).replace('-', '') # On lance notre client qui va se connecter au # routeur. if __name__ == '__main__': app.run(url="ws://127.0.0.1:8080/ws") # On ne peut rien mettre comme code ici, il faut le # mettre dans @app.signal('onjoined') si on veut # entrer du code après que l'app soit lancée. |
Et on lance notre app dans un autre terminal:
python app.py |
Nous avons maintenant Crossbar.io qui tourne d’une console, et le client Python qui tourne dans une seconde console, connecté au routeur.
Le lecteur vidéo
Il nous faut maintenant définir le comportement de notre lecteur vidéo, un client WAMP Javascript. Il s’agit essentiellement de se connecter au serveur WAMP, et d’échanger des messages via RPC ou PUB/SUB :
var player = {}; var url; /* On va utiliser du pur JS histoire de pas mélanger des notions de jQuery dans le tas. Je ne vais PAS utiliser les best practices sinon vous allez être noyés dans des détails */ /* Lancer le code une fois que la page est chargée */ window.addEventListener("load", function(){ /* Connexion au serveur WAMP. J'utilise les valeurs par défaut du serveur de dev. On ouvre explicitement la connection à la fin du script. */ var connection = new autobahn.Connection({ url: 'ws://' + window.location.hostname + ':8080/ws', realm: 'realm1' }); /* Lancer ce code une fois que la connexion est réussie. Notez que je ne gère pas les erreurs dans dans une APP JS, c'est un puits sans fond. */ connection.onopen = function (session) { /* Appel de la fonction ip() sur le serveur */ session.call('demo.ip') /* Une fois qu'on a récupéré l'IP, on peut fabriquer l'URL de notre projet et on appelle la fonction get_uuid() du serveur */ .then(function(ip){ url = 'http://' + ip + ':8000'; return session.call('demo.uuid'); }) /* Une fois qu'on a l'UUID, on peut commencer à gérer la partie télécommande */ .then(function(uuid){ /* Création du QR code avec le lien pointant sur la bonne URL. On met l'ID dans le hash. */ var controlUrl = url + '/control.html#' + uuid; var codeDiv = document.getElementById("qrcode"); new QRCode(codeDiv, controlUrl); var ctrllink = document.getElementById("ctrllink"); ctrllink.href = controlUrl; /* Notre travail consiste essentiellement à manipuler cet élément */ var video = document.getElementById("vid"); /* On attache déclare 4 fonctions comme étant appelable à distance. Ces fonctions sont appelables en utilisant le nom composé de notre ID et de l'action qu'on souhaite faire. Ex: 'b27f7e9360c04efabfae5ac21a8f4e3c.play' pour appeler "play" sur notre session. */ session.register(uuid + '.play', function(){ video.play(); }); session.register(uuid + '.pause', function(){ video.pause(); }); session.register(uuid + '.volume', function(val){ video.volume = val[0]; }); session.register(uuid + '.status', function(val){ return { 'playing': !video.paused, 'volume': video.volume }; }); /* Quelqu'un peut très bien appuyer sur play directement sur cette page. Il faut donc réagir si l'utilisateur le fait, publier un événement via WAMP pour permettre à notre télécommande de se mettre à jour */ video.addEventListener('play', function(){ /* On publie un message indiquant que le player a recommencé à lire la vidéo. */ session.publish(uuid + '.play'); }); video.addEventListener('pause', function(){ session.publish(uuid + '.pause'); }); video.addEventListener('volumechange', function(){ session.publish(uuid + '.volume', [video.volume]); }); }); }; /* Ouverture de la connection une fois que tous les callbacks sont bien en place.*/ connection.open(); }); |
Code de la télécommande
La télécommande est notre dernier client WAMP (on peut avoir plein de clients WAMP, ne vous inquiétez, ça tient 6000 connections simultanées sur un tout petit Raspberry PI).
Son code a pour but d’envoyer des ordres au player HTML5, mais aussi de mettre à jour son UI si le player change d’état.
/* L'objet qui se charge de la logique de nos controles play/pause et changement de volume. Rien de fou, il change l'affichage du bouton et du slider selon qu'on est en pause/play et la valeur du volume. */ var control = { playing: false, setPlaying: function(val){ control.playing = val; var button = window.document.getElementById('play'); if (!val){ button.innerHTML = 'Play' } else { button.innerHTML = 'Pause'; } }, setVolume: function(val){ var slider = window.document.getElementById('volume'); slider.value = val; } }; window.onload = function(){ var connection = new autobahn.Connection({ url: 'ws://' + window.location.hostname + ':8080/ws', realm: 'realm1' }); connection.onopen = function (session) { /* Récupération de l'ID dans le hash de l'URL */ var uuid = window.location.hash.replace('#', ''); /* Mise à jour des controles selon le status actuel du player grace à un appel RPC vers notre autre page. */ session.call(uuid + '.status').then(function(status){ control.setPlaying(status['playing']); control.setVolume(status['volume']) /* On attache l'appui sur les contrôles à un appel de la fonction play() sur le player distant. L'uuid nous permet de n'envoyer l'événement que sur le bon player. */ control.togglePlay = function() { if (control.playing){ session.call(uuid + '.pause'); control.setPlaying(false); } else { session.call(uuid + '.play'); control.setPlaying(true); } }; control.volume = function(val){ session.call(uuid + '.volume', [val / 100]); }; /* On ajoute un callback sur les événements de changement de status du player. Si quelqu'un fait play/pause ou change le volume, on veut mettre à jour la page. */ session.subscribe(uuid + '.play', function(){ control.setPlaying(true); }); session.subscribe(uuid + '.pause', function(){ control.setPlaying(false); }); session.subscribe(uuid + '.volume', function(val){ control.setVolume(val[0] * 100); }); }); }; connection.open(); }; |
En résumé
Voici à quoi ressemble le projet final :
. ├── app.py ├── control.html ├── .crossbar │ └── config.json └── index.html
Pour ce projet, on aura utilisé :
- WAMP: le protocole qui permet de faire communiquer en temps réel des parties d’application via RPC et PUB/SUB.
- Autobahn.js: une lib pour créer des clients WAMP en javascript.
- Autobahn.py: une lib pour créer des clients WAMP en Python.
- Crossbar.io: un routeur WAMP.
Il y a pas mal de notions à prendre en compte.
D’abord, le RPC.
Cela permet à un client de dire “les autres clients peuvent appeler cette fonction à distance”. On l’utilise pour exposer ip()
et get_uuid()
sur notre serveur et notre Javascript peut donc les appeler. Mais on l’utilise AUSSI pour qu’une des pages (le player) expose play()
, pause()
et volume()
et que l’autre page (notre télécommande) puisse les utiliser.
La grosse différence, c’est que ip()
peut être appelé par tous les clients en utilisant “demo.ip” alors que play()
ne peut être appelé que par les clients qui connaissent l’ID du player, puisqu’il faut utiliser “<id>.play”.
Ensuite, il y a le PUB/SUB.
Cela permet à un client de dire “j’écoute tous les messages adressés à ce nom”. Et un autre client peut envoyer un message (on appelle ça aussi un événement, c’est pareil) sur ce nom, de telle sorte que tous les clients abonnés le reçoivent.
On l’utilise pour que notre télécommande dise “j’écoute tous les messages qui concernent les changements de status du player.” De l’autre côté, quand on clique sur un contrôle du player, on envoie un message précisant si le volume a changé, ou si on a appuyé sur play/pause. La télécommande peut ainsi mettre son UI à jour et refléter par exemple, la nouvelle valeur du volume.
Cela résume bien les usages principaux de ces deux outils :
- RPC permet de donner un ordre ou récupérer une information.
- PUB/SUB permet de (se) tenir au courant d’un événement.
Voici le workflow de notre projet :
- On lance un serveur WAMP.
- On connecte des clients dessus (du code Python ou Js dans notre exemple).
- Les clients déclarent les fonctions qu’ils exposent en RPC et les événements qu’ils écoutent en PUB/SUB.
- Ensuite on réagit aux actions utilisateurs et on fait les appels RPC et les publications PUB/SUB en conséquence.
Si vous virez tous les commentaires, vous verrez que le code est en fait vraiment court pour une application aussi complexe.
Encore une fois, il est possible de le faire sans WAMP, ce sera juste plus compliqué. Je vous invite à essayer de le faire pour vous rendre compte. Avec PHP, Ruby ou une app WSGI, c’est pas marrant du tout. Avec NodeJs, c’est plus simple, mais il faut quand même se taper la logique de gestion RPC et PUB/SUB à la main ou installer pas mal de libs en plus.
WAMP rend ce genre d’app triviale à écrire. Enfin triviale parce que là j’ignore tous les edge cases, évidemment. Pour un produit solide, il faut toujours suer un peu.
Les limites du truc
C’est du Python 2.7. Bientôt on pourra le faire avec asyncio et donc Python 3.4, mais malheureusement sans le serveur de dev.
Heureusement, Twisted est en cours de portage vers Python 3, et donc tout finira par marcher en 3.2+.
C’est du HTML5, mais bien entendu, rien ne vous empêche de faire ça avec du Flash si ça vous amuse.
C’est du WebSocket, mais on peut utiliser un peu de Flash pour simuler WebSocket pour les vieux navigateurs qui ne le supportent pas.
Non, la vraie limite c’est encore la jeunesse du projet : pas d’autoreload pour le serveur (super chiant de devoir le faire à la main à chaque fois qu’on modifie le code) et les erreurs côté serveur se lisent dans la console JS, et pas dans le terminal depuis lequel on a lancé le serveur. Plein de petits détails comme ça.
Merci beaucoup Sam.
Je ne suis pas encore en mesure de tester
ton exemple.
Mais j’ai juste une question, un peu stupide sans doute, pourquoi ne pas utiliser directement crossbar ?
Est-ce uniquement une question d’installation, ou y a t-il un réel avantage à utiliser le serveur de dev ?
D’autre part, pourras-tu, à l’occasion, poster l’exemple pour mise en prod ?
Cordialement
Tain chapeau les gars pour tous les articles que vous postez. Y’a des fois ou vous êtes pas “aware” ?
Quand je vois votre boulot et que moi en ce moment j’arrive à rien… Je vois passer plein de trucs intéressant je n’arrive pas à les lire et si éventuellement je les lis je n’arrive pas à les comprendre.
@artyprog: je suis en train de voir si je peux le mettre en prod, mais ça me fait chier de config un serveur pour ça. J’utilise pas crossbar parce que ça fait des notions en plus à expliquer, et là le tuto est déjà très long. C’est juste trop pratique de pouvoir lancer un serveur de dev automatiquement.
Si j’ai le temps, je vais voir pour un exemple de mise en prod. Si j’ai le temps. Et la motive. Parce que je t’avoues que l’article, là, il m’a mis à genoux pour la journée.
@Laurent: les technos vont très vites, même pour moi. J’arrive plus à tout suivre depuis quelques mois, c’est effrayant.
Arf, moi et deux correcteurs qui éditent en même temps le même article, ça fait n’importe quoi :)
Bon, je modifie le titre de l’article, il promet une introduction alors que c’est plus une démo, et pas un truc pas à pas bien décortiqué. Je voudrais pas faire de publicité mensongère.
Typo dans le paragraphe résumé : Autohan.py; Il manque un b.
Aussi, je trouve ça assez frustrant de toujours remettre crossbar sur le tapis dés que tu parle de serveur, j’aurais plus vu une section à la fin du genre : “et pour utiliser ça en prod bien sur vous prenez pas le serveur de dev mais crossbar”.
Là ça fait plutôt “je connais telle techno qui déchire sa maman mais je vous montrerais pas parce que c’est trop d’explication” à chaque paragraphe.
(Ça à l’air un peu méchant quand je me relis, mais c’était censé être constructif)
J’aime bien l’article (et même que j’ai lu tout le code).
J’avais du mal à cerner l’utilité exacte de WAMP et l’intérêt de l’API flaskesque, mais maintenant c’est bien plus clair et tu m’as presque fait aimer le js avec la clarté du .then().
Ah et petite déception, la vidéo de la démo est SFW, c’est déstabilisant.
Hello !
Encore un super article ! merci
Juste, j’ai dû
pip install twisted
pour lancer le programme ; autobahn ne semble pas embarquer twisted dans ses dépendances.
@Zanguu: ouai mais t’as vu la taille de l’article. Si je parle d’autobahn, c’est comme parler de nginx quand tu fais un tuto sur django, c’est trop. Avec django, tu utilises le serveur de dev, et avec WAMP aussi. Mais bon, je vais voir, pour les guerriers, si je peux pas rajouter un paragraphe de mise en prod.
@bobuss: bien vu merci. En fait autobahn à 3 dépendances possible : trollus, asyncio ou twisted. Si on précise pas, il installe aucune. J’ai corrigé la ligne d’installation.
re,
la prochaine fois, j’opterai pour la pull-request …
dans le code du player, ne pas oublier de passer la valeur du volume dans le publish.
video.addEventListener('volumechange', function(){
session.publish(uuid + '.volume', [video.volume]);
});
et dans le code de la télécommande, dans le setVolume, c’est l’attribut value du slider
slider.value = val;
puis dans le callback du .status, pour setter à la bonne valeur le volume
control.setVolume(status['volume'] * 100)
et enfin, dans le subscribe .volume, lire la donnée qu’on envoie depuis le player, et qui est un tableau
session.subscribe(uuid + '.volume', function(val){
control.setVolume(val[0] * 100);
});
Encore merci pour l’introduction, ça promet pour la suite !
Merci beaucoup, j’ai rajouté le volume après coup et c’est vrai que je me suis pas relu.
@Sam, justement avec Django tu mettrais en avant le serveur de dev pour la démo et tu préciserais à la fin qu’il ne faut pas l’utiliser pour de la prod et qu’il y a des solutions comme nginx pour la prod. Sans forcement expliquer comment le configurer.
Alors que là tu relances “sans arrêt” le fait qu’il faudrait plutôt utiliser crossbar en prod comme si ça te frustrait de ne pas l’utiliser et du coup c’est frustrant pour le lecteur (ou juste pour moi, mais je suis bizarre alors on s’en fout).
Je demandais pas l’explication pour mettre ça en prod, il y a déjà une intro à crossbar et je suppose qu’un petit tuto est dans les drafts.
Et puis le point essentiel de mon dernier commentaire : C’est un très bon article qui m’a permis de beaucoup mieux appréhender WAMP et c’est l’essentiel.
@Zanguu: ah, pourtant, vu que c’est moi qui ait intégré le serveur de Dev, je suis pas trop fustré sur la question :)
Just génial ce tuto. On est vraiment gâtés. Je vais tester ça moi même sur un projet perso.
2 petites questions :
– Si j’ai bien compris, à chaque
on enregistre un évènement sur le serveur WAMP.
Est-ce que ces événements sont supprimés au bout d’un moment? Parce que j’imagine que sur un serveur qui tourne pendant longtemps ça doit prendre pas mal de ressources.
– Si j’ai un client javascript qui dit qu’il register uuid. Est-ce que ça veut dire que le routeur wamp va envoyer toutes les nouvelles demandes RPC vers moi ?
Bon après j’imagine qu’il doit y avoir des moyens de spécifier
Dès qu’il n’y a plus de client qui fournit un callback pour ce namespace, il est supprimé du serveur. Mais bon, c’est vraiment faible comme consommation, ça risque pas grand chose.
Il me semble que le dernier arrivé est servit, mais il faudrait que je check. Dans notre cas, ça ne peut pas arriver facilement, l’ID est unique et jetable. Dans d’autres cas, il faut adapter sa solution à sa problématique.
C’est à ça que servent les realms. On peut dire “ce realm là n’a pas le droit de déclarer un callback”.
Question con (mais vraiment hein), il y a une limite à l’imbrication des callbacks ?
Exemple, pour être plus clair :
session.register(uuid + '.volume.up', func)
session.register(uuid + '.volume.down', func)
session.register(uuid + '.volume.bass', func)
session.register(uuid + '.volume.mute', func)
// exagération
session.register(uuid + '.audio.language.default.add.main', func)
L’intérêt est assez réduit pour le dernier bien sur mais pour le mute par exemple je trouve ça plus clair qu’un uuid.mute_volume.
Tu veux dire l’imbrication des namespaces. Non au contraire, ta version est bien plus propre, c’est juste que j’ai laissé ça de côté pour simplifier la démo.
Wouaw, just wouawww …
J’avais déjà joué avec les websockets, avec nodejs, meteorjs et twisted … jadis.
Je lisais avec grand intérêt tes posts sur WAMP, qui me permettait juste d’effleurer le potentiel du bousin. Mais sans jamais vraiment y plonger (l’api “pas flaskiest” ne me branchait pas du tout).
J’ai récupéré tes sources, j’ai fait des register, des subscribe et des publish, de clients js et python : et just wouaw! ça appel/event dans tous les sens avec une simplicité déconcertante …
Je n’ose même pas imaginer le potentiel de ce truc WAMP.
Merci d’avoir posté ce post… Je crois que je suis tombé dedans : FAN !
Si tu fais des expérimentations sympas, fait tourner.
Avec les nouvelles API “flaskiest” d’autobahn, on peut créer facilement un serveur.
Et sauf erreur de ma part, j’ai beau matté le code, il n’y a pas encore d’api simple pour créer un client simple (juste pour consommer du serveur) ?
En fait, l’objet Application est un client simple.
Pour ne pas démarrer de serveur, il suffit de faire :
Je viens de tester, c’est vraiment impressionnant
Autobahn et Domotique devraient faire très bon ménage…
Quand on y pense, c’est vraiment horrible d’écrire et du lire du Javascript, même si j’aime bien le concept de promises.
J’ai de mon côté abandonné le dev web, fatigué des frameworks qui sortent tous les 4 matins, fatigué de la prédominance du Javascript, langage que je trouve imbitable, rien qu’à voir comment nos navigateurs rament à cause de lui, ça me dégoûte de plus en plus. Voir aussi toutes les appli de start up en carton qui nous pondent des milliers d’applications inutiles sur le net, de la merde en paquet de 10, je préfère encore le site à gifs animés de DarkVador (pour les plus vieux d’entre nous qui connaissaient).
Entre nodejs, angular, jquery, meteor et tout le bin’s, on a du code final sale, non homogène et difficile à maintenir, une horreur : débogage pourri, gestion des erreurs pourrie…D’ailleurs les dev du web ne sont plus dev, ils deviennent comme nous, les admins, de simples intégrateurs de solutions, cela donne des appli au périmètre non maîtrisé et difficile à consolider dans la durée.
Egalement horrible est le fait qu’on lie les lib javascript dynamiquement, dans le sens où on pointe une url…si on fait pas cela on se retrouve avec des api non mises à jour qui changent tous les 4 matins, difficile à maintenir. Si on le fait, on est obligé d’avoir une connexion au net pour l’application et les règles de firewall qui vont bien pour la production, bref ça aussi je trouve que c’est sale.
L’écosystème JS, ça me fait penser à la TV d’aujourd’hui, que des programmes de merde, mais que finalement tout le monde regarde car c’est hype et à la mode et surtout qu’on a tous la tête dans le guidon car le peur du décrochage technique est réelle et anxiogène.
Je vous le dit, dans 5 ans, sametmax feront du full JS, il n’y aura plus que cela…et des appli encore plus pourries que celles d’aujourd’hui.
Sinon j’ai bien aimé votre article, même si le RPC a au moins 20 ans et que le pattern pub/sub on le connaît aussi depuis longtemps, donc rien de nouveau et la longue vie du marketing hype continue…
Vivement le retour du VBScript ;) !
@artyprog: à la base ça a été créé pour l’Internet des objets, il se trouve que ça marche très bien pour le web aussi.
@Robert: y a pas que la prog web dans la vie, donc si la prog web devient full JS, on ira voir ailleurs.
Par ailleurs, comme je le dis dans les autres articles sur WAMP, ni RPC ni PUB/SUB ne sont nouveaux, ce qui est nouveau c’est :
– le fait qu’on ait RPC __ET__ PUB/SUB dans le même protocole
– la simplicité de mise en oeuvre
– le fait que ça marche dans le navigateur
– le fait que ça marche entre plusieurs techno (browser, python, nodejs, android, etc)
WAMP ne permet, je le répète, en rien de faire quelque chose qu’on ne pouvait pas faire avant. Mais quelle techno le permet ? Par contre, il permet de le faire vraiment plus facilement.
Thanks for having put time into this tutorial!
One sentence I did not understand (OK, it was translated by Google):
Do you mean to say that in a simple development environment
python -m SimpleHTTPServer
can not be replaced by
python -m http.server
?If so, could you explain that in some more detail?
Without the WAMP dev server, which currently only exists for twisted.
I’m going to provide a translation of this tutorial on tavendo’s blog soon. Cool that you could use GT to read it though. I never thought it would be good enough.
Hi Sam, thanks for another great post! This kind of hands-on, down to earth examples is highly welcome.
A note regarding “running in production”: you can (obviously) have Crossbar run without proxy in front, and serving everything, including static Web. This is what we regularily do. Works great. No Apache, Nginx, whatever needed. Less wheels means less stuff can go wild.
Docs are here: https://github.com/crossbario/crossbar/wiki#going-to-production
Includes description on how to run on port 80 etc.
:)
Wow super Sam ! Ça a de la gueule avec l’API à la flask et effectivement ça a l’air super simple.
Je vais attendre que ça mûrisse un peu pour jouer avec en attendant, si jamais ça t’intéresse, ma config de base c’est RabbitMQ en broker de message, et stomp.js + sockjs pour le support des vieux navigateurs sans websockets. RabbitMQ a l’avantage d’avoir plein de clients dans plein de langages et de supporter (via plugin) STOMP via Websocket. Avec ça c’est assez simple de faire du PUB/SUB ou du RPC.
Question conne : est-ce que ça a du sens d’utiliser cette techno pour interconnecter des services tiers dont on ne maîtrise pas le moment où les données arrivent? Genre des données tombent dans un flux RSS. Comment en être averti instantanément ?
Il te faudra forcément un lecteur de flux qui balance un événement sur un channel auquel une de tes app clientes aura souscrit.
Genre un lecteur robot qui parse les flux des sites à intervalles régulier, ou qui souscrit à des event de ces sites des fois qu’ils en proposent (hey peut être qu’un jour ils feront du WAMP aussi de leur côté)
@Foxmask : le RSS est une techno basée sur le polling, donc tu es obligé, comme dit G-ROM, de faire un robot qui tourne en background et vérifie régulièrement tous ces flux. Après, utiliser cette techno va avoir deux avantages : c’est asynchrone, donc tu peux faire 1000 requêtes à 1000 flux en parallèle, et une fois que tu as l’info, tu peux la pusher à tous les autres clients en même temps immédiatement.
Typiquement, pour ton trigger happy, ça veut dire que tu peux lancer des triggers non bloquants en même temps, et le premier qui retourne un résultat peut déclencher la propagation du dit résultat à tous les clients.
Merci sam je mets tout ça de cote .
Les problèmes du tiers monde, c’est du pipi de chat à côté. Ils ont de la chance, eux, ils ne connaissent pas le streaming.
On est en 2014 est vous avez toujours des idées aussi arriérée. Bravo! Je suis resté bouche bée aprés cette phrase.
On est en 2014, et il y a encore des gens qui ne comprennent pas le second degré. Bravo! Je vous colle un tampon après cette phrase.
Bonjour,
Très bon article, j’ai néanmoins quelques questions :
– peut-on utiliser cette récente API (bonne idée) en mode client ? Pour faire du publish vite fait ?
– est-ce que dans le fichier de configuration de crossbar il ne faut pas mettre plutôt cela “url”: “ws://127.0.0.1:8080/ws” au lieu de “url”: “ws://127.0.0.1/ws” ?
Merci.
Oui, cette API marche très bien pour une client, il suffit de faire app.run(standalone=False) et le serveur n’est pas démarré.
Pour le port, je ne suis pas sûr. Je pense que la déclaration de port au dessus est suffisante. A vérifier.
Bonjour,
je ne suis pas du tout, mais alors pas du tout web-dev ou sys-admin. Pourtant, il y a une question qui, au risque de vous faire rire (mais bon, si en plus on peut se marrer, c’est déjà pas mal), me turlupine : est-ce que ca peut servir en remplacement de module python comme Pyro ? Dans mon cas, je me sers de Pyro pour faire du couplage de code, et même si j’adore, ça me contraint à utiliser du python pour l’interfaçage. Je me demandais je pouvais pas rendre ça plus souple…
Sinon, super blog… Je ne fais que commencer à le défricher, mais je sens que je vais y passer du temps ! (heureusement que j’ai un collègue dans mon bureau, sinon il n’y aurait pas que la rubrique “Programmation” ;-))
Du couplage de code ?
“Donc mettez votre téléphone en Wifi, par en 3G.”
pas*
:)
sam,
super tuto domage qu’il n’y a pas de publish et suscribe cote python.
l’API flaskesque est utilisable pour le publish ?
j’ai chercher app.session pour faire un app.session.publish mais j’ai pas trouver de doc
Ouai y a l’api flaskesque maintenant. Faut chercher sur le blog, j’avais fais un tuto quand c’est sorti.
FYI https://demo.crossbar.io/videocontrol/ 404 :(
(pas besoin de publier ce commentaire)
Je leur ai déjà reporté mais ils s’en occupent pas. C’est pour ça qu’ils ont besoin de quelqu’un je pense.
1er post et vraiment merci pour tout ce que vous faites.
J’ai découvert WAMP grâce la publication des slides.
Désolé, mais pour moi c’est bluffant !!!
J’ai réussi à faire tourner la démo non sans mal :
app.py semble ne pas lancer de serveur WAMP. Le programme bloque et CTRL+C indique qu’il ne peut pas se connecter.
ca fonctionne dès qu’on lance le routeur crossbar (cf https://github.com/tavendo/AutobahnPython/issues/304)
app.py :
app.run(url="ws://localhost:8080/ws")
les fichiers HTML : ‘:8080/’ remplacé par ‘:8080/ws’
@keiser1080 : le subscribe en Python fonctionne. Mais toujours pas de publish.
app.py :
@app.subscribe('lecteur.volume')
def onevent1(x):
print("Volume du lecteur : ", x)
index.html : publication d’un nouveau PUB sans l’UUID (je ne sais pas comment faire référence à uuid.volume qui existe déjà.
session.publish('lecteur.volume', [video.volume]);
La classe autobahn.twisted.wamp.Application a pas mal changé.
Et en effet, les exemples fournis ne suivent pas trop le code (je sais c’est dur :-) )
app.run(standalone=False)
ne fonctionne plus par exemple.
Depuis la rédaction de l’article, l’API de crossbar a changé, et le routeur intégré n’existe plus dans autobahn. Je vais mettre à jour l’article.
Yop yop,
Juste un truc, j’ai un peu galéré à faire fonctionner l’exemple. J’ai du rajouter /ws dans le app.run :
app.run(url=u”ws://localhost:8080/ws”)
Et dans le coup idem dans le JS aussi :
url: ‘ws://’ + window.location.hostname + ‘:8080/ws’,
Est-ce que j’ai loupé quelque chose ou est-ce qu’il faut mettre l’article à jour ? :)
Merci en tout cas !
Sans vouloir être pénible, je pense que t’as oublié de modifier le code de la télécommande :-)
Au contraire, au contraire, tu m’aides beaucoup. J’ai tellement d’articles sur le blog, sans l’aide des lectures ils seraient tous complètement inutiles.