Chatroulette on iOS with node.js, socket.io & OpenTok

We’re going to create a implementation of chat roulette that works on iOS devices. We’ll use OpenTok for handling the video streams, node.js for the webserver, and socket.io for messaging.

In a previous tutorial, I covered how to build chat roulette on the web using JavaScript. This tutorial will focus on how to build it for iOS. Both the iOS and web app will be able to interoperate with each other.

Check out the web version of the app here.
Check out the GitHub repo here.

iOS Application

The iOS app is a Single View Application Xcode project. I made my project with name RouletteTok, class prefix Roulette, and device family iPhone.

There’s a few dependencies we need to include.

The two files we need to edit are the main ViewController header and method files. Mine are called RouletteViewController.h and RouletteViewController.m.

Storyboard

There’s a couple UI elements we need to set up in the storyboard. A label, to display a status message, and a button, to request to chat with the next person.

RouletteViewController.h

Our ViewController header file is relatively simple:

[objc wraplines=”false”]
#import <UIKit/UIKit.h>
#import <RestKit/RestKit.h>
#import "SRWebSocket.h"
#import <Opentok/Opentok.h>

@interface RouletteViewController : UIViewController <RKRequestDelegate, SRWebSocketDelegate, OTSessionDelegate, OTPublisherDelegate, OTSubscriberDelegate>
– (IBAction)nextButton;
@property (weak, nonatomic) IBOutlet UILabel *statusField;

@end
[/objc]

First we import the libraries we’re using: RestKit, SocketRocket, OpenTok.

Then we add the delegate protocols from those libraries. RKRequestDelegate will give us methods that get called after an HTTP request is made, SRWebSocketDelegate will give us methods that get called when new socket messages come in, and the OT Delegates will give us methods that get called when we connect to an OpenTok session, publish a video stream, and subscribe to a video stream.

Lastly, we connect the “Next” button to call the nextButton method in our ViewController, and connect the label to show the value of the statusField property.

RouletteViewController.m

Our ViewController method file is where most of the work is done. We’re going to walk through it in pieces.

[objc wraplines=”false”]
#import "RouletteViewController.h"

@implementation RouletteViewController {
SRWebSocket *_webSocket; // Socket that connects to socket.io

OTSession *_mySession; // Session that belongs to this user
OTPublisher *_publisher; // Publisher that belongs to this user

OTSubscriber *_subscriber; // Subscriber of the user chatting to
OTSession *_partnerSession; // Session that the user chatting to
}

@synthesize statusField = _statusField;

static int topOffset = 38;
static double widgetHeight = 216; // Height of stream
static double widgetWidth = 288; // Width of stream
static NSString* const apiKey = @"413302"; // OpenTok API key
static NSString* const serverUrl = @"roulettetok.com"; // Location of socket.io server
[/objc]

First, we set up the private variables we need, the webSocket and a bunch of OpenTok objects. We’ll describe these more later.

Next, we set a bunch of constant variables. You will want to change the apiKey to be your OpenTok API key, and serverUrl to be the location of where your node server is running.

[objc wraplines=”false”]
– (void)viewDidLoad
{
[super viewDidLoad];
[self initHandshake];
}

– (void)initHandshake
{
[RKClient clientWithBaseURL:[NSString stringWithFormat:@"http://%@", serverUrl]];
NSTimeInterval time = [[NSDate date] timeIntervalSince1970];
time = time * 1000;
[[RKClient sharedClient] get:[NSString stringWithFormat:@"/socket.io/1?t=%.0f", time] delegate:self];
}
[/objc]

The viewDidLoad method is called when the view loads. The only thing we do is call our initHandshake method.

The initHandshake method is where we begin the process of connecting to our socket.io server. Socket.io has a specific protocol clients must implement to connect to it. It works like this: client sends a GET request to the server at /socket.io/1?t=530883171853922706 where t is a UNIX timestamp. The server will respond with an id we will use to connect to the socket.

On line 8 above we make the GET request to the server using RestKit as an HTTP client and then wait for the response.

[objc wraplines=”false”]
– (void)request:(RKRequest*)request didLoadResponse:(RKResponse*)response {
NSString* handshakeToken = [[[response bodyAsString] componentsSeparatedByString:@":"] objectAtIndex:0];
[self socketConnect:handshakeToken];
}
[/objc]

This is a delegate method that gets called by RestKit when our HTTP response comes in. We parse the response to get the token and then call a method that initiates the socket connection.

[objc wraplines=”false”]
– (void)socketConnect:(NSString*)token
{
_webSocket = [[SRWebSocket alloc] initWithURLRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:[NSString stringWithFormat:@"ws://%@/socket.io/1/websocket/%@", serverUrl, token]]]];
_webSocket.delegate = self;

[_webSocket open];
}
[/objc]

This takes our token, and uses the SocketRocket library to connect to socket.io. Once the socket connects, the server is going to start dispatching us messages.

[objc wraplines=”false”]
– (void)webSocket:(SRWebSocket *)webSocket didReceiveMessage:(NSString *)message {
NSError *jsonError;
NSData *data = [[[message componentsSeparatedByString:@":::"] lastObject]dataUsingEncoding:NSUTF8StringEncoding];
NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&jsonError];

NSString *event = [json objectForKey:@"name"];
NSDictionary *args = [[json objectForKey:@"args"] objectAtIndex:0];

if ([event isEqualToString:@"initial"]) {
[self didReceiveInitialEvent:args];
} else if ([event isEqualToString:@"subscribe"]) {
[self didReceiveSubscribeEvent:args];
} else if ([event isEqualToString:@"empty"]) {
[self didReceiveEmptyEvent];
} else if ([event isEqualToString:@"disconnectPartner"]) {
[self didReceiveDisconnectPartnerEvent];
}
}
[/objc]

This method gets called when socket.io sends us a new socket message. It parses the message, and grabs any arguments that came along with it, then calls the appropriate method in your app. There are four socket message events the server sends, “initial”, “subscribe”, “empty”, and “disconnectPartner”. We’ll explain when each one of those are sent and what we should do when we get them.

[objc wraplines=”false”]
– (void)didReceiveInitialEvent:(NSDictionary *)args {
_mySession = [[OTSession alloc] initWithSessionId:[args objectForKey:@"sessionId"] delegate:self];
[_mySession connectWithApiKey:apiKey token:[args objectForKey:@"token"]];
}
[/objc]

The didReceiveInitialEvent method is called when we get an “initial” event from the server. The server sends us this event as soon as the socket connection is established.

On this event we get an OpenTok session and token that was generated on the server. A session is a room in which we can publish and subscribe to video streams. Here we connect to the OpenTok session so we can start publishing our video stream.

[objc wraplines=”false”]
– (void)sessionDidConnect:(OTSession*)session
{
// Starts publishing if the connected session is the users own session
if ([session.sessionId isEqualToString:_mySession.sessionId]) {
_publisher = [[OTPublisher alloc] initWithDelegate:self];
[_publisher setName:[[UIDevice currentDevice] name]];
[_mySession publish:_publisher];
[self.view addSubview:_publisher.view];
[_publisher.view setFrame:CGRectMake(0, topOffset+widgetHeight, widgetWidth, widgetHeight)];
}
}
[/objc]

Once the session is connected, the sessionDidConnect method gets called. There we publish this users video stream to the session, then places the video stream in the view.

[objc wraplines=”false”]
– (void)publisherDidStartStreaming:(OTPublisher*)publisher
{
[self socketSendNextEvent];
}

– (void)socketSendNextEvent {
NSString *message = [NSString stringWithFormat:@"5:::{\"name\":\"next\",\"args\":[{\"sessionId\":\"%@\"}]}", _mySession.sessionId];

[_webSocket send:message];
}
[/objc]

When the stream starts publishing the video, the publisherDidStartStreaming method is called. There we call a method that sends a “next” event to the socket server. This event asks the server to give us a new partner to chat with.

When we send the “next” event, the server will come back with one of two possible responses: 1) “empty” if there is nobody available to chat with, or 2) “subscribe” if there is somebody available.

[objc wraplines=”false”]
– (void)didReceiveEmptyEvent {
self.statusField.text = @"Nobody to talk to. Waiting…";
}

– (void)didReceiveSubscribeEvent:(NSDictionary *)args {
_partnerSession = [[OTSession alloc] initWithSessionId:[args objectForKey:@"sessionId"] delegate:self];
[_partnerSession connectWithApiKey:apiKey token:[args objectForKey:@"token"]];

self.statusField.text = @"Have fun!";
}
[/objc]

When we receive empty, we just update the status message to say there is nobody there.

When we receive the subscribe event, we are passed session information for the person we are going to chat with (referred to in code as partner). We can expect that our partner is in that session publishing a video stream.

Anytime we connect to a session, if there are video streams going on in that session, the didReceiveStream method will be called with information about the stream.

[objc wraplines=”false”]
– (void)session:(OTSession*)session didReceiveStream:(OTStream*)stream
{
if (stream.connection.connectionId != _mySession.connection.connectionId) {
_subscriber = [[OTSubscriber alloc] initWithStream:stream delegate:self];
}
}
[/objc]

When the didReceiveStream method gets called, we initialize a subscriber using the stream. The subscriber object is what actually displays the video stream.

[objc wraplines=”false”]
– (void)subscriberDidConnectToStream:(OTSubscriber*)subscriber
{
[self.view addSubview:subscriber.view];
[subscriber.view setFrame:CGRectMake(0, topOffset, widgetWidth, widgetHeight)];
}
[/objc]

The subscriberDidConnectToStream is called when the subscriber connects to the stream, in which we then add it to the view to be displayed.

At this point, we have our own session that we publish a video stream to, which my partner connects and subscribes to. The socket server has sent us a session from a partner that we have connected and subscribed to. We should now both be able to see each other.

The last thing left to do is handle what happens when we want to switch to a new partner.

[objc wraplines=”false”]
– (IBAction)nextButton
{
if (_partnerSession.sessionConnectionStatus == OTSessionConnectionStatusConnected) {
[self socketSendDisconnectPartnersEvent];
} else {
[self socketSendNextEvent];
}
}

– (void)socketSendDisconnectPartnersEvent {
NSString *message = @"5:::{\"name\":\"disconnectPartners\"}";

[_webSocket send:message];
}

– (void)didReceiveDisconnectPartnerEvent {
[_partnerSession disconnect];
}

– (void)sessionDidDisconnect:(OTSession*)session
{
[self socketSendNextEvent];
}

[/objc]

The nextButton method is called when the user hits the “Next” button. It checks to see if we are currently connected to another partner.

If we are not connected to a partner, we just send a “next” event to the server, which we used before, which asks the server to give us a new partner.

If we are connected to a partner, we have to get rid of our current partner before asking for a new one. To do that, we send a “disconnectPartners” event to the socket server (line 4).

The socket server will send back “disconnectPartner” event, which calls our method didReceiveDisconnectPartnerEvent to handle that event (line 17). There we just disconnect from our partner’s session (and our partner will get the same event and disconnect from ours).

Finally, after we disconnect from our partner’s session, the sessionDidDisconnect method gets called. Since now we no longer have a partner, we can ask the server for a new partner by sending a “next” event (line 22).

The Socket Server

Now we need to implement the socket server that our app connects to.

I’m not going to explain setting up the Node HTTP server, just look through the source code. If you need more guidance, the JavaScript version of this tutorial goes in to more depth. Instead, I’m going to explain the socket server implementation.

socketapp.js

Here is the complete socket server implementation, we will discuss it in detail below:

[javascript wraplines=”false”]
// 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 = [];
var clients = {}

// Sets up the socket server
exports.start = function(sockets) {
sockets.on(‘connection’, function(socket) {
clients[socket.id] = socket;

ot.createSession(‘localhost’, {}, function(session) {

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

// Send initialization data back to the client
socket.emit(‘initial’, data);
});

socket.on(‘next’, function (data) {
// Create a "user" data object for me
var me = {
sessionId: data.sessionId,
socketId: socket.id
};

var partner;
var partnerSocket;
// 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 (socket.partner != tmpUser) {
// Get the socket client for this user
partnerSocket = clients[tmpUser.socketId];

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

// If the user we found exists…
if (partnerSocket) {
// 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
socket.emit(‘subscribe’, {
sessionId: partner.sessionId,
token: ot.generateToken({
sessionId: partner.sessionId,
role: opentok.Roles.SUBSCRIBER
})
});

// Tell my partner to subscribe to me
partnerSocket.emit(‘subscribe’, {
sessionId: me.sessionId,
token: ot.generateToken({
sessionId: me.sessionId,
role: opentok.Roles.SUBSCRIBER
})
});

// Mark that my new partner and me are partners
socket.partner = partner;
partnerSocket.partner = me;

// Mark that we are not in the list of solo users anymore
socket.inlist = false;
partnerSocket.inlist = false;

} else {

// delete that i had a partner if i had one
if (socket.partner) {
delete socket.partner;
}

// add myself to list of solo users if i’m not in the list
if (!socket.inlist) {
socket.inlist = true;
soloUsers.push(me);
}

// tell myself that there is nobody to chat with right now
socket.emit(’empty’);
}
});

socket.on(‘disconnectPartners’, function() {
if (socket.partner && socket.partner.socketId) {
var partnerSocket = clients[socket.partner.socketId]

if (partnerSocket) {
partnerSocket.emit(‘disconnectPartner’);
}

socket.emit(‘disconnectPartner’);
}
});

socket.on(‘disconnect’, function() {
delete clients[socket.id];
});
});
};
[/javascript]

The purpose of the socket server is to pair users together to talk to each other.

When a new user connects to the socket (line 11), we generate a session id and token and pass it down through the ‘initial’ event (line 26), which the client then uses to connect to and publish a stream.

After the initial set up, there are only two events the socket server must listen for and respond to: 1) the next event on and 2) the disconnectPartners event.

next Event

This event (line 29) 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.

disconnectPartners Event

This event (line 106 ) is sent by clients when they want to terminate the current conversation. It grabs the partner of the person who sent the event, and then sends a message to both him and his partner to disconnect from each other.

Conclusion

That sums up the implementation. If you would like more explanation of the server-side and web app implementation, the previous tutorial cover this in more.

View the app here.
View the GitHub repo here.

Ask me questions on Twitter @jonmumm.