Small demo of a pragmatic use of WAMP in Python

, Sam

A Python WAMP programming tutorial creating a Web-based video player with remote control.

A hands-on tutorial

A guest post by Sam from Sam & Max. Original (French) version here. More related posts:

Top

Tavendo decided to release Flask-like API for WAMP earlier than I expected. We don't have unit tests yet or implementations for asyncio (or trollius, so it came as a surprise. But it's cool cause it means we can start playing with it right now.

Now, I wanted to make a sexy demo to make you want to give WAMP a try. So I searched on the Web, looking at real time Web projects, mostly NodeJS and Tornado, to find inspiration.

And I found something very fun: a video player you can pilot remotly

Indeed: isn't it lame to watch an online movie from you couch while your laptop is on the distant table ? If you wanna pause or change the volume, you have to get up. Doh!

3rd world people have it easy. They don't know the struggle of video streaming.

So here is the deal:

One page with an HTML5 player and a QR code:

Player page

To simplify the demo, you can click on the QR code and get the remote control on another tab in case you don't have a smartphone or a QR code scanner app.

If you scan the QR code with your phone, it will send you on a Web page with a remote control for the player, so you don't have to move your ass.

Remote control page

Of course, it's a basic design. I'm not launching a kickstarter, it's a tutorial guys.

And the best thing is, it's not even hard to do.

But first, the mandatory:

[Goto Live Demo](https://demo.crossbar.io/videocontrol/)

And you can download the source code here, also the comments are in french. I'm going to translate them here, but not in the repository, I'm too lazy for that. Maybe Tobias will host an english version of it, you will have to ask him (complete project code as run on the live demo can be found here).

To understand what's going to follow, you'll need Javascript and Python basic skills and a good understanding of callbacks. Been ok with promises can help as well.

The HTML

We'll need 2 Web pages, one for the video player, and one for the remote control.

Note: due to syntax-highlighting issues, the code below uses (incorrect) <scri pt> tags instead of <script>.

The player :

Then the remote control :

Nothing incredible, really. Just ol' good HTML, a little CSS, loading JS dependencies. Classic Web dev.

Since we are using hotlinked resources to make thing simpler, you'll need to be connected to Internet for it to Work.

Setuping the server

We gonna go with Python 2.7. I know, I know, V3 is all the rage right now. But for now the Flask-like API is only available for the Twisted backend, which is 2.7 only. It's a work in progress.

First, we need an HTTP server to serve our HTML files. Crossbar could do that for us in production, but for dev, we're not going to setup it, we gonna take the easy road.

So, at the project root, run the following command:

python -m SimpleHTTPServer

Now your Web pages will be served localy on the port 8000. E.g, to go to the video page:

http:localhost:8000/index.html

Then, you need to install a Python lib to deal with WAMP :

pip install autobahn[twisted]

Again, for the sake of simplicity, our Python app is going to start a little dev WAMP server. We won't need Crossbar at all for this demo.

Service side WAMP app

For this demo, the server doesn't have a lot to do. We would totally make it without any server code, but it will make our life simpler.

Indeed, we have 2 problems that a server side component can solve easily for use : create a unique ID for the player and get the local network IP.

The ID will make sure people running the remote page at the same time won't send an order to the wrong player. We need a unique ID, but JS doesn't provide something to do this out of the box. We could use time stamp, but there are too easy to guess and any script kiddies would mess up with it. Loading an additional lib to do that would be plain silly since we can do it in Python in 3 lines.

The IP is required because I don't want you to have to look for your own IP adress manually. And since you will access a website on your laptop from your phone, you'll need the laptop network local IP address. In production, we wouldn't do that, of course, but we don't have a domain name to help us here.

Here is what's the WAMP code look like on the server side :

   from autobahn.twisted.wamp import Application

   import socket
   import uuid

   # Just like for flask, the app object
   # is what's bind all elements together.
   # We give it a name, here "demo".
   app = Application("demo")
   # While the app is going to start
   # a server for us, the app is a CLIENT
   # of the WAMP server. The server is
   # started automatically as a courtesy
   # for dev purpose. In production, we
   # would use crossbar.

   # Just a container to store the IP
   app._data = {}

   # We ask for this function to be called when the
   # app is connected to the WAMP server. This allow
   # us to run caode right after the app.run() call
   # you can see at the bottom of the file. '_' is
   # a convention in Python meaning "this name has
   # no importance, it's disposable code that we'll
   # use only once"
   @app.signal("onjoined")
   def _():
      # We get our IP address on the local network.
      # It's a trick requiring an external IP to
      # be reachable, so you need an internet connection.
      s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
      s.connect(("8.8.8.8", 80))
      # We store the local IP address in a container
      # that will be accessible anywhere else.
      app._data["LOCAL_IP"] = s.getsockname()[0]
      s.close()

   # We declare that the "ip()" function is callable using
   # RPC. It means any WAMP client caan get thre result
   # of this function. So we will be able to call it
   # from our browser. Since our app is named "demo" and
   # our funciton is named "ip", a client will be able
   # to call it using "demo.ip"
   @app.register()
   def ip():
      # We just return the store local IP. Nothing crazy.
      return app._data["LOCAL_IP"]

   # I wanted to call this distant function "uuid", but that
   # would shadow the uuid Python module. This is not a good
   # id. Intead, I'll call it "get_uuid", but I'll declare
   # manually the full namespace in register(). A WAMP client
   # will be able to call it using "demo.uuid".
   # Please note the namespace should be written foo.bar.stuff,
   # not foo:bar, foo/bar or foo.BAR. Syntax is significant.
   @app.register("demo.uuid")
   def get_uuid():
      # Return the UUID without dashes.
      # E.G: b27f7e9360c04efabfae5ac21a8f4e3c
      return str(uuid.uuid4()).replace('-', '')

   # We run the application. This will start the
   # server then the client. You can disable the
   # dev server in prod.
   if __main__ == "__main__":
      app.run(url="ws://0.0.0.0:8080/")
      # You can't put anything here. You need to
      # put code in @app.signal('onjoined') if you
      # want to run it after the app has started.
   

Now let's start our app:

python app.py

We now have 2 servers running: an HTTP server listening on port 8000, and a WAMP server listening on port 8080. In production, crossbar can serve HTTP and WAMP, so no need for 2 tools.

The video player

We now need to define our video player behavior via javascript. It's mainly about connecting to the WAMP server and exchanging messages via RPC or PUB/SUB :

  var player = {};
  var url;
  /* Run the code once the page is loaded */
  window.addEventListener("load", function(){

    /* Connection to the WAMP server. I use
       the default values of the dev server. We
       open explicitly this connection at the end
       of the script. */
    var connection = new autobahn.Connection({
       url: 'ws://' + window.location.hostname + ':8080/',
       realm: 'realm1'
    });

    /* Run this code once the connection is
       successful. Note that I don't handle
       any error cases in this demo since
       it's a rabbit hole I don't want to
       fall in right now. */
    connection.onopen = function (session) {

    /* Calling the ip() function on the server */
      session.call("demo.ip")

      /* Once you got the IP, you can build our
         project URL and call the get_uuid() function
         on the server */
      .then(function(ip){
        url = "http://" + ip + ":8000";
        return session.call("demo.uuid");
      })

      /* Once you got the UUID, we can start to
        deal with the remote control part */
      .then(function(uuid){

        /* Creating the QR code with a link targetting
           the right URL. We put the ID in the hash. */
        var controlUrl = url + '/control.html#' + uuid;
        var codeDiv = document.getElementById("qrcode");
        new QRCode(codeDiv, controlUrl);
        var ctrllink = document.getElementById("ctrllink");
        ctrllink.href = controlUrl;

        /* Most of our job is about manipulating
           this element */
        var video = document.getElementById("vid");

        /* We declare that these 4 function can be
           called remotly. They can be called by
           usig a named composed with our ID and
           the action you wish to do. E.G:
           'b27f7e9360c04efabfae5ac21a8f4e3c.play'
           to call "play" in our 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
          };
        });


       /* Somebody could very will push play directly from
          this page.

          So we need to react if the user does it, and
          publish an WAMP event to allow our remote
          control to update its UI.
       */
       video.addEventListener("play", function(){
            /* We publish a message saing the player
              started to read the video */
         session.publish(uuid + ".play");
       });

         /* addEventListener has nothing to do with WAMP.
            it's vanilla Javascript DOM API. */
        video.addEventListener("pause", function(){
          session.publish(uuid + ".pause");
        });

        video.addEventListener("volumechange", function(){
          session.publish(uuid + ".volume", [video.volume]);
        });

     });
    };

    /* Opening the connection once all the callbacks are set */
    connection.open();
  });
   

Remote control code

The remote control is our 3rd WAMP client (we can have hundreds). Our 1st is the Python app, our second is the player.

Its code purpose is to send orders to the HTML5 player, but also update its UI if the player state changes.

   /* This is the object holding the logic
      for our play/pause and volume controls.
      Nothing crazy, it updates the button
      and slide display according to
      the play/pause state and the volume
      value .*/
   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/',
       realm: 'realm1'
     });

     connection.onopen = function (session) {

       /* We get the ID from the URL hash */
       var uuid = window.location.hash.replace('#', '');

       /* Updating the controls according to the current
          player state using a RPC call to the other
          page. */
       session.call(uuid + '.status').then(function(status){

         control.setPlaying(status['playing']);
         control.setVolume(status['volume'])

         /* We bind the button clic event to
            a call to the play() function on
            the remote player. The uuid allow
            us to only send the event to the
            right 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]);
         };

         /* We add a callback to react to events like
            the player state changing. If somebody
            clic on play/pause or change the volume,
            we want to update this 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();
   };
   

Summary

Here's what our project finally looks like:

  • app.py (python client)
  • control.html (remote control)
  • index.html (video player)

While the python app run silently and automatically a dev server, it's a separate component

For this project, we used:

  • WAMP: the protocol allowing all the application parts to communicate in real time using RPC and PUB/SUB.
  • AutobahnJS: a lib to create WAMP clients in javascript.
  • AutobahnPython: a lib to create WAMP clients in Python.

We didn't use Crossbar, the WAMP Python server. We use the small dev server included in Autobahn. In a production env, we would have used Crossbar as the WAMP server.

There are a some concepts to grasp.

First, there is RPC.

It allow a client to say "all other clients can call this function remotely". We use it to expose ip() and get_uuid() on our server, so our JS can call it. But we ALSO use it so that one of the pages (the player) exposes play(), pause() and voume() and the other one (our remote control) can use them.

The big difference is that ip() can be called by all cients using "demo.ip" while play() can only be called if clients know the player ID since you need to use "<id>.play".

Then you got PUB/SUB.

This allow a client to say "I listen to all messages about this topic". Then another client can send a message (we also call that an event, it's pretty much the same in that context) to that topic, so that all subscribers receive it.

We use it so our remote control say "I listen to all messages about the player state changing". On the other side, when you clic on a player button, we send a message saying if the volum has changed of if someone pushed pause/play/ The remote can then update its UI.

This is a good example of the typical usages you make of these 2 tools :

  • RPC let you give an order or retrieve an information.
  • PUB/SUB let you stay aware (or make people aware) of current events.

Here is our project workflow:

  1. We start the WAMP server.
  2. Clients connect to ir (in our case, Python and JS code).
  3. Clients declare what function they expose to RPC and what topic they listen to via PUB/SUB/
  4. We react to user actions by doing RPC calls and PUB/SUB publications.

If you remove all the comments, you'll see that the code is quite short for such a complex app.

Once again, it's totally possible to do this without WAMP. It's just going to be harder. I invite your to do it in PHP, Ruby or a WSGI app to compare, it's not fun at all. With NodeJS or gevent, it would be simpler, but you still got to handle the RPC and PUB/SUB manually or install some more libs and stich them together.

WAMP make this kind of app trivial to write. Well, I say trivial because I willingly ingored all the edge cases. Of course, for a robust product, you'll need to sweat a little.

Limits

It's Python 2.7. Soon, you'll be able to do it with Python 3.4, but without the dev server unfortunately.

Hopefully, Twisted is currenly being ported to Python 3, so everything should be working in 3.2+.

It's HTML5, obviously, but nothing stop you to put Flash in the mix if you feel like it.

It's WebSocket, but again, a bit of Flash allow you to reach old browsers. And then there is a WAMP-over-Longpoll transport currently being developed which works with any old browser - without Flash.

No, the real limit is again the youth of the project: no autoreload for the dev server (restarting it manually everytime you modify your code really gets old) and errors on the server sides can only be read in the JS console, and not from the terminal you started the server in. Little things like that.

Final Words

My message here is not "start coding all the things with WAMP", but rather "look at these things. It's beautiful, it has so much potential, and it needs a community to make it a killer app".

Python needs a Web killer app to compete with next gen framewok like meteor.js and alike. And I'm sure something incredible can be built with WAMP.

Top

About the author

Sam is a developer mainly working in Python and JavaScript. Contact him at Sam & Max.

License

This blog post is licensed under Creative Commons CC-BY-SA.

Start experimenting and prototyping for IoT applications with Crossbar.io and Raspberry Pi!

Loaded with all the software you need to make the board part of WAMP applications!



Learn more

Recent posts

Atom Feed

Search this Site

Stay Informed

Sign up for our newsletter to stay informed of new product releases and features:
Community Chat