Creating chatroulette with node.js, socket.io & OpenTok

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://roulettetok.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.

  • Anonymous

    Nice work, looks useful!

  • http://twitter.com/GabrielMtn Gabriel Martin

    The audio latency is quite an issue, any ideas on a solution? u00a0Love the concept though

    • http://www.tokbox.com/ TokBox

      how much latency did you experience? we will def look into it but in my experience this was not a problem.

  • Dick

    It would be fantaaaastic to get slightly more detailed instructions for deploying this specific app to Heroku… running into problems, and getting the feeling it’s something that a wiser man should already know.

  • Pingback: JavaScript Magazine Blog for JSMag » Blog Archive » News roundup: Paper.js, Fathom.js, test262

  • Pingback: Creating chat roulette with node.js, socket.io, and OpenTok | blog.mumm.me

  • Pingback: Comment créer un espace collaboratif de travail avec node.js, socket.io et OpenTok | technologies de l'information et de la communication | Scoop.it

  • Rilly Bennekamp

    hi,nfantastic job thank you.ni have no problem running locally, but i receive a couple socket.io related errors when i tryt o use with heroku…nmy console prints:n[DEBUG] opentok: TB.setLogLevel(4)n>GET http://localhost:13245/socket.io/xhr-multipart/ (X)n>GET http://localhost:13245/socket.io/xhr-polling/1312556082771 Aborted (X)nni followed the basics ofu00a0nhttp://devcenter.heroku.com/articles/node-js#deploy_to_herokucedarnto get node running and i am able to see the roulettTok screen, i just never get the “connecting…” message or anything after.

    • Rilly Bennekamp

      cleaned up errors but still never get to connecting part when running from heroku

      • Rilly Bennekamp

        new errors.nhttp://undefined/socket.io/1/?t=1312746434203&jsonp=0nhttp://undefined/socket.io/1/?t=1312746434203&jsonp=1nnwhere & when is it getting undefined from?

        • http://twitter.com/JonMumm Jonathan Mumm

          In order for it to work on heroku you have to set the following environment variable:nnNODE_ENV=productionnnThere are instructions on how to do that on Heroku here:u00a0http://devcenter.heroku.com/articles/config-varsnnAddtionally, there is a line in in ‘app.js’ that you have to set to be the address of your Heroku appnnapp.configure(‘production’, function() {tapp.set(‘port’, 80);tapp.set(‘address’, “myapp.heroku.com”); // <– SET THIS TO YOURStapp.use(express.errorHandler());});

  • omegatai

    For some reason, I’m having trouble with the variable config declared in the jade template file (index.jade) that isn’t passed to the javascript file (public/javascripts/app.js), which then makes the javascript crash. Any ideas?? Thanks by the way for this great post!

  • Ismalakazel

    I’m having some problems with this app. Apparently a few blocks of code had to be changes due to the new socket.io version >= 0.7.
    I read the migration tutorial here https://github.com/LearnBoost/Socket.IO/wiki/Migrating-0.6-to-0.7+

    The app still doesn’t work! and the only debug info that I receive in terminal is

    warn – websocket connection invalid
    info – transport end

    I’m not sure what to do here, im not an expert in this matter, so any help would be greatly appreciated.

    • http://twitter.com/JonMumm Jonathan Mumm

      Hey Ismalakazel,

      I’m updating this tutorial for the new version of socket.io. I’ll post the revised code shortly

      • http://twitter.com/awhillas Alexander Whillas

        hey, i was just butting my brains out trying to update the code for socket.io 0.9, which is a big leap from 0.6 (which this code is for). well, there is a neat migration guide here: https://github.com/LearnBoost/Socket.IO/wiki/Migrating-0.6-to-0.7+ but the dynamics of the whole thing are different with the polling and all. Looking forward to the updated article bro!

  • http://memojo.com/~sgala/blog/index.html Santiago Gala

    It doesn’t work here. I’m not getting asked authorization for cam or audio (Chromium on Ubuntu 10.04). Any clue? I already ensured that the flash configuration has “always ask” in cam and audio…

  • http://www.facebook.com/gvenkatesh.ilu Venkatesh Gajula

    i need to add frames over around he frame please help me my skype:gvcyber1

  • http://www.onlineroulettebetting.com/ tom

    always wanted to know about this! great work

  • donmac

    the running app example tries to connect, but nothing more. Wondering if the underpinnings have changed? Would love to see it in action.

  • فلل بالرياض

    liked the article because it has all details which i wanted.

  • leetfire666

    Thanks for the tutorial. I got it to kind of work but I’m having a little problem. The user that starts the session isn’t getting the video feed from the client that is connected to it. But the client that is connecting to the session gets both feeds fine. I feel like there needs to be an additional publish line in the ‘subscribe’ part of the app.js. Any thoughts? Really want to figure this out.

  • sahil

    hey ur app hosted at heroku in not working :)

  • Robet

    thanks, used it too for peoplechatroulette .com