Originally posted on TokBox Developer Blog

This tutorial will create a simple chat roulette app using node.js, socket.io and OpenTok. Socket.io allows us to pass data between clients in real-time using only javascript and eliminate the need for a database. OpenTok allows us to quickly publish and subscribe to webcam streams without having to worry about server requirements and bandwidth usage — all we have to do is implement a simple and free javascript API.

Here is a high-level overview of the application architecture:

You can check out the app running here: http://fierce-sword-182.herokuapp.com
You can view the GitHub repo here: https://github.com/jonmumm/RouletteTok

The Setup

If you don’t already have node.js and npm (node package manager), install them now.

The first thing we want to do is install express. Express is a node.js framework that abstracts the low level details of creating a web app, allowing us to get started quickly. Use the following command to install express globally so you can run it from the command line:

npm install -g express

Once express is installed, use the following commands to create the application we are going to build:

express RouletteTok
cd RouletteTok

This command create a new directory called RouletteTok that contains the basic structure of our application.

Next, edit package.json (in your root application directory) to include the modules we will need, which are express, jade, socket.io, and opentok.

{
	"name": "RouletteTok",
	"version": "0.0.1",
	"dependencies": {
		"express": "2.3.11",
		"jade": "0.12.1",
		"opentok": "0.1.0",
		"socket.io": "0.6.18"
	}
}

Now run the following command to install the dependencies:

npm install

That should be it for setting up. You should now be able to run your node server with the following command

node app.js

The Application

Our app consists of four main files, which are:

Let’s go through each of these four files to see what they do.

app.js

This is the main file used to start our app. The majority of the code here was generated by Express when we created our project, however I did add some things to it (explained below). Essentially what this file does is sets up our app, creates the routes, and starts the socket server at the very last line.

var express = require('express');
var io = require('socket.io');

var app = module.exports = express.createServer();

// Configuration
app.configure(function() {
  app.set('port', process.env.PORT || 3000);
  app.set('views', __dirname + '/views');
  app.set('view engine', 'jade');
  app.use(express.bodyParser());
  app.use(express.methodOverride());
  app.use(app.router);
  app.use(express.static(__dirname + '/public'));
});

app.configure('development', function() {
  app.set('address', 'localhost');
  app.use(express.errorHandler({
    dumpExceptions: true,
    showStack: true
  }));
});

app.configure('production', function() {
  app.set('address', 'fierce-sword-182.herokuapp.com');
  app.use(express.errorHandler());
});

// Routes
app.get('/', function(req, res) {
  res.render('index', {
    title: 'RouletteTok',
    address: app.settings.address,
    port: app.settings.port
  });
});

if (!module.parent) {
  app.listen(app.settings.port);
  console.log("Server listening on port %d", app.settings.port);
}

// Start my Socket.io app and pass in the socket
require('./socketapp').start(io.listen(app));

The most important thing here is at line 31, where we set up our root route (at ‘/’) to render our index template (which we will create later). We pass our template the server address and port so that we can connect to the socket.io server from the client. At line 45, we start the socketapp module that we are going to write next.

socketapp.js

This file implements the socket server logic. In this file, we set up the event handlers for when clients connect and send messages to the socket server. When a client sends a message, the server parses it for the event value, performs some logic, and then sends a message back to the client or clients. Here is the complete file, we will discuss it in detail below:

// Require and initialize OpenTok SDK
var opentok = require('opentok');
var ot = new opentok.OpenTokSDK('413302', 'fc512f1f3c13e3ec3f590386c986842f92efa7e7');

// An array of users that do not have a chat partner
var soloUsers = [];

// Sets up the socket server
exports.start = function(socket) {
  socket.on('connection', function(client) {

    client.on('message', function(message) {

      // Parse the incoming event
      switch (message.event) {

        // User requested initialization data
        case 'initial':
          // Create an OpenTok session for each user
          ot.createSession('localhost', {}, function(session) {

            // Each user should be a moderator for their own session
            var data = {
              sessionId: session.sessionId,
              token: ot.generateToken({
                sessionId: session.sessionId,
                role: opentok.Roles.MODERATOR
              })
            };

            // Send initialization data back to the client
            client.send({
              event: 'initial',
              data: data
            });
          });
        break;

        // User requested next partner
        case 'next':

          // Create a "user" data object for me
          var me = {
            sessionId: message.data.sessionId,
            clientId: client.sessionId
          };

          var partner;
          var partnerClient;
          // Look for a user to partner with in the list of solo users
          for (var i = 0; i < soloUsers.length; i++) {
            var tmpUser = soloUsers[i];

            // Make sure our last partner is not our new partner
            if (client.partner != tmpUser) {
              // Get the socket client for this user
              partnerClient = socket.clientsIndex[tmpUser.clientId];

              // Remove the partner we found from the list of solo users
              soloUsers.splice(i, 1);

              // If the user we found exists...
              if (partnerClient) {
                // Set as our partner and quit the loop today
                partner = tmpUser;
                break;
              }
            }
          }

          // If we found a partner...
          if (partner) {

            // Tell myself to subscribe to my partner
            client.send({
              event: 'subscribe',
              data: {
                sessionId: partner.sessionId,
                token: ot.generateToken({
                  sessionId: partner.sessionId,
                  role: opentok.Roles.SUBSCRIBER
                })
              }
            });

            // Tell my partner to subscribe to me
            partnerClient.send({
              event: 'subscribe',
              data: {
                sessionId: me.sessionId,
                token: ot.generateToken({
                  sessionId: me.sessionId,
                  role: opentok.Roles.SUBSCRIBER
                })
              }
            });

            // Mark that my new partner and me are partners
            client.partner = partner;
            partnerClient.partner = me;

            // Mark that we are not in the list of solo users anymore
            client.inList = false;
            partnerClient.inList = false;

          } else {

            // Delete that I had a partner if I had one
            if (client.partner) {
              delete client.partner;
            }

            // Add myself to list of solo users if I'm not in the list
            if (!client.inList) {
              client.inList = true;
              soloUsers.push(me);
            }

            // Tell myself that there is nobody to chat with right now
            client.send({
              event: 'empty'
            });
          }

        break;
      }
    });
  });
};

There are two events that the socket server listens for and responds to: 1) the initial event on line 18 and 2) the next event on line 40.

The initial event is sent by clients when they load the page (shown later in public/javascripts/app.js). All this event does is create an OpenTok session for this client to publish its webcam to, and then sends the session information and token back to the client.

The next event is sent by clients when they request to talk to a new person. This event maintains an array (called “soloUsers”) of users who do not currently have a partner. Every time a client triggers this event, it checks the array to see if there is a suitable partner to chat with — if there is a match it sends both clients the other client’s OpenTok session to connect and subscribe to, otherwise it adds the requesting client to the soloUsers array.

views/index.jade

This is the view file that is loaded at at our root route. It uses the Jade syntax language. Essentially it it is is a few divs for holding the video streams and the scripts necessary for our app.

h1 #{title}

div#streams
  div#publisherContainer
  div#subscriberContainer

div#controls
  div#notificationContainer
  button#nextButton Next

script(src='./socket.io/socket.io.js')
script(src='http://staging.tokbox.com/v0.91/js/TB.min.js')
script
  var config = {};
  config.address = '#{address}';
  config.port = '#{port}';
script(src='./javascripts/app.js')

Notice at line 13 in the template we create a config object which we will use in public/javascripts/app.js to connect to the socket server from the client. We set these variables using the variables passed in to our template from the route set up in our app.js server file at line 34.

public/javascripts/app.js

The last file is the client-side javascript that connects to the socket server and sends requests based on user actions, then listens for and reacts to responses sent from the server. This file is separated in to two logical parts: 1) the socket code that connects the server, parses received events, and gets the new data and 2) the RouletteApp code that handles the DOM manipulation, user interaction, and OpenTok publishing and subscribing.

(function() {

	var socket = new io.Socket(config.address, {port: config.port, rememberTransport: false});

	socket.on('connect', function() {
		socket.send({ event: 'initial' });
	});

	socket.on('message', function (message) {
		var sessionId;
		var token;

		switch(message.event) {
			case 'initial':
				sessionId = message.data.sessionId;
				token = message.data.token;

				RouletteApp.init(sessionId, token);
			break;

			case 'subscribe':
				sessionId = message.data.sessionId;
				token = message.data.token;

				RouletteApp.subscribe(sessionId, token);
			break;

			case 'empty':
				RouletteApp.wait();

			break;
		}
	});

	socket.connect();

	var SocketProxy = function() {

		var findPartner = function(mySessionId) {
			socket.send({
				event: 'next',
				data: {
					sessionId: mySessionId
				}
			});
		};

		return {
			findPartner: findPartner
		};
	}();

	var RouletteApp = function() {

		var apiKey = 413302;

		var mySession;
		var partnerSession;

		var partnerConnection;

		// Get view elements
		var ele = {};

		TB.setLogLevel(TB.DEBUG);

		var init = function(sessionId, token) {
			ele.publisherContainer = document.getElementById('publisherContainer');
			ele.subscriberContainer = document.getElementById('subscriberContainer');
			ele.notificationContainer = document.getElementById('notificationContainer');
			ele.nextButton = document.getElementById('nextButton');

			ele.notificationContainer.innerHTML = "Connecting...";

			ele.nextButton.onclick = function() {
				RouletteApp.next();
			};

			mySession = TB.initSession(sessionId);
			mySession.addEventListener('sessionConnected', sessionConnectedHandler);
			mySession.addEventListener('connectionCreated', connectionCreatedHandler);
			mySession.addEventListener('connectionDestroyed', connectionDestroyedHandler);
			mySession.connect(apiKey, 'moderator_token');

			function sessionConnectedHandler(event) {
				ele.notificationContainer.innerHTML = "Connected, press allow.";

				var div = document.createElement('div');
				div.setAttribute('id', 'publisher');
				ele.publisherContainer.appendChild(div);

				var publisher = mySession.publish(div.id);
				publisher.addEventListener('accessAllowed', accessAllowedHandler);
			};

			function accessAllowedHandler(event) {
				SocketProxy.findPartner(mySession.sessionId);
			};

			function connectionCreatedHandler(event) {
				partnerConnection = event.connections[0];
			};

			function connectionDestroyedHandler(event) {
				partnerConnection = null;
			}
		};

		var next = function() {
			if (partnerConnection) {
				mySession.forceDisconnect(partnerConnection);
			}

			if (partnerSession) {
				partnerSession.disconnect();
			}
		};

		var subscribe = function(sessionId, token) {
			ele.notificationContainer.innerHTML = "Have fun !!!!";

			partnerSession = TB.initSession(sessionId);

			partnerSession.addEventListener('sessionConnected', sessionConnectedHandler);
			partnerSession.addEventListener('sessionDisconnected', sessionDisconnectedHandler);
			partnerSession.addEventListener('streamDestroyed', streamDestroyedHandler);

			partnerSession.connect(apiKey, token);

			function sessionConnectedHandler(event) {
				var div = document.createElement('div');
				div.setAttribute('id', 'subscriber');
				ele.subscriberContainer.appendChild(div);

				partnerSession.subscribe(event.streams[0], div.id);
			}

			function sessionDisconnectedHandler(event) {
				partnerSession.removeEventListener('sessionConnected', sessionConnectedHandler);
				partnerSession.removeEventListener('sessionDisconnected', sessionDisconnectedHandler);
				partnerSession.removeEventListener('streamDestroyed', streamDestroyedHandler);

				SocketProxy.findPartner(mySession.sessionId);
				partnerSession = null;
			}

			function streamDestroyedHandler(event) {
				partnerSession.disconnect();
			}
		};

		var wait = function() {
			ele.notificationContainer.innerHTML = "Nobody to talk to.  When someone comes, you'll be the first to know!";
		};

		return {
			init: init,
			next: next,
			subscribe: subscribe,
			wait: wait
		};

	}();

})();

The client-side application script works like this:

  1. Client initializes socket connection and connects to server (line 3 and line 35).
  2. When client connects to socket server, it sends an initial event to the server (line 5).
  3. Client will receive an initial event response from the server that contains an OpenTok session for the user to publish to (line 16). Then we call RouletteApp.init() and pass in the OpenTok session ID to connect to the session and publish the webcam stream (line 67).
  4. When the user clicks the next button (line 75), it disconnects the current partner from his session and disconnects himself from his partner’s session (line 109). In turn this triggers the sessionDisconnectedHandler (line 138) which then calls a method that asks the server to find them a new partner (line 39).
  5. In response, the client will receive either a subscribe event or an empty event from the server (line 21, line 28). On an empty event, the app will do nothing other than set a message saying there is nobody to talk to (line 152). On a subscribe event, the client will connect to his partner’s session and subscribe to her stream (line 119).

Conclusion

That sums up the implementation. I deployed the app using Heroku, which recently added official support for Node.js — you can learn how to set this up here.

If you have any questions, comments, or ideas related to node, socket.io, chat roulette, or OpenTok — please post them here.

You can check out the app running here.
You can view the GitHub repo here.
You can, if you would like, follow me on twitter here.