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:

#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

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.

#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

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.

- (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];
}

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.

- (void)request:(RKRequest*)request didLoadResponse:(RKResponse*)response {
    NSString* handshakeToken = [[[response bodyAsString] componentsSeparatedByString:@":"] objectAtIndex:0];
    [self socketConnect:handshakeToken];
}

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.

- (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];
}

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.

- (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];
    }
}

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.

- (void)didReceiveInitialEvent:(NSDictionary *)args {
    _mySession = [[OTSession alloc] initWithSessionId:[args objectForKey:@"sessionId"] delegate:self];
    [_mySession connectWithApiKey:apiKey token:[args objectForKey:@"token"]];
}

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.

- (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)];
    }
}

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.

- (void)publisherDidStartStreaming:(OTPublisher*)publisher
{
    [self socketSendNextEvent];
}

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

    [_webSocket send:message];
}

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.

- (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!";
}

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.

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

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

- (void)subscriberDidConnectToStream:(OTSubscriber*)subscriber
{
    [self.view addSubview:subscriber.view];
    [subscriber.view setFrame:CGRectMake(0, topOffset, widgetWidth, widgetHeight)];
}

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.

- (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];
}

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:

// 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];
    });
  });
};

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.

  • Czzsunset

    hi, when I imported RestKit and SocketRocket,it can build, but after imported the opentok and build then, it has a compile error:
    ld: duplicate symbol _NewBase64Decode in /Users/User/Desktop/Projects/RandonFaceTime/Opentok.framework/Opentok(Base64Util.o) and /Users/User/Library/Developer/Xcode/DerivedData/RandonFaceTime-frxovtvswxokgkexxbxedktouuvp/Build/Products/Debug-iphonesimulator/libRestKit.a(NSData+Base64.o) for architecture i386
    clang: error: linker command failed with exit code 1 (use -v to see invocation)

    how to solve this problem?
    all libs are downloaded from github recently, Xcode version is 4.3.2

  • Czzsunset

    I remove the -ObjC tag in Other linker flags and it works fine

  • Michael Titus

    I’m having a lot of trouble compiling this project “out of the box” on Lion using Xcode 4.3.2:

    1) First, build failed with lots of errors like:

    Warning: Multiple build commands for output file /Users/administrator/Library/Developer/Xcode/DerivedData/RouletteTok-edoctvyxxogtvuankanuiprwkmtl/Build/Products/Release-iphonesimulator/RouletteTok.app/BG.png

    I resolved this by navigating to app target -> build phases -> copy bundle resources and deleting the files marked in red.

    2) Then build failed with this error:

    DataModelCompile /Users/administrator/Library/Developer/Xcode/DerivedData/RouletteTok-edoctvyxxogtvuankanuiprwkmtl/Build/Products/Debug-iphonesimulator/RouletteTok.app/RKTwitterCoreData.mom RestKit/Examples/RKTwitterCoreData/Resources/RKTwitterCoreData.xcdatamodel
    cd /Users/administrator/Documents/RouletteTok-iOS
    setenv PATH “/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/usr/bin:/Applications/Xcode.app/Contents/Developer/usr/bin:/usr/bin:/bin:/usr/sbin:/sbin”
    /Applications/Xcode.app/Contents/Developer/usr/bin/momc -XD_MOMC_SDKROOT=/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator5.1.sdk -XD_MOMC_IOS_TARGET_VERSION=5.0 -MOMC_PLATFORMS iphonesimulator -MOMC_PLATFORMS iphoneos -XD_MOMC_TARGET_VERSION=10.6 /Users/administrator/Documents/RouletteTok-iOS/RestKit/Examples/RKTwitterCoreData/Resources/RKTwitterCoreData.xcdatamodel /Users/administrator/Library/Developer/Xcode/DerivedData/RouletteTok-edoctvyxxogtvuankanuiprwkmtl/Build/Products/Debug-iphonesimulator/RouletteTok.app/RKTwitterCoreData.mom

    Command /Applications/Xcode.app/Contents/Developer/usr/bin/momc failed with exit code 1

    I resolved this by following the directions in this post:
    http://stackoverflow.com/questions/7705206/momc-error-with-xcode4-and-data-model-compile

    3) Then, the build failed with this error:

    Lexical or Preprocessor Issue
    ‘RestKit/RestKit.h’ file not found

  • Michael Titus

    I’m having a lot of trouble building this project “out of the box” on OS/X Lion using Xcode 4.3.2:

    git clone –recursive git://github.com/jonmumm/RouletteTok-iOS.git

    Build Clean, then Build succeeds for the (default) RKMacOSX scheme. When I switch to the RouletteTok scheme for my iPad, Build fails:

    1) First, there are 11 of warnings like this:
    Warning: Multiple build commands for output file /Users/administrator/Library/Developer/Xcode/DerivedData/RouletteTok-edoctvyxxogtvuankanuiprwkmtl/Build/Products/Release-iphoneos/RouletteTok.app/users.json

    I navigated to Build Phases (for RouletteTok), Copy Bundle Resources, and deleted the indicated files.

    2) Now the build fails with many errors like these:
    /Users/administrator/Documents/RouletteTok-iOS/RestKit/Code/CoreData/NSManagedObject+ActiveRecord.m:33:10: error: receiver type ‘RKManagedObjectStore’ for instance message is a forward declaration
    return [[[RKObjectManager sharedManager] objectStore] managedObjectContext];
    ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    /Users/administrator/Documents/RouletteTok-iOS/RestKit/Code/CoreData/NSManagedObject+ActiveRecord.m:42:34: error: ‘autorelease’ is unavailable: not available in automatic reference counting mode
    NSFetchRequest *fetchRequest = [[[NSFetchRequest alloc] init] autorelease];
    ^
    /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS5.1.sdk/System/Library/Frameworks/Foundation.framework/Headers/NSObject.h:37:1: note: declaration has been explicitly marked unavailable here
    - (id)autorelease NS_AUTOMATED_REFCOUNT_UNAVAILABLE;
    ^
    /Users/administrator/Documents/RouletteTok-iOS/RestKit/Code/CoreData/NSManagedObject+ActiveRecord.m:42:34: error: ARC forbids explicit message send of ‘autorelease’
    NSFetchRequest *fetchRequest = [[[NSFetchRequest alloc] init] autorelease];

    3) I tried selecting the project, and then Edit -> Refactor -> Convert to Objective-C ARC. This failed with 174 errors like:
    /Users/administrator/Documents/RouletteTok-iOS/RouletteTok/RouletteViewController.m:21:13: error: @synthesize of ‘weak’ property is only allowed in ARC or GC mode
    @synthesize statusField = _statusField;

    4) According to this post:
    http://stackoverflow.com/questions/6646052/how-can-i-disable-arc-for-a-single-file-in-a-project

    it looks like I have to add the -fno-objc-arc compiler flag to some or all source files?

    Mike

  • Michael Titus

    Continuing from previous post, I added the -fno-objc-arc compiler flag to all the source files which appear in Build Phases -> Compile Sources

    Now I get 49 compile errors like these:

    In file included from /Users/administrator/Documents/RouletteTok-iOS/RestKit/Code/Support/Parsers/XML/RKXMLParserLibXML.m:20:
    /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS5.1.sdk/usr/include/libxml2/libxml/parser.h:15:10: fatal error: ‘libxml/xmlversion.h’ file not found
    #include

    /Users/administrator/Documents/RouletteTok-iOS/RestKit/Examples/RKMacOSX/RKMacOSX/main.m:9:9: fatal error: ‘Cocoa/Cocoa.h’ file not found
    #import

    etc, etc.

  • Pingback: Chatroulette on iOS with node.js, socket.io & OpenTok | Tokbox Blog | back2dev | Scoop.it

  • Roger

    Looks good, thanks, also added it in http://www.peoplechatroulette.com, its uses the same code

  • Roger

    And seen on a dutch version http://www.chatroulette88.nl, think its all jabbercam, but can be fully edited

  • Roger

    on chatroulettesite.nl seems to be the same

  • Akhileshwar

    Hi here you are using RKClint, how can I make same request using RKObjectManagar

  • Akhileshwar

    Hi Jan Mumm

    I am implementing chat fun chatroulatte in my app, here in init handshake you have use

    [RKClient clientWithBaseURL:[NSString stringWithFormat:@"http://%@", serverUrl]];

    10 NSTimeInterval time = [[NSDate date] timeIntervalSince1970];

    11 time = time * 1000;

    12 [[RKClient sharedClient] get:[NSString stringWithFormat:@"/socket.io/1?t=%.0f", time] delegate:self];

    How can I create RKRequest kind of object with RKObjectManager (new rest kit 0.2.0),
    please sould you suggest me

  • james

    When i used http://www.powchat.com the same code was used, It was quite buggy but i guess the author fixed the bug.