I’ve been playing about with node.js this week, prototyping a “Second Screen” experience using websockets. There are lots of tutorials on how to create a basic broadcast chat client, but I couldn’t find any examples which did quite what I wanted.

The idea is that a user visits a website on their desktop computer and is shown a QR code which they can scan on their phone. The QR code sends their mobile browser to a unique URL, which presents controls for interacting with the desktop site. The unique URL provides a link between the two browsers and allows messages to be sent from the mobile to the desktop. This might mean scrolling to a particular page, swiping to load a particular article, using their phone to draw something on the desktop, anything really.

This is a very bare-bones example, so don’t expect anything fancy.

So the basic process for how this should work is:

  1. A node.js script is running on a server, listening for requests from clients
  2. A desktop browser visits the site. Some javascript runs which creates a websocket and connects to the server
  3. The server receives the connection from the desktop, generates a random 5 character ID string and sends it back to the desktop
  4. The desktop receives the ID and uses it to build a URL for the mobile device, eg. second-screen.com/mobile?ID=abcde
  5. The QR code is loaded from the Google Chart Tools API as an image
  6. The user scans the QR code on their phone and visits the unique URL
  7. The mobile browser creates a websocket and connects to the server, sending across the ID string
  8. The server receives the connection from the mobile and checks to see if there is a desktop with a corresponding ID
  9. The server sends a message to the desktop and the user is notified that a mobile has connected
  10. We can then send instructions from the mobile to the desktop

The code examples below are just to demonstrate the interesting parts of the process, but you can download the code here.

Getting started

There are loads of tutorials on how to install node.js and once that’s installed, getting websockets installed should be as simple as typing:


npm install websocket.io

Once you start developing your server script (I’ve called mine server.js), you can run it on the command line by typing:


node server.js

HTML

This example is just going to allow the user to scroll up and down through pages of the desktop site. The desktop has three pages, and then two DIVs which are hidden by CSS.


<div class="page" id="page-1">
    <p>Page 1</p>
</div>

<div class="page" id="page-2">
    <p>Page 2</p>
</div>

<div class="page" id="page-3">
    <p>Page 3</p>
</div>

<div id="unique-url">
    <p>Visit <span>http://<?php print $_SERVER['HTTP_HOST']; ?>/phone.php?id=%id</span> on your mobile device for a second screen experience, or use the following QR code: <img src="https://chart.googleapis.com/chart?cht=qr&chl=http%3A%2F%2F<?php print $_SERVER['HTTP_HOST']; ?>%2Fphone.php%3Fid%3D%id&choe=UTF-8&chs=150x150" /></p>
    <a id="close" href="#">X</a>
</div>

<div id="status-message">
    <p>THIS IS A STATUS MESSAGE</p>
</div>

The #status-message div will be used to show the user messages such as confirmation that a mobile device has connected, or perhaps if there is a connection error.

The #unique-url DIV will be shown after the desktop site has successfully connected to the server (after step 3), and the unique code has been sent back. A string replacement will happen to swap “%id” with the unique code.

The mobile browser has more simple markup, just “Scroll up” and “Scroll down” buttons:


<a class="scroll" id="scroll-up" href="#">Up</a>
<a class="scroll" id="scroll-down" href="#">Down</a>

Node.js server

So next we need a node.js script to run as the webserver, accepting connections from both the desktop and mobile devices. I called this server.js. The following code is just the bit which sets up connections with clients, the message processing will come later.


// Create a websocket.
var WebSocketServer = require('websocket').server;
var http = require('http');
var webSocketsServerPort = 1337;

// Clients object stores all connections.
var clients = { };
var clientIndex = -1;

// WebSocket server - a connection is made from desktop or mobile device.
wsServer.on('request', function(request) {

    // The connection is stored.
    var connection = request.accept(null, request.origin);

    // Create an index for the connection so we can delete it later.
    clientIndex++;
    request.index = clientIndex;

    // Store the connection in the clients object.
    clients[clientIndex] = { 'connection': connection };

    // A message is received from the desktop or mobile device.
    connection.on('message', function(message) {

    // Turn the message in to a JSON object.
    var received = JSON.parse(message.utf8Data);

    // This is where all the message processing happens.
});

// When a connection is lost, we should delete it from the clients object.
connection.on('close', function(connection) {
    delete(clients[request.index]);
});

The desktop connection

In the JavaScript file, desktop.js, a connection is made to the server.


// Get the domain.
var host = window.location.hostname;

// Create a websocket object. TODO: Make it work in IE.
window.WebSocket = window.WebSocket || window.MozWebSocket;

// Create a connection to the server.
var connection = new WebSocket('ws://' + host + ':1337');

Once a connection is made, we tell the server that we are a desktop connecting for the first time:


// When a connection is opened for the first time.
connection.onopen = function () {

    // Send a message to the server telling it the desktop is connecting.
    var msg = JSON.stringify({ type:'connect', data: 'desktop' });
    connection.send(msg);
};

The following code can then be added to the “onmessage” function in Server.js. If the connection is accepted, it generates the random ID and sends it back to the desktop. It also stores the random ID and connection type (mobile or dekstop) in the clients object.


// A desktop is connecting for the first time. Generate a unique
// ID and send it back to the client.
if (received.type == 'connect' && received.data == 'desktop') {

    // Generate the unique ID.
    var uniqueID = makeid();

    // Store the connection type and unique ID.
    clients[request.index].type = 'desktop';
    clients[request.index].uniqueID = uniqueID;

    // Send the unique ID back to the client.
    var send = JSON.stringify({ type:'uniqueID', data: uniqueID });
    connection.send(send);

    return;
}

Back in desktop.js, the unique ID is received. We can now update the HTML with the ID and show the message and QR code to the user.


if (received.type == 'uniqueID') {

    // Update the mobile connection message and show it to the user.
    var uniqueID = received.data;
    $('#unique-url p').html($('#unique-url p').html().replace(/%id/g, uniqueID));
    $('#unique-url').fadeIn(100);
    return;
}

The mobile device connection

The user now scans the QR code and it takes the to the unique URL generated in the previous step. They visit a URL similar to second-screen.com/mobile?ID=abcde. A new Javascript file is loaded - mobile.js, containing code to create a websocket and connect to the server. Once the connection has been established, the ID is read from the URL and sent across to the server.


// When a connection is opened.
connection.onopen = function () {

    // Get the ID from the URL.
    var uniqueID = $.urlParam('id');

    // Send the unique ID back to the client.
    var msg = JSON.stringify({ type:'connect', data: 'mobile', uniqueID: uniqueID });
    connection.send(msg);
};

Server.js is updated to process this message and then notify the desktop a connection has been made.


// A mobile devide is connecting for the first time. Tell any
// desktop clients with a matching ID.
if (received.type == 'connect' && received.data == 'mobile') {

    // Store the connection type and unique ID.
    clients[request.index].uniqueID = received.uniqueID;
    clients[request.index].type = 'mobile';

    // Search for a desktop client and send the message to them.
    for (var i in clients) {

        if (clients[i].uniqueID == received.uniqueID && clients[i].type == 'desktop') {

            // Send the message to the desktop.
            var send = JSON.stringify({ type: 'mobile_device_connected' });
            clients[i].connection.send(send);
        }
    }
        return;
}

And finally desktop.js is updated to notify the user that a mobile device has connected.


// When a module device connects with the same unique URL, the
// server sends a notification to the desktop.
if (received.type == 'mobile_device_connected') {

    // Remove the mobile connection message, and tell the user
    // that the mobile device has connected successfully.
    $('#unique-url').fadeOut(100);
    $('#status-message')
        .html('<p>Mobile device connected!</p>')
        .fadeIn(100, function() {
            setTimeout(function() {
                $('#status-message').fadeOut(100);
            }, 3000);
        });

    return;
}

Sending messages

In server.js, our clients variable stores connected clients. Each client is an object with “type”, “uniqueID” and “connection” properties. With each message received from a mobile, we just need to search through the clients variable for a type of “desktop” with a matching uniqueID and send the message across.

In mobile.js we can add the following:


// Process scroll requests and send them to the server.
$('.scroll').bind('click', function(e) {

    e.preventDefault();

    // Send the mssage to the server.
    var msg = JSON.stringify({ type:'scroll_request', data: $(this).prop('id') });
    connection.send(msg);
});

This sends either “scroll-up” or “scroll-down” to the server. Finally, we just pass the message across to the desktop. Desktop.js is updated with the following:


// A scroll request has been received.
if (received.type == 'scroll_request') {

    // Search for a desktop connection where the unique ID
    // matches the mobile device's unique ID.
    for (var i in clients) {

        if (clients[i].uniqueID == clients[request.index].uniqueID && clients[i].type == 'desktop') {

            // Send the message to the client.
            var send = JSON.stringify({ type: 'scroll_request', data: received.data });
            clients[i].connection.send(send);
        }
    }
}

That’s it! It can be updated to send across any data from mobile to desktop.

Download the full code here.

« Back