iAppRTCDemo: WebSocket based signaling.

Updates the iOS code to use the new signaling model. Removes old Channel API
code. Note that this no longer logs messages to UI. UI update forthcoming.

BUG=
R=glaznev@webrtc.org, jiayl@webrtc.org

Review URL: https://webrtc-codereview.appspot.com/35369004

git-svn-id: http://webrtc.googlecode.com/svn/trunk@7852 4adac7df-926f-26a2-2b94-8c16560cd09d
diff --git a/talk/examples/objc/AppRTCDemo/APPRTCAppClient.h b/talk/examples/objc/AppRTCDemo/APPRTCAppClient.h
deleted file mode 100644
index 880d5f8..0000000
--- a/talk/examples/objc/AppRTCDemo/APPRTCAppClient.h
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * libjingle
- * Copyright 2013, Google Inc.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- *  1. Redistributions of source code must retain the above copyright notice,
- *     this list of conditions and the following disclaimer.
- *  2. Redistributions in binary form must reproduce the above copyright notice,
- *     this list of conditions and the following disclaimer in the documentation
- *     and/or other materials provided with the distribution.
- *  3. The name of the author may not be used to endorse or promote products
- *     derived from this software without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
- * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
- * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
- * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
- * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
- * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
- * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
- * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-#import <Foundation/Foundation.h>
-
-#import "ARDSignalingParams.h"
-#import "GAEChannelClient.h"
-
-@class APPRTCAppClient;
-@protocol APPRTCAppClientDelegate
-
-- (void)appClient:(APPRTCAppClient*)appClient
-    didErrorWithMessage:(NSString*)message;
-- (void)appClient:(APPRTCAppClient*)appClient
-    didReceiveICEServers:(NSArray*)servers;
-
-@end
-
-@class RTCMediaConstraints;
-
-// Negotiates signaling for chatting with apprtc.appspot.com "rooms".
-// Uses the client<->server specifics of the apprtc AppEngine webapp.
-//
-// To use: create an instance of this object (registering a message handler) and
-// call connectToRoom().  apprtc.appspot.com will signal that is successful via
-// onOpen through the browser channel.  Then you should call sendData() and wait
-// for the registered handler to be called with received messages.
-@interface APPRTCAppClient : NSObject
-
-@property(nonatomic, readonly) ARDSignalingParams *params;
-@property(nonatomic, weak) id<APPRTCAppClientDelegate> delegate;
-
-- (instancetype)initWithDelegate:(id<APPRTCAppClientDelegate>)delegate
-                  messageHandler:(id<GAEMessageHandler>)handler;
-- (void)connectToRoom:(NSURL *)room;
-- (void)sendData:(NSData *)data;
-
-#ifndef DOXYGEN_SHOULD_SKIP_THIS
-// Disallow init and don't add to documentation
-- (instancetype)init __attribute__((
-    unavailable("init is not a supported initializer for this class.")));
-#endif /* DOXYGEN_SHOULD_SKIP_THIS */
-
-@end
diff --git a/talk/examples/objc/AppRTCDemo/APPRTCAppClient.m b/talk/examples/objc/AppRTCDemo/APPRTCAppClient.m
deleted file mode 100644
index 20e708d..0000000
--- a/talk/examples/objc/AppRTCDemo/APPRTCAppClient.m
+++ /dev/null
@@ -1,223 +0,0 @@
-/*
- * libjingle
- * Copyright 2013, Google Inc.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- *  1. Redistributions of source code must retain the above copyright notice,
- *     this list of conditions and the following disclaimer.
- *  2. Redistributions in binary form must reproduce the above copyright notice,
- *     this list of conditions and the following disclaimer in the documentation
- *     and/or other materials provided with the distribution.
- *  3. The name of the author may not be used to endorse or promote products
- *     derived from this software without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
- * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
- * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
- * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
- * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
- * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
- * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
- * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-#if !defined(__has_feature) || !__has_feature(objc_arc)
-#error "This file requires ARC support."
-#endif
-
-#import "APPRTCAppClient.h"
-
-#import <dispatch/dispatch.h>
-
-#import "ARDSignalingParams.h"
-#import "ARDUtilities.h"
-#import "GAEChannelClient.h"
-#import "RTCICEServer.h"
-#import "RTCICEServer+JSON.h"
-#import "RTCMediaConstraints.h"
-#import "RTCPair.h"
-
-@implementation APPRTCAppClient {
-  dispatch_queue_t _backgroundQueue;
-  GAEChannelClient* _gaeChannel;
-  NSURL* _postMessageURL;
-  BOOL _verboseLogging;
-  __weak id<GAEMessageHandler> _messageHandler;
-}
-
-- (instancetype)initWithDelegate:(id<APPRTCAppClientDelegate>)delegate
-                  messageHandler:(id<GAEMessageHandler>)handler {
-  if (self = [super init]) {
-    _delegate = delegate;
-    _messageHandler = handler;
-    _backgroundQueue = dispatch_queue_create("RTCBackgroundQueue",
-                                             DISPATCH_QUEUE_SERIAL);
-    // Uncomment to see Request/Response logging.
-    // _verboseLogging = YES;
-  }
-  return self;
-}
-
-- (void)connectToRoom:(NSURL*)url {
-  NSString *urlString =
-      [[url absoluteString] stringByAppendingString:@"&t=json"];
-  NSURL *requestURL = [NSURL URLWithString:urlString];
-  NSURLRequest *request = [NSURLRequest requestWithURL:requestURL];
-  [NSURLConnection sendAsynchronousRequest:request
-                         completionHandler:^(NSURLResponse *response,
-                                             NSData *data,
-                                             NSError *error) {
-    NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
-    int statusCode = [httpResponse statusCode];
-    [self logVerbose:[NSString stringWithFormat:
-        @"Response received\nURL\n%@\nStatus [%d]\nHeaders\n%@",
-        [httpResponse URL],
-        statusCode,
-        [httpResponse allHeaderFields]]];
-    NSAssert(statusCode == 200,
-             @"Invalid response of %d received while connecting to: %@",
-             statusCode,
-             urlString);
-    if (statusCode != 200) {
-      return;
-    }
-    [self handleResponseData:data forRoomRequest:request];
-  }];
-}
-
-- (void)sendData:(NSData*)data {
-  NSParameterAssert([data length] > 0);
-  NSString *message = [NSString stringWithUTF8String:[data bytes]];
-  [self logVerbose:[NSString stringWithFormat:@"Send message:\n%@", message]];
-  if (!_postMessageURL) {
-    return;
-  }
-  NSMutableURLRequest *request =
-      [NSMutableURLRequest requestWithURL:_postMessageURL];
-  request.HTTPMethod = @"POST";
-  [request setHTTPBody:data];
-  [NSURLConnection sendAsynchronousRequest:request
-                         completionHandler:^(NSURLResponse *response,
-                                             NSData *data,
-                                             NSError *error) {
-    NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
-    int status = [httpResponse statusCode];
-    NSString *responseString = [data length] > 0 ?
-        [NSString stringWithUTF8String:[data bytes]] :
-        nil;
-    NSAssert(status == 200,
-             @"Bad response [%d] to message: %@\n\n%@",
-             status,
-             message,
-             responseString);
-  }];
-}
-
-#pragma mark - Private
-
-- (void)logVerbose:(NSString *)message {
-  if (_verboseLogging) {
-    NSLog(@"%@", message);
-  }
-}
-
-- (void)handleResponseData:(NSData *)responseData
-            forRoomRequest:(NSURLRequest *)request {
-  ARDSignalingParams *params =
-      [ARDSignalingParams paramsFromJSONData:responseData];
-  if (params.errorMessages.count > 0) {
-    NSMutableString *message = [NSMutableString string];
-    for (NSString *errorMessage in params.errorMessages) {
-      [message appendFormat:@"%@\n", errorMessage];
-    }
-    [self.delegate appClient:self didErrorWithMessage:message];
-    return;
-  }
-  [self requestTURNServerForICEServers:params.iceServers
-                         turnServerUrl:[params.turnRequestURL absoluteString]];
-  NSString *token = params.channelToken;
-  [self logVerbose:
-      [NSString stringWithFormat:@"About to open GAE with token:  %@",
-                                 token]];
-  _gaeChannel =
-      [[GAEChannelClient alloc] initWithToken:token
-                                     delegate:_messageHandler];
-  _params = params;
-  // Generate URL for posting data.
-  NSDictionary *roomJSON = [NSDictionary dictionaryWithJSONData:responseData];
-  _postMessageURL = [self parsePostMessageURLForRoomJSON:roomJSON
-                                                 request:request];
-  [self logVerbose:[NSString stringWithFormat:@"POST message URL:\n%@",
-                                              _postMessageURL]];
-}
-
-- (NSURL*)parsePostMessageURLForRoomJSON:(NSDictionary*)roomJSON
-                                 request:(NSURLRequest*)request {
-  NSString* requestUrl = [[request URL] absoluteString];
-  NSRange queryRange = [requestUrl rangeOfString:@"?"];
-  NSString* baseUrl = [requestUrl substringToIndex:queryRange.location];
-  NSString* roomKey = roomJSON[@"room_key"];
-  NSParameterAssert([roomKey length] > 0);
-  NSString* me = roomJSON[@"me"];
-  NSParameterAssert([me length] > 0);
-  NSString* postMessageUrl =
-      [NSString stringWithFormat:@"%@/message?r=%@&u=%@", baseUrl, roomKey, me];
-  return [NSURL URLWithString:postMessageUrl];
-}
-
-- (void)requestTURNServerWithUrl:(NSString *)turnServerUrl
-               completionHandler:
-    (void (^)(RTCICEServer *turnServer))completionHandler {
-  NSURL *turnServerURL = [NSURL URLWithString:turnServerUrl];
-  NSMutableURLRequest *request =
-      [NSMutableURLRequest requestWithURL:turnServerURL];
-  [request addValue:@"Mozilla/5.0" forHTTPHeaderField:@"user-agent"];
-  [request addValue:@"https://apprtc.appspot.com"
-      forHTTPHeaderField:@"origin"];
-  [NSURLConnection sendAsynchronousRequest:request
-                         completionHandler:^(NSURLResponse *response,
-                                             NSData *data,
-                                             NSError *error) {
-    if (error) {
-      NSLog(@"Unable to get TURN server.");
-      completionHandler(nil);
-      return;
-    }
-    NSDictionary *json = [NSDictionary dictionaryWithJSONData:data];
-    RTCICEServer *turnServer = [RTCICEServer serverFromCEODJSONDictionary:json];
-    completionHandler(turnServer);
-  }];
-}
-
-- (void)requestTURNServerForICEServers:(NSArray*)iceServers
-                         turnServerUrl:(NSString*)turnServerUrl {
-  BOOL isTurnPresent = NO;
-  for (RTCICEServer* iceServer in iceServers) {
-    if ([[iceServer.URI scheme] isEqualToString:@"turn"]) {
-      isTurnPresent = YES;
-      break;
-    }
-  }
-  if (!isTurnPresent) {
-    [self requestTURNServerWithUrl:turnServerUrl
-                 completionHandler:^(RTCICEServer* turnServer) {
-      NSArray* servers = iceServers;
-      if (turnServer) {
-        servers = [servers arrayByAddingObject:turnServer];
-      }
-      NSLog(@"ICE servers:\n%@", servers);
-      [self.delegate appClient:self didReceiveICEServers:servers];
-    }];
-  } else {
-    NSLog(@"ICE servers:\n%@", iceServers);
-    dispatch_async(dispatch_get_main_queue(), ^{
-      [self.delegate appClient:self didReceiveICEServers:iceServers];
-    });
-  }
-}
-
-@end
diff --git a/talk/examples/objc/AppRTCDemo/APPRTCConnectionManager.h b/talk/examples/objc/AppRTCDemo/APPRTCConnectionManager.h
deleted file mode 100644
index 98fe755..0000000
--- a/talk/examples/objc/AppRTCDemo/APPRTCConnectionManager.h
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * libjingle
- * Copyright 2014, Google Inc.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- *  1. Redistributions of source code must retain the above copyright notice,
- *     this list of conditions and the following disclaimer.
- *  2. Redistributions in binary form must reproduce the above copyright notice,
- *     this list of conditions and the following disclaimer in the documentation
- *     and/or other materials provided with the distribution.
- *  3. The name of the author may not be used to endorse or promote products
- *     derived from this software without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
- * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
- * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
- * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
- * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
- * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
- * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
- * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-#import <Foundation/Foundation.h>
-
-// Used to log messages to destination like UI.
-@protocol APPRTCLogger<NSObject>
-- (void)logMessage:(NSString*)message;
-@end
-
-@class RTCVideoTrack;
-@class APPRTCConnectionManager;
-
-// Used to provide AppRTC connection information.
-@protocol APPRTCConnectionManagerDelegate<NSObject>
-
-- (void)connectionManager:(APPRTCConnectionManager*)manager
-    didReceiveLocalVideoTrack:(RTCVideoTrack*)localVideoTrack;
-
-- (void)connectionManager:(APPRTCConnectionManager*)manager
-    didReceiveRemoteVideoTrack:(RTCVideoTrack*)remoteVideoTrack;
-
-- (void)connectionManagerDidReceiveHangup:(APPRTCConnectionManager*)manager;
-
-- (void)connectionManager:(APPRTCConnectionManager*)manager
-      didErrorWithMessage:(NSString*)errorMessage;
-
-@end
-
-// Abstracts the network connection aspect of AppRTC. The delegate will receive
-// information about connection status as changes occur.
-@interface APPRTCConnectionManager : NSObject
-
-@property(nonatomic, weak) id<APPRTCConnectionManagerDelegate> delegate;
-@property(nonatomic, weak) id<APPRTCLogger> logger;
-
-- (instancetype)initWithDelegate:(id<APPRTCConnectionManagerDelegate>)delegate
-                          logger:(id<APPRTCLogger>)logger;
-- (BOOL)connectToRoomWithURL:(NSURL*)url;
-- (void)disconnect;
-
-@end
diff --git a/talk/examples/objc/AppRTCDemo/APPRTCConnectionManager.m b/talk/examples/objc/AppRTCDemo/APPRTCConnectionManager.m
deleted file mode 100644
index 9134e4d..0000000
--- a/talk/examples/objc/AppRTCDemo/APPRTCConnectionManager.m
+++ /dev/null
@@ -1,390 +0,0 @@
-/*
- * libjingle
- * Copyright 2014, Google Inc.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- *  1. Redistributions of source code must retain the above copyright notice,
- *     this list of conditions and the following disclaimer.
- *  2. Redistributions in binary form must reproduce the above copyright notice,
- *     this list of conditions and the following disclaimer in the documentation
- *     and/or other materials provided with the distribution.
- *  3. The name of the author may not be used to endorse or promote products
- *     derived from this software without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
- * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
- * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
- * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
- * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
- * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
- * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
- * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-#import "APPRTCConnectionManager.h"
-
-#import <AVFoundation/AVFoundation.h>
-#import "APPRTCAppClient.h"
-#import "GAEChannelClient.h"
-#import "RTCICECandidate.h"
-#import "RTCICECandidate+JSON.h"
-#import "RTCMediaConstraints.h"
-#import "RTCMediaStream.h"
-#import "RTCPair.h"
-#import "RTCPeerConnection.h"
-#import "RTCPeerConnectionDelegate.h"
-#import "RTCPeerConnectionFactory.h"
-#import "RTCSessionDescription.h"
-#import "RTCSessionDescription+JSON.h"
-#import "RTCSessionDescriptionDelegate.h"
-#import "RTCStatsDelegate.h"
-#import "RTCVideoCapturer.h"
-#import "RTCVideoSource.h"
-
-@interface APPRTCConnectionManager ()
-    <APPRTCAppClientDelegate, GAEMessageHandler, RTCPeerConnectionDelegate,
-     RTCSessionDescriptionDelegate, RTCStatsDelegate>
-
-@property(nonatomic, strong) APPRTCAppClient* client;
-@property(nonatomic, strong) RTCPeerConnection* peerConnection;
-@property(nonatomic, strong) RTCPeerConnectionFactory* peerConnectionFactory;
-@property(nonatomic, strong) RTCVideoSource* videoSource;
-@property(nonatomic, strong) NSMutableArray* queuedRemoteCandidates;
-
-@end
-
-@implementation APPRTCConnectionManager {
-  NSTimer* _statsTimer;
-}
-
-- (instancetype)initWithDelegate:(id<APPRTCConnectionManagerDelegate>)delegate
-                          logger:(id<APPRTCLogger>)logger {
-  if (self = [super init]) {
-    self.delegate = delegate;
-    self.logger = logger;
-    self.peerConnectionFactory = [[RTCPeerConnectionFactory alloc] init];
-    // TODO(tkchin): turn this into a button.
-    // Uncomment for stat logs.
-    // _statsTimer =
-    //     [NSTimer scheduledTimerWithTimeInterval:10
-    //                                      target:self
-    //                                    selector:@selector(didFireStatsTimer:)
-    //                                    userInfo:nil
-    //                                     repeats:YES];
-  }
-  return self;
-}
-
-- (void)dealloc {
-  [self disconnect];
-}
-
-- (BOOL)connectToRoomWithURL:(NSURL*)url {
-  if (self.client) {
-    // Already have a connection.
-    return NO;
-  }
-  self.client = [[APPRTCAppClient alloc] initWithDelegate:self
-                                           messageHandler:self];
-  [self.client connectToRoom:url];
-  return YES;
-}
-
-- (void)disconnect {
-  if (!self.client) {
-    return;
-  }
-  [self.client
-      sendData:[@"{\"type\": \"bye\"}" dataUsingEncoding:NSUTF8StringEncoding]];
-  [self.peerConnection close];
-  self.peerConnection = nil;
-  self.client = nil;
-  self.videoSource = nil;
-  self.queuedRemoteCandidates = nil;
-}
-
-#pragma mark - APPRTCAppClientDelegate
-
-- (void)appClient:(APPRTCAppClient*)appClient
-    didErrorWithMessage:(NSString*)message {
-  [self.delegate connectionManager:self
-               didErrorWithMessage:message];
-}
-
-- (void)appClient:(APPRTCAppClient*)appClient
-    didReceiveICEServers:(NSArray*)servers {
-  self.queuedRemoteCandidates = [NSMutableArray array];
-  RTCMediaConstraints* constraints = [[RTCMediaConstraints alloc]
-      initWithMandatoryConstraints:
-          @[
-            [[RTCPair alloc] initWithKey:@"OfferToReceiveAudio" value:@"true"],
-            [[RTCPair alloc] initWithKey:@"OfferToReceiveVideo" value:@"true"]
-          ]
-               optionalConstraints:
-                   @[
-                     [[RTCPair alloc] initWithKey:@"internalSctpDataChannels"
-                                            value:@"true"],
-                     [[RTCPair alloc] initWithKey:@"DtlsSrtpKeyAgreement"
-                                            value:@"true"]
-                   ]];
-  self.peerConnection =
-      [self.peerConnectionFactory peerConnectionWithICEServers:servers
-                                                   constraints:constraints
-                                                      delegate:self];
-  RTCMediaStream* lms =
-      [self.peerConnectionFactory mediaStreamWithLabel:@"ARDAMS"];
-
-  // The iOS simulator doesn't provide any sort of camera capture
-  // support or emulation (http://goo.gl/rHAnC1) so don't bother
-  // trying to open a local stream.
-  RTCVideoTrack* localVideoTrack;
-
-  // TODO(tkchin): local video capture for OSX. See
-  // https://code.google.com/p/webrtc/issues/detail?id=3417.
-#if !TARGET_IPHONE_SIMULATOR && TARGET_OS_IPHONE
-  NSString* cameraID = nil;
-  for (AVCaptureDevice* captureDevice in
-       [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo]) {
-    if (captureDevice.position == AVCaptureDevicePositionFront) {
-      cameraID = [captureDevice localizedName];
-      break;
-    }
-  }
-  NSAssert(cameraID, @"Unable to get the front camera id");
-
-  RTCVideoCapturer* capturer =
-      [RTCVideoCapturer capturerWithDeviceName:cameraID];
-  self.videoSource = [self.peerConnectionFactory
-      videoSourceWithCapturer:capturer
-                  constraints:self.client.params.mediaConstraints];
-  localVideoTrack =
-      [self.peerConnectionFactory videoTrackWithID:@"ARDAMSv0"
-                                            source:self.videoSource];
-  if (localVideoTrack) {
-    [lms addVideoTrack:localVideoTrack];
-  }
-  [self.delegate connectionManager:self
-         didReceiveLocalVideoTrack:localVideoTrack];
-#endif
-
-  [lms addAudioTrack:[self.peerConnectionFactory audioTrackWithID:@"ARDAMSa0"]];
-  [self.peerConnection addStream:lms];
-  [self.logger logMessage:@"onICEServers - added local stream."];
-}
-
-#pragma mark - GAEMessageHandler methods
-
-- (void)onOpen {
-  if (!self.client.params.isInitiator) {
-    [self.logger logMessage:@"Callee; waiting for remote offer"];
-    return;
-  }
-  [self.logger logMessage:@"GAE onOpen - create offer."];
-  RTCPair* audio =
-      [[RTCPair alloc] initWithKey:@"OfferToReceiveAudio" value:@"true"];
-  RTCPair* video =
-      [[RTCPair alloc] initWithKey:@"OfferToReceiveVideo" value:@"true"];
-  NSArray* mandatory = @[ audio, video ];
-  RTCMediaConstraints* constraints =
-      [[RTCMediaConstraints alloc] initWithMandatoryConstraints:mandatory
-                                            optionalConstraints:nil];
-  [self.peerConnection createOfferWithDelegate:self constraints:constraints];
-  [self.logger logMessage:@"PC - createOffer."];
-}
-
-- (void)onMessage:(NSDictionary*)messageData {
-  NSString* type = messageData[@"type"];
-  NSAssert(type, @"Missing type: %@", messageData);
-  [self.logger logMessage:[NSString stringWithFormat:@"GAE onMessage type - %@",
-                                                      type]];
-  if ([type isEqualToString:@"candidate"]) {
-    RTCICECandidate* candidate =
-        [RTCICECandidate candidateFromJSONDictionary:messageData];
-    if (self.queuedRemoteCandidates) {
-      [self.queuedRemoteCandidates addObject:candidate];
-    } else {
-      [self.peerConnection addICECandidate:candidate];
-    }
-  } else if ([type isEqualToString:@"offer"] ||
-             [type isEqualToString:@"answer"]) {
-    RTCSessionDescription* sdp =
-        [RTCSessionDescription descriptionFromJSONDictionary:messageData];
-    [self.peerConnection setRemoteDescriptionWithDelegate:self
-                                       sessionDescription:sdp];
-    [self.logger logMessage:@"PC - setRemoteDescription."];
-  } else if ([type isEqualToString:@"bye"]) {
-    [self.delegate connectionManagerDidReceiveHangup:self];
-  } else {
-    NSAssert(NO, @"Invalid message: %@", messageData);
-  }
-}
-
-- (void)onClose {
-  [self.logger logMessage:@"GAE onClose."];
-  [self.delegate connectionManagerDidReceiveHangup:self];
-}
-
-- (void)onError:(int)code withDescription:(NSString*)description {
-  NSString* message = [NSString stringWithFormat:@"GAE onError: %d, %@",
-                                code, description];
-  [self.logger logMessage:message];
-  [self.delegate connectionManager:self
-               didErrorWithMessage:message];
-}
-
-#pragma mark - RTCPeerConnectionDelegate
-
-- (void)peerConnection:(RTCPeerConnection*)peerConnection
-    signalingStateChanged:(RTCSignalingState)stateChanged {
-  dispatch_async(dispatch_get_main_queue(), ^{
-    NSLog(@"PCO onSignalingStateChange: %d", stateChanged);
-  });
-}
-
-- (void)peerConnection:(RTCPeerConnection*)peerConnection
-           addedStream:(RTCMediaStream*)stream {
-  dispatch_async(dispatch_get_main_queue(), ^{
-    NSLog(@"PCO onAddStream.");
-    NSAssert([stream.audioTracks count] == 1 || [stream.videoTracks count] == 1,
-             @"Expected audio or video track");
-    NSAssert([stream.audioTracks count] <= 1,
-             @"Expected at most 1 audio stream");
-    NSAssert([stream.videoTracks count] <= 1,
-             @"Expected at most 1 video stream");
-    if ([stream.videoTracks count] != 0) {
-      [self.delegate connectionManager:self
-            didReceiveRemoteVideoTrack:stream.videoTracks[0]];
-    }
-  });
-}
-
-- (void)peerConnection:(RTCPeerConnection*)peerConnection
-         removedStream:(RTCMediaStream*)stream {
-  dispatch_async(dispatch_get_main_queue(),
-                 ^{ NSLog(@"PCO onRemoveStream."); });
-}
-
-- (void)peerConnectionOnRenegotiationNeeded:(RTCPeerConnection*)peerConnection {
-  dispatch_async(dispatch_get_main_queue(), ^{
-    NSLog(@"PCO onRenegotiationNeeded - ignoring because AppRTC has a "
-           "predefined negotiation strategy");
-  });
-}
-
-- (void)peerConnection:(RTCPeerConnection*)peerConnection
-       gotICECandidate:(RTCICECandidate*)candidate {
-  dispatch_async(dispatch_get_main_queue(), ^{
-    NSLog(@"PCO onICECandidate.\n%@", candidate);
-    [self.client sendData:[candidate JSONData]];
-  });
-}
-
-- (void)peerConnection:(RTCPeerConnection*)peerConnection
-    iceGatheringChanged:(RTCICEGatheringState)newState {
-  dispatch_async(dispatch_get_main_queue(),
-                 ^{ NSLog(@"PCO onIceGatheringChange. %d", newState); });
-}
-
-- (void)peerConnection:(RTCPeerConnection*)peerConnection
-    iceConnectionChanged:(RTCICEConnectionState)newState {
-  dispatch_async(dispatch_get_main_queue(), ^{
-    NSLog(@"PCO onIceConnectionChange. %d", newState);
-    if (newState == RTCICEConnectionConnected)
-      [self.logger logMessage:@"ICE Connection Connected."];
-    NSAssert(newState != RTCICEConnectionFailed, @"ICE Connection failed!");
-  });
-}
-
-- (void)peerConnection:(RTCPeerConnection*)peerConnection
-    didOpenDataChannel:(RTCDataChannel*)dataChannel {
-  NSAssert(NO, @"AppRTC doesn't use DataChannels");
-}
-
-#pragma mark - RTCSessionDescriptionDelegate
-
-- (void)peerConnection:(RTCPeerConnection*)peerConnection
-    didCreateSessionDescription:(RTCSessionDescription*)sdp
-                          error:(NSError*)error {
-  dispatch_async(dispatch_get_main_queue(), ^{
-    if (error) {
-      [self.logger logMessage:@"SDP onFailure."];
-      NSAssert(NO, error.description);
-      return;
-    }
-    [self.logger logMessage:@"SDP onSuccess(SDP) - set local description."];
-    [self.peerConnection setLocalDescriptionWithDelegate:self
-                                      sessionDescription:sdp];
-    [self.logger logMessage:@"PC setLocalDescription."];
-    [self.client sendData:[sdp JSONData]];
-  });
-}
-
-- (void)peerConnection:(RTCPeerConnection*)peerConnection
-    didSetSessionDescriptionWithError:(NSError*)error {
-  dispatch_async(dispatch_get_main_queue(), ^{
-    if (error) {
-      [self.logger logMessage:@"SDP onFailure."];
-      NSAssert(NO, error.description);
-      return;
-    }
-    [self.logger logMessage:@"SDP onSuccess() - possibly drain candidates"];
-    if (!self.client.params.isInitiator) {
-      if (self.peerConnection.remoteDescription &&
-          !self.peerConnection.localDescription) {
-        [self.logger logMessage:@"Callee, setRemoteDescription succeeded"];
-        RTCPair* audio = [[RTCPair alloc] initWithKey:@"OfferToReceiveAudio"
-                                                value:@"true"];
-        RTCPair* video = [[RTCPair alloc] initWithKey:@"OfferToReceiveVideo"
-                                                value:@"true"];
-        NSArray* mandatory = @[ audio, video ];
-        RTCMediaConstraints* constraints = [[RTCMediaConstraints alloc]
-            initWithMandatoryConstraints:mandatory
-                     optionalConstraints:nil];
-        [self.peerConnection createAnswerWithDelegate:self
-                                          constraints:constraints];
-        [self.logger logMessage:@"PC - createAnswer."];
-      } else {
-        [self.logger logMessage:@"SDP onSuccess - drain candidates"];
-        [self drainRemoteCandidates];
-      }
-    } else {
-      if (self.peerConnection.remoteDescription) {
-        [self.logger logMessage:@"SDP onSuccess - drain candidates"];
-        [self drainRemoteCandidates];
-      }
-    }
-  });
-}
-
-#pragma mark - RTCStatsDelegate methods
-
-- (void)peerConnection:(RTCPeerConnection*)peerConnection
-           didGetStats:(NSArray*)stats {
-  dispatch_async(dispatch_get_main_queue(), ^{
-    NSString* message = [NSString stringWithFormat:@"Stats:\n %@", stats];
-    [self.logger logMessage:message];
-  });
-}
-
-#pragma mark - Private
-
-- (void)drainRemoteCandidates {
-  for (RTCICECandidate* candidate in self.queuedRemoteCandidates) {
-    [self.peerConnection addICECandidate:candidate];
-  }
-  self.queuedRemoteCandidates = nil;
-}
-
-- (void)didFireStatsTimer:(NSTimer*)timer {
-  if (self.peerConnection) {
-    [self.peerConnection getStatsWithDelegate:self
-                             mediaStreamTrack:nil
-                             statsOutputLevel:RTCStatsOutputLevelDebug];
-  }
-}
-
-@end
diff --git a/talk/examples/objc/AppRTCDemo/ARDAppClient.h b/talk/examples/objc/AppRTCDemo/ARDAppClient.h
new file mode 100644
index 0000000..d742ea3
--- /dev/null
+++ b/talk/examples/objc/AppRTCDemo/ARDAppClient.h
@@ -0,0 +1,76 @@
+/*
+ * libjingle
+ * Copyright 2014, Google Inc.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *  1. Redistributions of source code must retain the above copyright notice,
+ *     this list of conditions and the following disclaimer.
+ *  2. Redistributions in binary form must reproduce the above copyright notice,
+ *     this list of conditions and the following disclaimer in the documentation
+ *     and/or other materials provided with the distribution.
+ *  3. The name of the author may not be used to endorse or promote products
+ *     derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+ * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+ * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+ * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "RTCVideoTrack.h"
+
+typedef NS_ENUM(NSInteger, ARDAppClientState) {
+  // Disconnected from servers.
+  kARDAppClientStateDisconnected,
+  // Connecting to servers.
+  kARDAppClientStateConnecting,
+  // Connected to servers.
+  kARDAppClientStateConnected,
+};
+
+@class ARDAppClient;
+@protocol ARDAppClientDelegate <NSObject>
+
+- (void)appClient:(ARDAppClient *)client
+    didChangeState:(ARDAppClientState)state;
+
+- (void)appClient:(ARDAppClient *)client
+    didReceiveLocalVideoTrack:(RTCVideoTrack *)localVideoTrack;
+
+- (void)appClient:(ARDAppClient *)client
+    didReceiveRemoteVideoTrack:(RTCVideoTrack *)remoteVideoTrack;
+
+- (void)appClient:(ARDAppClient *)client
+         didError:(NSError *)error;
+
+@end
+
+// Handles connections to the AppRTC server for a given room.
+@interface ARDAppClient : NSObject
+
+@property(nonatomic, readonly) ARDAppClientState state;
+@property(nonatomic, weak) id<ARDAppClientDelegate> delegate;
+
+- (instancetype)initWithDelegate:(id<ARDAppClientDelegate>)delegate;
+
+// Establishes a connection with the AppRTC servers for the given room id.
+// TODO(tkchin): provide available keys/values for options. This will be used
+// for call configurations such as overriding server choice, specifying codecs
+// and so on.
+- (void)connectToRoomWithId:(NSString *)roomId
+                    options:(NSDictionary *)options;
+
+// Disconnects from the AppRTC servers and any connected clients.
+- (void)disconnect;
+
+@end
diff --git a/talk/examples/objc/AppRTCDemo/ARDAppClient.m b/talk/examples/objc/AppRTCDemo/ARDAppClient.m
new file mode 100644
index 0000000..eba3ae9
--- /dev/null
+++ b/talk/examples/objc/AppRTCDemo/ARDAppClient.m
@@ -0,0 +1,675 @@
+/*
+ * libjingle
+ * Copyright 2014, Google Inc.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *  1. Redistributions of source code must retain the above copyright notice,
+ *     this list of conditions and the following disclaimer.
+ *  2. Redistributions in binary form must reproduce the above copyright notice,
+ *     this list of conditions and the following disclaimer in the documentation
+ *     and/or other materials provided with the distribution.
+ *  3. The name of the author may not be used to endorse or promote products
+ *     derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+ * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+ * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+ * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#import "ARDAppClient.h"
+
+#import <AVFoundation/AVFoundation.h>
+
+#import "ARDMessageResponse.h"
+#import "ARDRegisterResponse.h"
+#import "ARDSignalingMessage.h"
+#import "ARDUtilities.h"
+#import "ARDWebSocketChannel.h"
+#import "RTCICECandidate+JSON.h"
+#import "RTCICEServer+JSON.h"
+#import "RTCMediaConstraints.h"
+#import "RTCMediaStream.h"
+#import "RTCPair.h"
+#import "RTCPeerConnection.h"
+#import "RTCPeerConnectionDelegate.h"
+#import "RTCPeerConnectionFactory.h"
+#import "RTCSessionDescription+JSON.h"
+#import "RTCSessionDescriptionDelegate.h"
+#import "RTCVideoCapturer.h"
+#import "RTCVideoTrack.h"
+
+// TODO(tkchin): move these to a configuration object.
+static NSString *kARDRoomServerHostUrl =
+    @"https://3-dot-apprtc.appspot.com";
+static NSString *kARDRoomServerRegisterFormat =
+    @"https://3-dot-apprtc.appspot.com/register/%@";
+static NSString *kARDRoomServerMessageFormat =
+    @"https://3-dot-apprtc.appspot.com/message/%@/%@";
+static NSString *kARDRoomServerByeFormat =
+    @"https://3-dot-apprtc.appspot.com/bye/%@/%@";
+
+static NSString *kARDDefaultSTUNServerUrl =
+    @"stun:stun.l.google.com:19302";
+// TODO(tkchin): figure out a better username for CEOD statistics.
+static NSString *kARDTurnRequestUrl =
+    @"https://computeengineondemand.appspot.com"
+    @"/turn?username=iapprtc&key=4080218913";
+
+static NSString *kARDAppClientErrorDomain = @"ARDAppClient";
+static NSInteger kARDAppClientErrorUnknown = -1;
+static NSInteger kARDAppClientErrorRoomFull = -2;
+static NSInteger kARDAppClientErrorCreateSDP = -3;
+static NSInteger kARDAppClientErrorSetSDP = -4;
+static NSInteger kARDAppClientErrorNetwork = -5;
+static NSInteger kARDAppClientErrorInvalidClient = -6;
+static NSInteger kARDAppClientErrorInvalidRoom = -7;
+
+@interface ARDAppClient () <ARDWebSocketChannelDelegate,
+    RTCPeerConnectionDelegate, RTCSessionDescriptionDelegate>
+@property(nonatomic, strong) ARDWebSocketChannel *channel;
+@property(nonatomic, strong) RTCPeerConnection *peerConnection;
+@property(nonatomic, strong) RTCPeerConnectionFactory *factory;
+@property(nonatomic, strong) NSMutableArray *messageQueue;
+
+@property(nonatomic, assign) BOOL isTurnComplete;
+@property(nonatomic, assign) BOOL hasReceivedSdp;
+@property(nonatomic, readonly) BOOL isRegisteredWithRoomServer;
+
+@property(nonatomic, strong) NSString *roomId;
+@property(nonatomic, strong) NSString *clientId;
+@property(nonatomic, assign) BOOL isInitiator;
+@property(nonatomic, strong) NSMutableArray *iceServers;
+@property(nonatomic, strong) NSURL *webSocketURL;
+@property(nonatomic, strong) NSURL *webSocketRestURL;
+@end
+
+@implementation ARDAppClient
+
+@synthesize delegate = _delegate;
+@synthesize state = _state;
+@synthesize channel = _channel;
+@synthesize peerConnection = _peerConnection;
+@synthesize factory = _factory;
+@synthesize messageQueue = _messageQueue;
+@synthesize isTurnComplete = _isTurnComplete;
+@synthesize hasReceivedSdp  = _hasReceivedSdp;
+@synthesize roomId = _roomId;
+@synthesize clientId = _clientId;
+@synthesize isInitiator = _isInitiator;
+@synthesize iceServers = _iceServers;
+@synthesize webSocketURL = _websocketURL;
+@synthesize webSocketRestURL = _websocketRestURL;
+
+- (instancetype)initWithDelegate:(id<ARDAppClientDelegate>)delegate {
+  if (self = [super init]) {
+    _delegate = delegate;
+    _factory = [[RTCPeerConnectionFactory alloc] init];
+    _messageQueue = [NSMutableArray array];
+    _iceServers = [NSMutableArray arrayWithObject:[self defaultSTUNServer]];
+  }
+  return self;
+}
+
+- (void)dealloc {
+  [self disconnect];
+}
+
+- (void)setState:(ARDAppClientState)state {
+  if (_state == state) {
+    return;
+  }
+  _state = state;
+  [_delegate appClient:self didChangeState:_state];
+}
+
+- (void)connectToRoomWithId:(NSString *)roomId
+                    options:(NSDictionary *)options {
+  NSParameterAssert(roomId.length);
+  NSParameterAssert(_state == kARDAppClientStateDisconnected);
+  self.state = kARDAppClientStateConnecting;
+
+  // Request TURN.
+  __weak ARDAppClient *weakSelf = self;
+  NSURL *turnRequestURL = [NSURL URLWithString:kARDTurnRequestUrl];
+  [self requestTURNServersWithURL:turnRequestURL
+                completionHandler:^(NSArray *turnServers) {
+    ARDAppClient *strongSelf = weakSelf;
+    [strongSelf.iceServers addObjectsFromArray:turnServers];
+    strongSelf.isTurnComplete = YES;
+    [strongSelf startSignalingIfReady];
+  }];
+  
+  // Register with room server.
+  [self registerWithRoomServerForRoomId:roomId
+                      completionHandler:^(ARDRegisterResponse *response) {
+    ARDAppClient *strongSelf = weakSelf;
+    if (!response || response.result != kARDRegisterResultTypeSuccess) {
+      NSLog(@"Failed to register with room server. Result:%d",
+          (int)response.result);
+      [strongSelf disconnect];
+      NSDictionary *userInfo = @{
+        NSLocalizedDescriptionKey: @"Room is full.",
+      };
+      NSError *error =
+          [[NSError alloc] initWithDomain:kARDAppClientErrorDomain
+                                     code:kARDAppClientErrorRoomFull
+                                 userInfo:userInfo];
+      [strongSelf.delegate appClient:strongSelf didError:error];
+      return;
+    }
+    NSLog(@"Registered with room server.");
+    strongSelf.roomId = response.roomId;
+    strongSelf.clientId = response.clientId;
+    strongSelf.isInitiator = response.isInitiator;
+    for (ARDSignalingMessage *message in response.messages) {
+      if (message.type == kARDSignalingMessageTypeOffer ||
+          message.type == kARDSignalingMessageTypeAnswer) {
+        strongSelf.hasReceivedSdp = YES;
+        [strongSelf.messageQueue insertObject:message atIndex:0];
+      } else {
+        [strongSelf.messageQueue addObject:message];
+      }
+    }
+    strongSelf.webSocketURL = response.webSocketURL;
+    strongSelf.webSocketRestURL = response.webSocketRestURL;
+    [strongSelf registerWithColliderIfReady];
+    [strongSelf startSignalingIfReady];
+  }];
+}
+
+- (void)disconnect {
+  if (_state == kARDAppClientStateDisconnected) {
+    return;
+  }
+  if (self.isRegisteredWithRoomServer) {
+    [self unregisterWithRoomServer];
+  }
+  if (_channel) {
+    if (_channel.state == kARDWebSocketChannelStateRegistered) {
+      // Tell the other client we're hanging up.
+      ARDByeMessage *byeMessage = [[ARDByeMessage alloc] init];
+      NSData *byeData = [byeMessage JSONData];
+      [_channel sendData:byeData];
+    }
+    // Disconnect from collider.
+    _channel = nil;
+  }
+  _clientId = nil;
+  _roomId = nil;
+  _isInitiator = NO;
+  _hasReceivedSdp = NO;
+  _messageQueue = [NSMutableArray array];
+  _peerConnection = nil;
+  self.state = kARDAppClientStateDisconnected;
+}
+
+#pragma mark - ARDWebSocketChannelDelegate
+
+- (void)channel:(ARDWebSocketChannel *)channel
+    didReceiveMessage:(ARDSignalingMessage *)message {
+  switch (message.type) {
+    case kARDSignalingMessageTypeOffer:
+    case kARDSignalingMessageTypeAnswer:
+      _hasReceivedSdp = YES;
+      [_messageQueue insertObject:message atIndex:0];
+      break;
+    case kARDSignalingMessageTypeCandidate:
+      [_messageQueue addObject:message];
+      break;
+    case kARDSignalingMessageTypeBye:
+      [self processSignalingMessage:message];
+      return;
+  }
+  [self drainMessageQueueIfReady];
+}
+
+- (void)channel:(ARDWebSocketChannel *)channel
+    didChangeState:(ARDWebSocketChannelState)state {
+  switch (state) {
+    case kARDWebSocketChannelStateOpen:
+      break;
+    case kARDWebSocketChannelStateRegistered:
+      break;
+    case kARDWebSocketChannelStateClosed:
+    case kARDWebSocketChannelStateError:
+      // TODO(tkchin): reconnection scenarios. Right now we just disconnect
+      // completely if the websocket connection fails.
+      [self disconnect];
+      break;
+  }
+}
+
+#pragma mark - RTCPeerConnectionDelegate
+
+- (void)peerConnection:(RTCPeerConnection *)peerConnection
+    signalingStateChanged:(RTCSignalingState)stateChanged {
+  NSLog(@"Signaling state changed: %d", stateChanged);
+}
+
+- (void)peerConnection:(RTCPeerConnection *)peerConnection
+           addedStream:(RTCMediaStream *)stream {
+  dispatch_async(dispatch_get_main_queue(), ^{
+    NSLog(@"Received %lu video tracks and %lu audio tracks",
+        (unsigned long)stream.videoTracks.count,
+        (unsigned long)stream.audioTracks.count);
+    if (stream.videoTracks.count) {
+      RTCVideoTrack *videoTrack = stream.videoTracks[0];
+      [_delegate appClient:self didReceiveRemoteVideoTrack:videoTrack];
+    }
+  });
+}
+
+- (void)peerConnection:(RTCPeerConnection *)peerConnection
+        removedStream:(RTCMediaStream *)stream {
+  NSLog(@"Stream was removed.");
+}
+
+- (void)peerConnectionOnRenegotiationNeeded:
+    (RTCPeerConnection *)peerConnection {
+  NSLog(@"WARNING: Renegotiation needed but unimplemented.");
+}
+
+- (void)peerConnection:(RTCPeerConnection *)peerConnection
+    iceConnectionChanged:(RTCICEConnectionState)newState {
+  NSLog(@"ICE state changed: %d", newState);
+}
+
+- (void)peerConnection:(RTCPeerConnection *)peerConnection
+    iceGatheringChanged:(RTCICEGatheringState)newState {
+  NSLog(@"ICE gathering state changed: %d", newState);
+}
+
+- (void)peerConnection:(RTCPeerConnection *)peerConnection
+       gotICECandidate:(RTCICECandidate *)candidate {
+  dispatch_async(dispatch_get_main_queue(), ^{
+    ARDICECandidateMessage *message =
+        [[ARDICECandidateMessage alloc] initWithCandidate:candidate];
+    [self sendSignalingMessage:message];
+  });
+}
+
+- (void)peerConnection:(RTCPeerConnection*)peerConnection
+    didOpenDataChannel:(RTCDataChannel*)dataChannel {
+}
+
+#pragma mark - RTCSessionDescriptionDelegate
+
+- (void)peerConnection:(RTCPeerConnection *)peerConnection
+    didCreateSessionDescription:(RTCSessionDescription *)sdp
+                          error:(NSError *)error {
+  dispatch_async(dispatch_get_main_queue(), ^{
+    if (error) {
+      NSLog(@"Failed to create session description. Error: %@", error);
+      [self disconnect];
+      NSDictionary *userInfo = @{
+        NSLocalizedDescriptionKey: @"Failed to create session description.",
+      };
+      NSError *sdpError =
+          [[NSError alloc] initWithDomain:kARDAppClientErrorDomain
+                                     code:kARDAppClientErrorCreateSDP
+                                 userInfo:userInfo];
+      [_delegate appClient:self didError:sdpError];
+      return;
+    }
+    [_peerConnection setLocalDescriptionWithDelegate:self
+                                  sessionDescription:sdp];
+    ARDSessionDescriptionMessage *message =
+        [[ARDSessionDescriptionMessage alloc] initWithDescription:sdp];
+    [self sendSignalingMessage:message];
+  });
+}
+
+- (void)peerConnection:(RTCPeerConnection *)peerConnection
+    didSetSessionDescriptionWithError:(NSError *)error {
+  dispatch_async(dispatch_get_main_queue(), ^{
+    if (error) {
+      NSLog(@"Failed to set session description. Error: %@", error);
+      [self disconnect];
+      NSDictionary *userInfo = @{
+        NSLocalizedDescriptionKey: @"Failed to set session description.",
+      };
+      NSError *sdpError =
+          [[NSError alloc] initWithDomain:kARDAppClientErrorDomain
+                                     code:kARDAppClientErrorSetSDP
+                                 userInfo:userInfo];
+      [_delegate appClient:self didError:sdpError];
+      return;
+    }
+    // If we're answering and we've just set the remote offer we need to create
+    // an answer and set the local description.
+    if (!_isInitiator && !_peerConnection.localDescription) {
+      RTCMediaConstraints *constraints = [self defaultAnswerConstraints];
+      [_peerConnection createAnswerWithDelegate:self
+                                    constraints:constraints];
+
+    }
+  });
+}
+
+#pragma mark - Private
+
+- (BOOL)isRegisteredWithRoomServer {
+  return _clientId.length;
+}
+
+- (void)startSignalingIfReady {
+  if (!_isTurnComplete || !self.isRegisteredWithRoomServer) {
+    return;
+  }
+  self.state = kARDAppClientStateConnected;
+
+  // Create peer connection.
+  RTCMediaConstraints *constraints = [self defaultPeerConnectionConstraints];
+  _peerConnection = [_factory peerConnectionWithICEServers:_iceServers
+                                               constraints:constraints
+                                                  delegate:self];
+  RTCMediaStream *localStream = [self createLocalMediaStream];
+  [_peerConnection addStream:localStream];
+  if (_isInitiator) {
+    [self sendOffer];
+  } else {
+    [self waitForAnswer];
+  }
+}
+
+- (void)sendOffer {
+  [_peerConnection createOfferWithDelegate:self
+                               constraints:[self defaultOfferConstraints]];
+}
+
+- (void)waitForAnswer {
+  [self drainMessageQueueIfReady];
+}
+
+- (void)drainMessageQueueIfReady {
+  if (!_peerConnection || !_hasReceivedSdp) {
+    return;
+  }
+  for (ARDSignalingMessage *message in _messageQueue) {
+    [self processSignalingMessage:message];
+  }
+  [_messageQueue removeAllObjects];
+}
+
+- (void)processSignalingMessage:(ARDSignalingMessage *)message {
+  NSParameterAssert(_peerConnection ||
+      message.type == kARDSignalingMessageTypeBye);
+  switch (message.type) {
+    case kARDSignalingMessageTypeOffer:
+    case kARDSignalingMessageTypeAnswer: {
+      ARDSessionDescriptionMessage *sdpMessage =
+          (ARDSessionDescriptionMessage *)message;
+      RTCSessionDescription *description = sdpMessage.sessionDescription;
+      [_peerConnection setRemoteDescriptionWithDelegate:self
+                                     sessionDescription:description];
+      break;
+    }
+    case kARDSignalingMessageTypeCandidate: {
+      ARDICECandidateMessage *candidateMessage =
+          (ARDICECandidateMessage *)message;
+      [_peerConnection addICECandidate:candidateMessage.candidate];
+      break;
+    }
+    case kARDSignalingMessageTypeBye:
+      // Other client disconnected.
+      // TODO(tkchin): support waiting in room for next client. For now just
+      // disconnect.
+      [self disconnect];
+      break;
+  }
+}
+
+- (void)sendSignalingMessage:(ARDSignalingMessage *)message {
+  if (_isInitiator) {
+    [self sendSignalingMessageToRoomServer:message completionHandler:nil];
+  } else {
+    [self sendSignalingMessageToCollider:message];
+  }
+}
+
+- (RTCMediaStream *)createLocalMediaStream {
+  RTCMediaStream* localStream = [_factory mediaStreamWithLabel:@"ARDAMS"];
+  RTCVideoTrack* localVideoTrack = nil;
+
+  // The iOS simulator doesn't provide any sort of camera capture
+  // support or emulation (http://goo.gl/rHAnC1) so don't bother
+  // trying to open a local stream.
+  // TODO(tkchin): local video capture for OSX. See
+  // https://code.google.com/p/webrtc/issues/detail?id=3417.
+#if !TARGET_IPHONE_SIMULATOR && TARGET_OS_IPHONE
+  NSString *cameraID = nil;
+  for (AVCaptureDevice *captureDevice in
+       [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo]) {
+    if (captureDevice.position == AVCaptureDevicePositionFront) {
+      cameraID = [captureDevice localizedName];
+      break;
+    }
+  }
+  NSAssert(cameraID, @"Unable to get the front camera id");
+
+  RTCVideoCapturer *capturer =
+      [RTCVideoCapturer capturerWithDeviceName:cameraID];
+  RTCMediaConstraints *mediaConstraints = [self defaultMediaStreamConstraints];
+  RTCVideoSource *videoSource =
+      [_factory videoSourceWithCapturer:capturer
+                            constraints:mediaConstraints];
+  localVideoTrack =
+      [_factory videoTrackWithID:@"ARDAMSv0" source:videoSource];
+  if (localVideoTrack) {
+    [localStream addVideoTrack:localVideoTrack];
+  }
+  [_delegate appClient:self didReceiveLocalVideoTrack:localVideoTrack];
+#endif
+  [localStream addAudioTrack:[_factory audioTrackWithID:@"ARDAMSa0"]];
+  return localStream;
+}
+
+- (void)requestTURNServersWithURL:(NSURL *)requestURL
+    completionHandler:(void (^)(NSArray *turnServers))completionHandler {
+  NSParameterAssert([requestURL absoluteString].length);
+  NSMutableURLRequest *request =
+      [NSMutableURLRequest requestWithURL:requestURL];
+  // We need to set origin because TURN provider whitelists requests based on
+  // origin.
+  [request addValue:@"Mozilla/5.0" forHTTPHeaderField:@"user-agent"];
+  [request addValue:kARDRoomServerHostUrl forHTTPHeaderField:@"origin"];
+  [NSURLConnection sendAsyncRequest:request
+                  completionHandler:^(NSURLResponse *response,
+                                      NSData *data,
+                                      NSError *error) {
+    NSArray *turnServers = [NSArray array];
+    if (error) {
+      NSLog(@"Unable to get TURN server.");
+      completionHandler(turnServers);
+      return;
+    }
+    NSDictionary *dict = [NSDictionary dictionaryWithJSONData:data];
+    turnServers = [RTCICEServer serversFromCEODJSONDictionary:dict];
+    completionHandler(turnServers);
+  }];
+}
+
+#pragma mark - Room server methods
+
+- (void)registerWithRoomServerForRoomId:(NSString *)roomId
+    completionHandler:(void (^)(ARDRegisterResponse *))completionHandler {
+  NSString *urlString =
+      [NSString stringWithFormat:kARDRoomServerRegisterFormat, roomId];
+  NSURL *roomURL = [NSURL URLWithString:urlString];
+  NSLog(@"Registering with room server.");
+  __weak ARDAppClient *weakSelf = self;
+  [NSURLConnection sendAsyncPostToURL:roomURL
+                             withData:nil
+                    completionHandler:^(BOOL succeeded, NSData *data) {
+    ARDAppClient *strongSelf = weakSelf;
+    if (!succeeded) {
+      NSError *error = [self roomServerNetworkError];
+      [strongSelf.delegate appClient:strongSelf didError:error];
+      completionHandler(nil);
+      return;
+    }
+    ARDRegisterResponse *response =
+        [ARDRegisterResponse responseFromJSONData:data];
+    completionHandler(response);
+  }];
+}
+
+- (void)sendSignalingMessageToRoomServer:(ARDSignalingMessage *)message
+    completionHandler:(void (^)(ARDMessageResponse *))completionHandler {
+  NSData *data = [message JSONData];
+  NSString *urlString =
+      [NSString stringWithFormat:
+          kARDRoomServerMessageFormat, _roomId, _clientId];
+  NSURL *url = [NSURL URLWithString:urlString];
+  NSLog(@"C->RS POST: %@", message);
+  __weak ARDAppClient *weakSelf = self;
+  [NSURLConnection sendAsyncPostToURL:url
+                             withData:data
+                    completionHandler:^(BOOL succeeded, NSData *data) {
+    ARDAppClient *strongSelf = weakSelf;
+    if (!succeeded) {
+      NSError *error = [self roomServerNetworkError];
+      [strongSelf.delegate appClient:strongSelf didError:error];
+      return;
+    }
+    ARDMessageResponse *response =
+        [ARDMessageResponse responseFromJSONData:data];
+    NSError *error = nil;
+    switch (response.result) {
+      case kARDMessageResultTypeSuccess:
+        break;
+      case kARDMessageResultTypeUnknown:
+        error =
+            [[NSError alloc] initWithDomain:kARDAppClientErrorDomain
+                                       code:kARDAppClientErrorUnknown
+                                   userInfo:@{
+          NSLocalizedDescriptionKey: @"Unknown error.",
+        }];
+      case kARDMessageResultTypeInvalidClient:
+        error =
+            [[NSError alloc] initWithDomain:kARDAppClientErrorDomain
+                                       code:kARDAppClientErrorInvalidClient
+                                   userInfo:@{
+          NSLocalizedDescriptionKey: @"Invalid client.",
+        }];
+        break;
+      case kARDMessageResultTypeInvalidRoom:
+        error =
+            [[NSError alloc] initWithDomain:kARDAppClientErrorDomain
+                                       code:kARDAppClientErrorInvalidRoom
+                                   userInfo:@{
+          NSLocalizedDescriptionKey: @"Invalid room.",
+        }];
+        break;
+    };
+    if (error) {
+      [strongSelf.delegate appClient:strongSelf didError:error];
+    }
+    if (completionHandler) {
+      completionHandler(response);
+    }
+  }];
+}
+
+- (void)unregisterWithRoomServer {
+  NSString *urlString =
+      [NSString stringWithFormat:kARDRoomServerByeFormat, _roomId, _clientId];
+  NSURL *url = [NSURL URLWithString:urlString];
+  NSURLRequest *request = [NSURLRequest requestWithURL:url];
+  NSURLResponse *response = nil;
+  // We want a synchronous request so that we know that we're unregistered from
+  // room server before we do any further unregistration.
+  NSLog(@"C->RS: BYE");
+  NSError *error = nil;
+  [NSURLConnection sendSynchronousRequest:request
+                        returningResponse:&response
+                                    error:&error];
+  if (error) {
+    NSLog(@"Error unregistering from room server: %@", error);
+  }
+  NSLog(@"Unregistered from room server.");
+}
+
+- (NSError *)roomServerNetworkError {
+  NSError *error =
+      [[NSError alloc] initWithDomain:kARDAppClientErrorDomain
+                                 code:kARDAppClientErrorNetwork
+                             userInfo:@{
+    NSLocalizedDescriptionKey: @"Room server network error",
+  }];
+  return error;
+}
+
+#pragma mark - Collider methods
+
+- (void)registerWithColliderIfReady {
+  if (!self.isRegisteredWithRoomServer) {
+    return;
+  }
+  // Open WebSocket connection.
+  _channel =
+      [[ARDWebSocketChannel alloc] initWithURL:_websocketURL
+                                       restURL:_websocketRestURL
+                                      delegate:self];
+  [_channel registerForRoomId:_roomId clientId:_clientId];
+}
+
+- (void)sendSignalingMessageToCollider:(ARDSignalingMessage *)message {
+  NSData *data = [message JSONData];
+  [_channel sendData:data];
+}
+
+#pragma mark - Defaults
+
+- (RTCMediaConstraints *)defaultMediaStreamConstraints {
+  RTCMediaConstraints* constraints =
+      [[RTCMediaConstraints alloc]
+          initWithMandatoryConstraints:nil
+                   optionalConstraints:nil];
+  return constraints;
+}
+
+- (RTCMediaConstraints *)defaultAnswerConstraints {
+  return [self defaultOfferConstraints];
+}
+
+- (RTCMediaConstraints *)defaultOfferConstraints {
+  NSArray *mandatoryConstraints = @[
+      [[RTCPair alloc] initWithKey:@"OfferToReceiveAudio" value:@"true"],
+      [[RTCPair alloc] initWithKey:@"OfferToReceiveVideo" value:@"true"]
+  ];
+  RTCMediaConstraints* constraints =
+      [[RTCMediaConstraints alloc]
+          initWithMandatoryConstraints:mandatoryConstraints
+                   optionalConstraints:nil];
+  return constraints;
+}
+
+- (RTCMediaConstraints *)defaultPeerConnectionConstraints {
+  NSArray *optionalConstraints = @[
+      [[RTCPair alloc] initWithKey:@"DtlsSrtpKeyAgreement" value:@"true"]
+  ];
+  RTCMediaConstraints* constraints =
+      [[RTCMediaConstraints alloc]
+          initWithMandatoryConstraints:nil
+                   optionalConstraints:optionalConstraints];
+  return constraints;
+}
+
+- (RTCICEServer *)defaultSTUNServer {
+  NSURL *defaultSTUNServerURL = [NSURL URLWithString:kARDDefaultSTUNServerUrl];
+  return [[RTCICEServer alloc] initWithURI:defaultSTUNServerURL
+                                  username:@""
+                                  password:@""];
+}
+
+@end
diff --git a/talk/examples/objc/AppRTCDemo/ARDSignalingParams.h b/talk/examples/objc/AppRTCDemo/ARDMessageResponse.h
similarity index 69%
copy from talk/examples/objc/AppRTCDemo/ARDSignalingParams.h
copy to talk/examples/objc/AppRTCDemo/ARDMessageResponse.h
index df2114c..bbe35ab 100644
--- a/talk/examples/objc/AppRTCDemo/ARDSignalingParams.h
+++ b/talk/examples/objc/AppRTCDemo/ARDMessageResponse.h
@@ -27,20 +27,17 @@
 
 #import <Foundation/Foundation.h>
 
-#import "RTCMediaConstraints.h"
+typedef NS_ENUM(NSInteger, ARDMessageResultType) {
+  kARDMessageResultTypeUnknown,
+  kARDMessageResultTypeSuccess,
+  kARDMessageResultTypeInvalidRoom,
+  kARDMessageResultTypeInvalidClient
+};
 
-// Struct for holding the signaling parameters of an AppRTC room.
-@interface ARDSignalingParams : NSObject
+@interface ARDMessageResponse : NSObject
 
-@property(nonatomic, assign) BOOL isInitiator;
-@property(nonatomic, readonly) NSArray *errorMessages;
-@property(nonatomic, readonly) RTCMediaConstraints *offerConstraints;
-@property(nonatomic, readonly) RTCMediaConstraints *mediaConstraints;
-@property(nonatomic, readonly) NSMutableArray *iceServers;
-@property(nonatomic, readonly) NSURL *signalingServerURL;
-@property(nonatomic, readonly) NSURL *turnRequestURL;
-@property(nonatomic, readonly) NSString *channelToken;
+@property(nonatomic, readonly) ARDMessageResultType result;
 
-+ (ARDSignalingParams *)paramsFromJSONData:(NSData *)data;
++ (ARDMessageResponse *)responseFromJSONData:(NSData *)data;
 
 @end
diff --git a/talk/examples/objc/AppRTCDemo/ARDMessageResponse.m b/talk/examples/objc/AppRTCDemo/ARDMessageResponse.m
new file mode 100644
index 0000000..c6ab1d4
--- /dev/null
+++ b/talk/examples/objc/AppRTCDemo/ARDMessageResponse.m
@@ -0,0 +1,69 @@
+/*
+ * libjingle
+ * Copyright 2014, Google Inc.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *  1. Redistributions of source code must retain the above copyright notice,
+ *     this list of conditions and the following disclaimer.
+ *  2. Redistributions in binary form must reproduce the above copyright notice,
+ *     this list of conditions and the following disclaimer in the documentation
+ *     and/or other materials provided with the distribution.
+ *  3. The name of the author may not be used to endorse or promote products
+ *     derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+ * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+ * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+ * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#import "ARDMessageResponse.h"
+
+#import "ARDUtilities.h"
+
+static NSString const *kARDMessageResultKey = @"result";
+
+@interface ARDMessageResponse ()
+
+@property(nonatomic, assign) ARDMessageResultType result;
+
+@end
+
+@implementation ARDMessageResponse
+
+@synthesize result = _result;
+
++ (ARDMessageResponse *)responseFromJSONData:(NSData *)data {
+  NSDictionary *responseJSON = [NSDictionary dictionaryWithJSONData:data];
+  if (!responseJSON) {
+    return nil;
+  }
+  ARDMessageResponse *response = [[ARDMessageResponse alloc] init];
+  response.result =
+      [[self class] resultTypeFromString:responseJSON[kARDMessageResultKey]];
+  return response;
+}
+
+#pragma mark - Private
+
++ (ARDMessageResultType)resultTypeFromString:(NSString *)resultString {
+  ARDMessageResultType result = kARDMessageResultTypeUnknown;
+  if ([resultString isEqualToString:@"SUCCESS"]) {
+    result = kARDMessageResultTypeSuccess;
+  } else if ([resultString isEqualToString:@"INVALID_CLIENT"]) {
+    result = kARDMessageResultTypeInvalidClient;
+  } else if ([resultString isEqualToString:@"INVALID_ROOM"]) {
+    result = kARDMessageResultTypeInvalidRoom;
+  }
+  return result;
+}
+
+@end
diff --git a/talk/examples/objc/AppRTCDemo/ARDSignalingParams.h b/talk/examples/objc/AppRTCDemo/ARDRegisterResponse.h
similarity index 69%
rename from talk/examples/objc/AppRTCDemo/ARDSignalingParams.h
rename to talk/examples/objc/AppRTCDemo/ARDRegisterResponse.h
index df2114c..c26786c 100644
--- a/talk/examples/objc/AppRTCDemo/ARDSignalingParams.h
+++ b/talk/examples/objc/AppRTCDemo/ARDRegisterResponse.h
@@ -27,20 +27,23 @@
 
 #import <Foundation/Foundation.h>
 
-#import "RTCMediaConstraints.h"
+typedef NS_ENUM(NSInteger, ARDRegisterResultType) {
+  kARDRegisterResultTypeUnknown,
+  kARDRegisterResultTypeSuccess,
+  kARDRegisterResultTypeFull
+};
 
-// Struct for holding the signaling parameters of an AppRTC room.
-@interface ARDSignalingParams : NSObject
+// Result of registering with the GAE server.
+@interface ARDRegisterResponse : NSObject
 
-@property(nonatomic, assign) BOOL isInitiator;
-@property(nonatomic, readonly) NSArray *errorMessages;
-@property(nonatomic, readonly) RTCMediaConstraints *offerConstraints;
-@property(nonatomic, readonly) RTCMediaConstraints *mediaConstraints;
-@property(nonatomic, readonly) NSMutableArray *iceServers;
-@property(nonatomic, readonly) NSURL *signalingServerURL;
-@property(nonatomic, readonly) NSURL *turnRequestURL;
-@property(nonatomic, readonly) NSString *channelToken;
+@property(nonatomic, readonly) ARDRegisterResultType result;
+@property(nonatomic, readonly) BOOL isInitiator;
+@property(nonatomic, readonly) NSString *roomId;
+@property(nonatomic, readonly) NSString *clientId;
+@property(nonatomic, readonly) NSArray *messages;
+@property(nonatomic, readonly) NSURL *webSocketURL;
+@property(nonatomic, readonly) NSURL *webSocketRestURL;
 
-+ (ARDSignalingParams *)paramsFromJSONData:(NSData *)data;
++ (ARDRegisterResponse *)responseFromJSONData:(NSData *)data;
 
 @end
diff --git a/talk/examples/objc/AppRTCDemo/ARDRegisterResponse.m b/talk/examples/objc/AppRTCDemo/ARDRegisterResponse.m
new file mode 100644
index 0000000..76eb15c
--- /dev/null
+++ b/talk/examples/objc/AppRTCDemo/ARDRegisterResponse.m
@@ -0,0 +1,111 @@
+/*
+ * libjingle
+ * Copyright 2014, Google Inc.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *  1. Redistributions of source code must retain the above copyright notice,
+ *     this list of conditions and the following disclaimer.
+ *  2. Redistributions in binary form must reproduce the above copyright notice,
+ *     this list of conditions and the following disclaimer in the documentation
+ *     and/or other materials provided with the distribution.
+ *  3. The name of the author may not be used to endorse or promote products
+ *     derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+ * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+ * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+ * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#import "ARDRegisterResponse.h"
+
+#import "ARDSignalingMessage.h"
+#import "ARDUtilities.h"
+#import "RTCICEServer+JSON.h"
+
+static NSString const *kARDRegisterResultKey = @"result";
+static NSString const *kARDRegisterResultParamsKey = @"params";
+static NSString const *kARDRegisterInitiatorKey = @"is_initiator";
+static NSString const *kARDRegisterRoomIdKey = @"room_id";
+static NSString const *kARDRegisterClientIdKey = @"client_id";
+static NSString const *kARDRegisterMessagesKey = @"messages";
+static NSString const *kARDRegisterWebSocketURLKey = @"wss_url";
+static NSString const *kARDRegisterWebSocketRestURLKey = @"wss_post_url";
+
+@interface ARDRegisterResponse ()
+
+@property(nonatomic, assign) ARDRegisterResultType result;
+@property(nonatomic, assign) BOOL isInitiator;
+@property(nonatomic, strong) NSString *roomId;
+@property(nonatomic, strong) NSString *clientId;
+@property(nonatomic, strong) NSArray *messages;
+@property(nonatomic, strong) NSURL *webSocketURL;
+@property(nonatomic, strong) NSURL *webSocketRestURL;
+
+@end
+
+@implementation ARDRegisterResponse
+
+@synthesize result = _result;
+@synthesize isInitiator = _isInitiator;
+@synthesize roomId = _roomId;
+@synthesize clientId = _clientId;
+@synthesize messages = _messages;
+@synthesize webSocketURL = _webSocketURL;
+@synthesize webSocketRestURL = _webSocketRestURL;
+
++ (ARDRegisterResponse *)responseFromJSONData:(NSData *)data {
+  NSDictionary *responseJSON = [NSDictionary dictionaryWithJSONData:data];
+  if (!responseJSON) {
+    return nil;
+  }
+  ARDRegisterResponse *response = [[ARDRegisterResponse alloc] init];
+  NSString *resultString = responseJSON[kARDRegisterResultKey];
+  response.result = [[self class] resultTypeFromString:resultString];
+  NSDictionary *params = responseJSON[kARDRegisterResultParamsKey];
+
+  response.isInitiator = [params[kARDRegisterInitiatorKey] boolValue];
+  response.roomId = params[kARDRegisterRoomIdKey];
+  response.clientId = params[kARDRegisterClientIdKey];
+
+  // Parse messages.
+  NSArray *messages = params[kARDRegisterMessagesKey];
+  NSMutableArray *signalingMessages =
+      [NSMutableArray arrayWithCapacity:messages.count];
+  for (NSString *message in messages) {
+    ARDSignalingMessage *signalingMessage =
+        [ARDSignalingMessage messageFromJSONString:message];
+    [signalingMessages addObject:signalingMessage];
+  }
+  response.messages = signalingMessages;
+
+  // Parse websocket urls.
+  NSString *webSocketURLString = params[kARDRegisterWebSocketURLKey];
+  response.webSocketURL = [NSURL URLWithString:webSocketURLString];
+  NSString *webSocketRestURLString = params[kARDRegisterWebSocketRestURLKey];
+  response.webSocketRestURL = [NSURL URLWithString:webSocketRestURLString];
+
+  return response;
+}
+
+#pragma mark - Private
+
++ (ARDRegisterResultType)resultTypeFromString:(NSString *)resultString {
+  ARDRegisterResultType result = kARDRegisterResultTypeUnknown;
+  if ([resultString isEqualToString:@"SUCCESS"]) {
+    result = kARDRegisterResultTypeSuccess;
+  } else if ([resultString isEqualToString:@"FULL"]) {
+    result = kARDRegisterResultTypeFull;
+  }
+  return result;
+}
+
+@end
diff --git a/talk/examples/objc/AppRTCDemo/ARDSignalingParams.h b/talk/examples/objc/AppRTCDemo/ARDSignalingMessage.h
similarity index 62%
copy from talk/examples/objc/AppRTCDemo/ARDSignalingParams.h
copy to talk/examples/objc/AppRTCDemo/ARDSignalingMessage.h
index df2114c..b342694 100644
--- a/talk/examples/objc/AppRTCDemo/ARDSignalingParams.h
+++ b/talk/examples/objc/AppRTCDemo/ARDSignalingMessage.h
@@ -27,20 +27,40 @@
 
 #import <Foundation/Foundation.h>
 
-#import "RTCMediaConstraints.h"
+#import "RTCICECandidate.h"
+#import "RTCSessionDescription.h"
 
-// Struct for holding the signaling parameters of an AppRTC room.
-@interface ARDSignalingParams : NSObject
+typedef enum {
+  kARDSignalingMessageTypeCandidate,
+  kARDSignalingMessageTypeOffer,
+  kARDSignalingMessageTypeAnswer,
+  kARDSignalingMessageTypeBye,
+} ARDSignalingMessageType;
 
-@property(nonatomic, assign) BOOL isInitiator;
-@property(nonatomic, readonly) NSArray *errorMessages;
-@property(nonatomic, readonly) RTCMediaConstraints *offerConstraints;
-@property(nonatomic, readonly) RTCMediaConstraints *mediaConstraints;
-@property(nonatomic, readonly) NSMutableArray *iceServers;
-@property(nonatomic, readonly) NSURL *signalingServerURL;
-@property(nonatomic, readonly) NSURL *turnRequestURL;
-@property(nonatomic, readonly) NSString *channelToken;
+@interface ARDSignalingMessage : NSObject
 
-+ (ARDSignalingParams *)paramsFromJSONData:(NSData *)data;
+@property(nonatomic, readonly) ARDSignalingMessageType type;
 
++ (ARDSignalingMessage *)messageFromJSONString:(NSString *)jsonString;
+- (NSData *)JSONData;
+
+@end
+
+@interface ARDICECandidateMessage : ARDSignalingMessage
+
+@property(nonatomic, readonly) RTCICECandidate *candidate;
+
+- (instancetype)initWithCandidate:(RTCICECandidate *)candidate;
+
+@end
+
+@interface ARDSessionDescriptionMessage : ARDSignalingMessage
+
+@property(nonatomic, readonly) RTCSessionDescription *sessionDescription;
+
+- (instancetype)initWithDescription:(RTCSessionDescription *)description;
+
+@end
+
+@interface ARDByeMessage : ARDSignalingMessage
 @end
diff --git a/talk/examples/objc/AppRTCDemo/ARDSignalingMessage.m b/talk/examples/objc/AppRTCDemo/ARDSignalingMessage.m
new file mode 100644
index 0000000..38c4d5d
--- /dev/null
+++ b/talk/examples/objc/AppRTCDemo/ARDSignalingMessage.m
@@ -0,0 +1,143 @@
+/*
+ * libjingle
+ * Copyright 2014, Google Inc.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *  1. Redistributions of source code must retain the above copyright notice,
+ *     this list of conditions and the following disclaimer.
+ *  2. Redistributions in binary form must reproduce the above copyright notice,
+ *     this list of conditions and the following disclaimer in the documentation
+ *     and/or other materials provided with the distribution.
+ *  3. The name of the author may not be used to endorse or promote products
+ *     derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+ * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+ * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+ * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#import "ARDSignalingMessage.h"
+
+#import "ARDUtilities.h"
+#import "RTCICECandidate+JSON.h"
+#import "RTCSessionDescription+JSON.h"
+
+static NSString const *kARDSignalingMessageTypeKey = @"type";
+
+@implementation ARDSignalingMessage
+
+@synthesize type = _type;
+
+- (instancetype)initWithType:(ARDSignalingMessageType)type {
+  if (self = [super init]) {
+    _type = type;
+  }
+  return self;
+}
+
+- (NSString *)description {
+  return [[NSString alloc] initWithData:[self JSONData]
+                               encoding:NSUTF8StringEncoding];
+}
+
++ (ARDSignalingMessage *)messageFromJSONString:(NSString *)jsonString {
+  NSDictionary *values = [NSDictionary dictionaryWithJSONString:jsonString];
+  if (!values) {
+    NSLog(@"Error parsing signaling message JSON.");
+    return nil;
+  }
+
+  NSString *typeString = values[kARDSignalingMessageTypeKey];
+  ARDSignalingMessage *message = nil;
+  if ([typeString isEqualToString:@"candidate"]) {
+    RTCICECandidate *candidate =
+        [RTCICECandidate candidateFromJSONDictionary:values];
+    message = [[ARDICECandidateMessage alloc] initWithCandidate:candidate];
+  } else if ([typeString isEqualToString:@"offer"] ||
+             [typeString isEqualToString:@"answer"]) {
+    RTCSessionDescription *description =
+        [RTCSessionDescription descriptionFromJSONDictionary:values];
+    message =
+        [[ARDSessionDescriptionMessage alloc] initWithDescription:description];
+  } else if ([typeString isEqualToString:@"bye"]) {
+    message = [[ARDByeMessage alloc] init];
+  } else {
+    NSLog(@"Unexpected type: %@", typeString);
+  }
+  return message;
+}
+
+- (NSData *)JSONData {
+  return nil;
+}
+
+@end
+
+@implementation ARDICECandidateMessage
+
+@synthesize candidate = _candidate;
+
+- (instancetype)initWithCandidate:(RTCICECandidate *)candidate {
+  if (self = [super initWithType:kARDSignalingMessageTypeCandidate]) {
+    _candidate = candidate;
+  }
+  return self;
+}
+
+- (NSData *)JSONData {
+  return [_candidate JSONData];
+}
+
+@end
+
+@implementation ARDSessionDescriptionMessage
+
+@synthesize sessionDescription = _sessionDescription;
+
+- (instancetype)initWithDescription:(RTCSessionDescription *)description {
+  ARDSignalingMessageType type = kARDSignalingMessageTypeOffer;
+  NSString *typeString = description.type;
+  if ([typeString isEqualToString:@"offer"]) {
+    type = kARDSignalingMessageTypeOffer;
+  } else if ([typeString isEqualToString:@"answer"]) {
+    type = kARDSignalingMessageTypeAnswer;
+  } else {
+    NSAssert(NO, @"Unexpected type: %@", typeString);
+  }
+  if (self = [super initWithType:type]) {
+    _sessionDescription = description;
+  }
+  return self;
+}
+
+- (NSData *)JSONData {
+  return [_sessionDescription JSONData];
+}
+
+@end
+
+@implementation ARDByeMessage
+
+- (instancetype)init {
+  return [super initWithType:kARDSignalingMessageTypeBye];
+}
+
+- (NSData *)JSONData {
+  NSDictionary *message = @{
+    @"type": @"bye"
+  };
+  return [NSJSONSerialization dataWithJSONObject:message
+                                         options:NSJSONWritingPrettyPrinted
+                                           error:NULL];
+}
+
+@end
diff --git a/talk/examples/objc/AppRTCDemo/ARDSignalingParams.m b/talk/examples/objc/AppRTCDemo/ARDSignalingParams.m
deleted file mode 100644
index 58c8684..0000000
--- a/talk/examples/objc/AppRTCDemo/ARDSignalingParams.m
+++ /dev/null
@@ -1,130 +0,0 @@
-/*
- * libjingle
- * Copyright 2014, Google Inc.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- *  1. Redistributions of source code must retain the above copyright notice,
- *     this list of conditions and the following disclaimer.
- *  2. Redistributions in binary form must reproduce the above copyright notice,
- *     this list of conditions and the following disclaimer in the documentation
- *     and/or other materials provided with the distribution.
- *  3. The name of the author may not be used to endorse or promote products
- *     derived from this software without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
- * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
- * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
- * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
- * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
- * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
- * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
- * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-#import "ARDSignalingParams.h"
-
-#import "ARDUtilities.h"
-#import "RTCICEServer+JSON.h"
-#import "RTCMediaConstraints+JSON.h"
-
-static NSString const *kARDSignalingParamsErrorKey = @"error";
-static NSString const *kARDSignalingParamsErrorMessagesKey = @"error_messages";
-static NSString const *kARDSignalingParamsInitiatorKey = @"initiator";
-static NSString const *kARDSignalingParamsPeerConnectionConfigKey =
-    @"pc_config";
-static NSString const *kARDSignalingParamsICEServersKey = @"iceServers";
-static NSString const *kARDSignalingParamsMediaConstraintsKey =
-    @"media_constraints";
-static NSString const *kARDSignalingParamsMediaConstraintsVideoKey =
-    @"video";
-static NSString const *kARDSignalingParamsTokenKey = @"token";
-static NSString const *kARDSignalingParamsTurnRequestUrlKey = @"turn_url";
-
-@interface ARDSignalingParams ()
-
-@property(nonatomic, strong) NSArray *errorMessages;
-@property(nonatomic, strong) RTCMediaConstraints *offerConstraints;
-@property(nonatomic, strong) RTCMediaConstraints *mediaConstraints;
-@property(nonatomic, strong) NSMutableArray *iceServers;
-@property(nonatomic, strong) NSURL *signalingServerURL;
-@property(nonatomic, strong) NSURL *turnRequestURL;
-@property(nonatomic, strong) NSString *channelToken;
-
-@end
-
-@implementation ARDSignalingParams
-
-@synthesize errorMessages = _errorMessages;
-@synthesize isInitiator = _isInitiator;
-@synthesize offerConstraints = _offerConstraints;
-@synthesize mediaConstraints = _mediaConstraints;
-@synthesize iceServers = _iceServers;
-@synthesize signalingServerURL = _signalingServerURL;
-
-+ (ARDSignalingParams *)paramsFromJSONData:(NSData *)data {
-  NSDictionary *paramsJSON = [NSDictionary dictionaryWithJSONData:data];
-  if (!paramsJSON) {
-    return nil;
-  }
-  ARDSignalingParams *params = [[ARDSignalingParams alloc] init];
-
-  // Parse errors.
-  BOOL hasError = NO;
-  NSArray *errorMessages = paramsJSON[kARDSignalingParamsErrorMessagesKey];
-  if (errorMessages.count > 0) {
-    params.errorMessages = errorMessages;
-    return params;
-  }
-
-  // Parse ICE servers.
-  NSString *peerConnectionConfigString =
-      paramsJSON[kARDSignalingParamsPeerConnectionConfigKey];
-  NSDictionary *peerConnectionConfig =
-      [NSDictionary dictionaryWithJSONString:peerConnectionConfigString];
-  NSArray *iceServerJSONArray =
-      peerConnectionConfig[kARDSignalingParamsICEServersKey];
-  NSMutableArray *iceServers = [NSMutableArray array];
-  for (NSDictionary *iceServerJSON in iceServerJSONArray) {
-    RTCICEServer *iceServer =
-        [RTCICEServer serverFromJSONDictionary:iceServerJSON];
-    [iceServers addObject:iceServer];
-  }
-  params.iceServers = iceServers;
-
-  // Parse initiator.
-  BOOL isInitiator = [paramsJSON[kARDSignalingParamsInitiatorKey] boolValue];
-  params.isInitiator = isInitiator;
-
-  // Parse video constraints.
-  RTCMediaConstraints *videoConstraints = nil;
-  NSString *mediaConstraintsJSONString =
-      paramsJSON[kARDSignalingParamsMediaConstraintsKey];
-  NSDictionary *mediaConstraintsJSON =
-      [NSDictionary dictionaryWithJSONString:mediaConstraintsJSONString];
-  id videoJSON =
-      mediaConstraintsJSON[kARDSignalingParamsMediaConstraintsVideoKey];
-  if ([videoJSON isKindOfClass:[NSDictionary class]]) {
-    videoConstraints =
-        [RTCMediaConstraints constraintsFromJSONDictionary:videoJSON];
-  } else if ([videoJSON isKindOfClass:[NSNumber class]] &&
-             [videoJSON boolValue]) {
-    videoConstraints = [[RTCMediaConstraints alloc] init];
-  }
-  params.mediaConstraints = videoConstraints;
-
-  // Parse channel token.
-  NSString *token = paramsJSON[kARDSignalingParamsTokenKey];
-  params.channelToken = token;
-
-  // Parse turn request url.
-  params.turnRequestURL =
-      [NSURL URLWithString:paramsJSON[kARDSignalingParamsTurnRequestUrlKey]];
-
-  return params;
-}
-
-@end
diff --git a/talk/examples/objc/AppRTCDemo/ARDUtilities.h b/talk/examples/objc/AppRTCDemo/ARDUtilities.h
index ca4d1d7..f2219f3 100644
--- a/talk/examples/objc/AppRTCDemo/ARDUtilities.h
+++ b/talk/examples/objc/AppRTCDemo/ARDUtilities.h
@@ -38,9 +38,15 @@
 @interface NSURLConnection (ARDUtilities)
 
 // Issues an asynchronous request that calls back on main queue.
-+ (void)sendAsynchronousRequest:(NSURLRequest *)request
-              completionHandler:(void (^)(NSURLResponse *response,
-                                          NSData *data,
-                                          NSError *error))completionHandler;
++ (void)sendAsyncRequest:(NSURLRequest *)request
+       completionHandler:(void (^)(NSURLResponse *response,
+                                   NSData *data,
+                                   NSError *error))completionHandler;
+
+// Posts data to the specified URL.
++ (void)sendAsyncPostToURL:(NSURL *)url
+                  withData:(NSData *)data
+         completionHandler:(void (^)(BOOL succeeded,
+                                     NSData *data))completionHandler;
 
 @end
diff --git a/talk/examples/objc/AppRTCDemo/ARDUtilities.m b/talk/examples/objc/AppRTCDemo/ARDUtilities.m
index 937013e..54b1c51 100644
--- a/talk/examples/objc/AppRTCDemo/ARDUtilities.m
+++ b/talk/examples/objc/AppRTCDemo/ARDUtilities.m
@@ -55,17 +55,55 @@
 
 @implementation NSURLConnection (ARDUtilities)
 
-+ (void)sendAsynchronousRequest:(NSURLRequest *)request
-              completionHandler:(void (^)(NSURLResponse *response,
-                                          NSData *data,
-                                          NSError *error))completionHandler {
++ (void)sendAsyncRequest:(NSURLRequest *)request
+       completionHandler:(void (^)(NSURLResponse *response,
+                                   NSData *data,
+                                   NSError *error))completionHandler {
   // Kick off an async request which will call back on main thread.
   [NSURLConnection sendAsynchronousRequest:request
                                      queue:[NSOperationQueue mainQueue]
                          completionHandler:^(NSURLResponse *response,
                                              NSData *data,
                                              NSError *error) {
-    completionHandler(response, data, error);
+    if (completionHandler) {
+      completionHandler(response, data, error);
+    }
+  }];
+}
+
+// Posts data to the specified URL.
++ (void)sendAsyncPostToURL:(NSURL *)url
+                  withData:(NSData *)data
+         completionHandler:(void (^)(BOOL succeeded,
+                                     NSData *data))completionHandler {
+  NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
+  request.HTTPMethod = @"POST";
+  request.HTTPBody = data;
+  [[self class] sendAsyncRequest:request
+                completionHandler:^(NSURLResponse *response,
+                                    NSData *data,
+                                    NSError *error) {
+    if (error) {
+      NSLog(@"Error posting data: %@", error.localizedDescription);
+      if (completionHandler) {
+        completionHandler(NO, data);
+      }
+      return;
+    }
+    NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
+    if (httpResponse.statusCode != 200) {
+      NSString *serverResponse = data.length > 0 ?
+          [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] :
+          nil;
+      NSLog(@"Received bad response: %@", serverResponse);
+      if (completionHandler) {
+        completionHandler(NO, data);
+      }
+      return;
+    }
+    if (completionHandler) {
+      completionHandler(YES, data);
+    }
   }];
 }
 
diff --git a/talk/examples/objc/AppRTCDemo/ARDWebSocketChannel.h b/talk/examples/objc/AppRTCDemo/ARDWebSocketChannel.h
new file mode 100644
index 0000000..06c6520
--- /dev/null
+++ b/talk/examples/objc/AppRTCDemo/ARDWebSocketChannel.h
@@ -0,0 +1,75 @@
+/*
+ * libjingle
+ * Copyright 2014, Google Inc.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *  1. Redistributions of source code must retain the above copyright notice,
+ *     this list of conditions and the following disclaimer.
+ *  2. Redistributions in binary form must reproduce the above copyright notice,
+ *     this list of conditions and the following disclaimer in the documentation
+ *     and/or other materials provided with the distribution.
+ *  3. The name of the author may not be used to endorse or promote products
+ *     derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+ * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+ * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+ * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "ARDSignalingMessage.h"
+
+typedef NS_ENUM(NSInteger, ARDWebSocketChannelState) {
+  // State when disconnected.
+  kARDWebSocketChannelStateClosed,
+  // State when connection is established but not ready for use.
+  kARDWebSocketChannelStateOpen,
+  // State when connection is established and registered.
+  kARDWebSocketChannelStateRegistered,
+  // State when connection encounters a fatal error.
+  kARDWebSocketChannelStateError
+};
+
+@class ARDWebSocketChannel;
+@protocol ARDWebSocketChannelDelegate <NSObject>
+
+- (void)channel:(ARDWebSocketChannel *)channel
+    didChangeState:(ARDWebSocketChannelState)state;
+
+- (void)channel:(ARDWebSocketChannel *)channel
+    didReceiveMessage:(ARDSignalingMessage *)message;
+
+@end
+
+// Wraps a WebSocket connection to the AppRTC WebSocket server.
+@interface ARDWebSocketChannel : NSObject
+
+@property(nonatomic, readonly) NSString *roomId;
+@property(nonatomic, readonly) NSString *clientId;
+@property(nonatomic, readonly) ARDWebSocketChannelState state;
+@property(nonatomic, weak) id<ARDWebSocketChannelDelegate> delegate;
+
+- (instancetype)initWithURL:(NSURL *)url
+                    restURL:(NSURL *)restURL
+                   delegate:(id<ARDWebSocketChannelDelegate>)delegate;
+
+// Registers with the WebSocket server for the given room and client id once
+// the web socket connection is open.
+- (void)registerForRoomId:(NSString *)roomId
+                 clientId:(NSString *)clientId;
+
+// Sends data over the WebSocket connection if registered, otherwise POSTs to
+// the web socket server instead.
+- (void)sendData:(NSData *)data;
+
+@end
diff --git a/talk/examples/objc/AppRTCDemo/ARDWebSocketChannel.m b/talk/examples/objc/AppRTCDemo/ARDWebSocketChannel.m
new file mode 100644
index 0000000..1707201
--- /dev/null
+++ b/talk/examples/objc/AppRTCDemo/ARDWebSocketChannel.m
@@ -0,0 +1,213 @@
+/*
+ * libjingle
+ * Copyright 2014, Google Inc.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *  1. Redistributions of source code must retain the above copyright notice,
+ *     this list of conditions and the following disclaimer.
+ *  2. Redistributions in binary form must reproduce the above copyright notice,
+ *     this list of conditions and the following disclaimer in the documentation
+ *     and/or other materials provided with the distribution.
+ *  3. The name of the author may not be used to endorse or promote products
+ *     derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+ * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+ * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+ * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#import "ARDWebSocketChannel.h"
+
+#import "ARDUtilities.h"
+#import "SRWebSocket.h"
+
+// TODO(tkchin): move these to a configuration object.
+static NSString const *kARDWSSMessageErrorKey = @"error";
+static NSString const *kARDWSSMessagePayloadKey = @"msg";
+
+@interface ARDWebSocketChannel () <SRWebSocketDelegate>
+@end
+
+@implementation ARDWebSocketChannel {
+  NSURL *_url;
+  NSURL *_restURL;
+  SRWebSocket *_socket;
+}
+
+@synthesize delegate = _delegate;
+@synthesize state = _state;
+@synthesize roomId = _roomId;
+@synthesize clientId = _clientId;
+
+- (instancetype)initWithURL:(NSURL *)url
+                    restURL:(NSURL *)restURL
+                   delegate:(id<ARDWebSocketChannelDelegate>)delegate {
+  if (self = [super init]) {
+    _url = url;
+    _restURL = restURL;
+    _delegate = delegate;
+    _socket = [[SRWebSocket alloc] initWithURL:url];
+    _socket.delegate = self;
+    NSLog(@"Opening WebSocket.");
+    [_socket open];
+  }
+  return self;
+}
+
+- (void)dealloc {
+  [self disconnect];
+}
+
+- (void)setState:(ARDWebSocketChannelState)state {
+  if (_state == state) {
+    return;
+  }
+  _state = state;
+  [_delegate channel:self didChangeState:_state];
+}
+
+- (void)registerForRoomId:(NSString *)roomId
+                 clientId:(NSString *)clientId {
+  NSParameterAssert(roomId.length);
+  NSParameterAssert(clientId.length);
+  _roomId = roomId;
+  _clientId = clientId;
+  if (_state == kARDWebSocketChannelStateOpen) {
+    [self registerWithCollider];
+  }
+}
+
+- (void)sendData:(NSData *)data {
+  NSParameterAssert(_clientId.length);
+  NSParameterAssert(_roomId.length);
+  if (_state == kARDWebSocketChannelStateRegistered) {
+    NSString *payload =
+        [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
+    NSDictionary *message = @{
+      @"cmd": @"send",
+      @"msg": payload,
+    };
+    NSData *messageJSONObject =
+        [NSJSONSerialization dataWithJSONObject:message
+                                        options:NSJSONWritingPrettyPrinted
+                                          error:nil];
+    NSString *messageString =
+        [[NSString alloc] initWithData:messageJSONObject
+                              encoding:NSUTF8StringEncoding];
+    NSLog(@"C->WSS: %@", messageString);
+    [_socket send:messageString];
+  } else {
+    NSString *dataString =
+        [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
+    NSLog(@"C->WSS POST: %@", dataString);
+    NSString *urlString =
+        [NSString stringWithFormat:@"%@/%@/%@",
+            [_restURL absoluteString], _roomId, _clientId];
+    NSURL *url = [NSURL URLWithString:urlString];
+    [NSURLConnection sendAsyncPostToURL:url
+                               withData:data
+                      completionHandler:nil];
+  }
+}
+
+- (void)disconnect {
+  if (_state == kARDWebSocketChannelStateClosed ||
+      _state == kARDWebSocketChannelStateError) {
+    return;
+  }
+  [_socket close];
+  NSLog(@"C->WSS DELETE rid:%@ cid:%@", _roomId, _clientId);
+  NSString *urlString =
+      [NSString stringWithFormat:@"%@/%@/%@",
+          [_restURL absoluteString], _roomId, _clientId];
+  NSURL *url = [NSURL URLWithString:urlString];
+  NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
+  request.HTTPMethod = @"DELETE";
+  request.HTTPBody = nil;
+  [NSURLConnection sendAsyncRequest:request completionHandler:nil];
+}
+
+#pragma mark - SRWebSocketDelegate
+
+- (void)webSocketDidOpen:(SRWebSocket *)webSocket {
+  NSLog(@"WebSocket connection opened.");
+  self.state = kARDWebSocketChannelStateOpen;
+  if (_roomId.length && _clientId.length) {
+    [self registerWithCollider];
+  }
+}
+
+- (void)webSocket:(SRWebSocket *)webSocket didReceiveMessage:(id)message {
+  NSString *messageString = message;
+  NSData *messageData = [messageString dataUsingEncoding:NSUTF8StringEncoding];
+  id jsonObject = [NSJSONSerialization JSONObjectWithData:messageData
+                                                  options:0
+                                                    error:nil];
+  if (![jsonObject isKindOfClass:[NSDictionary class]]) {
+    NSLog(@"Unexpected message: %@", jsonObject);
+    return;
+  }
+  NSDictionary *wssMessage = jsonObject;
+  NSString *errorString = wssMessage[kARDWSSMessageErrorKey];
+  if (errorString.length) {
+    NSLog(@"WSS error: %@", errorString);
+    return;
+  }
+  NSString *payload = wssMessage[kARDWSSMessagePayloadKey];
+  ARDSignalingMessage *signalingMessage =
+      [ARDSignalingMessage messageFromJSONString:payload];
+  NSLog(@"WSS->C: %@", payload);
+  [_delegate channel:self didReceiveMessage:signalingMessage];
+}
+
+- (void)webSocket:(SRWebSocket *)webSocket didFailWithError:(NSError *)error {
+  NSLog(@"WebSocket error: %@", error);
+  self.state = kARDWebSocketChannelStateError;
+}
+
+- (void)webSocket:(SRWebSocket *)webSocket
+    didCloseWithCode:(NSInteger)code
+              reason:(NSString *)reason
+            wasClean:(BOOL)wasClean {
+  NSLog(@"WebSocket closed with code: %ld reason:%@ wasClean:%d",
+      (long)code, reason, wasClean);
+  NSParameterAssert(_state != kARDWebSocketChannelStateError);
+  self.state = kARDWebSocketChannelStateClosed;
+}
+
+#pragma mark - Private
+
+- (void)registerWithCollider {
+  if (_state == kARDWebSocketChannelStateRegistered) {
+    return;
+  }
+  NSParameterAssert(_roomId.length);
+  NSParameterAssert(_clientId.length);
+  NSDictionary *registerMessage = @{
+    @"cmd": @"register",
+    @"roomid" : _roomId,
+    @"clientid" : _clientId,
+  };
+  NSData *message =
+      [NSJSONSerialization dataWithJSONObject:registerMessage
+                                      options:NSJSONWritingPrettyPrinted
+                                        error:nil];
+  NSString *messageString =
+      [[NSString alloc] initWithData:message encoding:NSUTF8StringEncoding];
+  NSLog(@"Registering on WSS for rid:%@ cid:%@", _roomId, _clientId);
+  // Registration can fail if server rejects it. For example, if the room is
+  // full.
+  [_socket send:messageString];
+  self.state = kARDWebSocketChannelStateRegistered;
+}
+
+@end
diff --git a/talk/examples/objc/AppRTCDemo/GAEChannelClient.h b/talk/examples/objc/AppRTCDemo/GAEChannelClient.h
deleted file mode 100644
index dbaeceb..0000000
--- a/talk/examples/objc/AppRTCDemo/GAEChannelClient.h
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * libjingle
- * Copyright 2013, Google Inc.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- *  1. Redistributions of source code must retain the above copyright notice,
- *     this list of conditions and the following disclaimer.
- *  2. Redistributions in binary form must reproduce the above copyright notice,
- *     this list of conditions and the following disclaimer in the documentation
- *     and/or other materials provided with the distribution.
- *  3. The name of the author may not be used to endorse or promote products
- *     derived from this software without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
- * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
- * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
- * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
- * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
- * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
- * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
- * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-#import <Foundation/Foundation.h>
-
-// These methods will be called by the AppEngine chanel.  The documentation
-// for these methods is found here.  (Yes, it is a JS API.)
-// https://developers.google.com/appengine/docs/java/channel/javascript
-@protocol GAEMessageHandler<NSObject>
-
-- (void)onOpen;
-- (void)onMessage:(NSDictionary*)data;
-- (void)onClose;
-- (void)onError:(int)code withDescription:(NSString*)description;
-
-@end
-
-// Initialize with a token for an AppRTC data channel.  This will load
-// ios_channel.html and use the token to establish a data channel between the
-// application and AppEngine.
-@interface GAEChannelClient : NSObject
-
-@property(nonatomic, weak) id<GAEMessageHandler> delegate;
-
-- (instancetype)initWithToken:(NSString*)token
-                     delegate:(id<GAEMessageHandler>)delegate;
-
-@end
diff --git a/talk/examples/objc/AppRTCDemo/GAEChannelClient.m b/talk/examples/objc/AppRTCDemo/GAEChannelClient.m
deleted file mode 100644
index a95e99a..0000000
--- a/talk/examples/objc/AppRTCDemo/GAEChannelClient.m
+++ /dev/null
@@ -1,167 +0,0 @@
-/*
- * libjingle
- * Copyright 2013, Google Inc.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- *  1. Redistributions of source code must retain the above copyright notice,
- *     this list of conditions and the following disclaimer.
- *  2. Redistributions in binary form must reproduce the above copyright notice,
- *     this list of conditions and the following disclaimer in the documentation
- *     and/or other materials provided with the distribution.
- *  3. The name of the author may not be used to endorse or promote products
- *     derived from this software without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
- * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
- * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
- * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
- * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
- * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
- * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
- * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-#import "GAEChannelClient.h"
-
-#import "RTCPeerConnectionFactory.h"
-
-#if TARGET_OS_IPHONE
-
-#import <UIKit/UIKit.h>
-
-@interface GAEChannelClient () <UIWebViewDelegate>
-
-@property(nonatomic, strong) UIWebView* webView;
-
-#else
-
-#import <WebKit/WebKit.h>
-
-@interface GAEChannelClient ()
-
-@property(nonatomic, strong) WebView* webView;
-
-#endif
-
-@end
-
-@implementation GAEChannelClient
-
-- (instancetype)initWithToken:(NSString*)token
-                     delegate:(id<GAEMessageHandler>)delegate {
-  NSParameterAssert([token length] > 0);
-  NSParameterAssert(delegate);
-  self = [super init];
-  if (self) {
-#if TARGET_OS_IPHONE
-    _webView = [[UIWebView alloc] init];
-    _webView.delegate = self;
-#else
-    _webView = [[WebView alloc] init];
-    _webView.policyDelegate = self;
-#endif
-    _delegate = delegate;
-    NSString* htmlPath =
-        [[NSBundle mainBundle] pathForResource:@"channel" ofType:@"html"];
-    NSURL* htmlUrl = [NSURL fileURLWithPath:htmlPath];
-    NSString* path = [NSString
-        stringWithFormat:@"%@?token=%@", [htmlUrl absoluteString], token];
-#if TARGET_OS_IPHONE
-    [_webView
-#else
-    [[_webView mainFrame]
-#endif
-        loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:path]]];
-  }
-  return self;
-}
-
-- (void)dealloc {
-#if TARGET_OS_IPHONE
-  _webView.delegate = nil;
-  [_webView stopLoading];
-#else
-  _webView.policyDelegate = nil;
-  [[_webView mainFrame] stopLoading];
-#endif
-}
-
-#if TARGET_OS_IPHONE
-#pragma mark - UIWebViewDelegate
-
-- (BOOL)webView:(UIWebView*)webView
-    shouldStartLoadWithRequest:(NSURLRequest*)request
-                navigationType:(UIWebViewNavigationType)navigationType {
-#else
-// WebPolicyDelegate is an informal delegate.
-#pragma mark - WebPolicyDelegate
-
-- (void)webView:(WebView*)webView
-    decidePolicyForNavigationAction:(NSDictionary*)actionInformation
-                            request:(NSURLRequest*)request
-                              frame:(WebFrame*)frame
-                   decisionListener:(id<WebPolicyDecisionListener>)listener {
-#endif
-  NSString* scheme = [request.URL scheme];
-  NSAssert(scheme, @"scheme is nil: %@", request);
-  if (![scheme isEqualToString:@"js-frame"]) {
-#if TARGET_OS_IPHONE
-    return YES;
-#else
-    [listener use];
-    return;
-#endif
-  }
-  dispatch_async(dispatch_get_main_queue(), ^{
-      NSString* queuedMessage = [webView
-          stringByEvaluatingJavaScriptFromString:@"popQueuedMessage();"];
-      NSAssert([queuedMessage length], @"Empty queued message from JS");
-
-      NSDictionary* queuedMessageDict =
-          [GAEChannelClient jsonStringToDictionary:queuedMessage];
-      NSString* method = queuedMessageDict[@"type"];
-      NSAssert(method, @"Missing method: %@", queuedMessageDict);
-      NSDictionary* payload = queuedMessageDict[@"payload"];  // May be nil.
-
-      if ([method isEqualToString:@"onopen"]) {
-        [self.delegate onOpen];
-      } else if ([method isEqualToString:@"onmessage"]) {
-        NSDictionary* payloadData =
-            [GAEChannelClient jsonStringToDictionary:payload[@"data"]];
-        [self.delegate onMessage:payloadData];
-      } else if ([method isEqualToString:@"onclose"]) {
-        [self.delegate onClose];
-      } else if ([method isEqualToString:@"onerror"]) {
-        NSNumber* codeNumber = payload[@"code"];
-        int code = [codeNumber intValue];
-        NSAssert([codeNumber isEqualToNumber:[NSNumber numberWithInt:code]],
-                 @"Unexpected non-integral code: %@", payload);
-        [self.delegate onError:code withDescription:payload[@"description"]];
-      } else {
-        NSAssert(NO, @"Invalid message sent from UIWebView: %@", queuedMessage);
-      }
-  });
-#if TARGET_OS_IPHONE
-  return NO;
-#else
-  [listener ignore];
-  return;
-#endif
-}
-
-#pragma mark - Private
-
-+ (NSDictionary*)jsonStringToDictionary:(NSString*)str {
-  NSData* data = [str dataUsingEncoding:NSUTF8StringEncoding];
-  NSError* error;
-  NSDictionary* dict =
-      [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
-  NSAssert(!error, @"Invalid JSON? %@", str);
-  return dict;
-}
-
-@end
diff --git a/talk/examples/objc/AppRTCDemo/RTCICECandidate+JSON.m b/talk/examples/objc/AppRTCDemo/RTCICECandidate+JSON.m
index 62817a5..85cf95b 100644
--- a/talk/examples/objc/AppRTCDemo/RTCICECandidate+JSON.m
+++ b/talk/examples/objc/AppRTCDemo/RTCICECandidate+JSON.m
@@ -50,7 +50,16 @@
     kRTCICECandidateMidKey : self.sdpMid,
     kRTCICECandidateSdpKey : self.sdp
   };
-  return [NSJSONSerialization dataWithJSONObject:json options:0 error:nil];
+  NSError *error = nil;
+  NSData *data =
+      [NSJSONSerialization dataWithJSONObject:json
+                                      options:NSJSONWritingPrettyPrinted
+                                        error:&error];
+  if (error) {
+    NSLog(@"Error serializing JSON: %@", error);
+    return nil;
+  }
+  return data;
 }
 
 @end
diff --git a/talk/examples/objc/AppRTCDemo/RTCICEServer+JSON.h b/talk/examples/objc/AppRTCDemo/RTCICEServer+JSON.h
index d8a7207..2fb2fa0 100644
--- a/talk/examples/objc/AppRTCDemo/RTCICEServer+JSON.h
+++ b/talk/examples/objc/AppRTCDemo/RTCICEServer+JSON.h
@@ -31,6 +31,6 @@
 
 + (RTCICEServer *)serverFromJSONDictionary:(NSDictionary *)dictionary;
 // CEOD provides different JSON, and this parses that.
-+ (RTCICEServer *)serverFromCEODJSONDictionary:(NSDictionary *)dictionary;
++ (NSArray *)serversFromCEODJSONDictionary:(NSDictionary *)dictionary;
 
 @end
diff --git a/talk/examples/objc/AppRTCDemo/RTCICEServer+JSON.m b/talk/examples/objc/AppRTCDemo/RTCICEServer+JSON.m
index 29321f6..9465213 100644
--- a/talk/examples/objc/AppRTCDemo/RTCICEServer+JSON.m
+++ b/talk/examples/objc/AppRTCDemo/RTCICEServer+JSON.m
@@ -46,14 +46,19 @@
                                   password:credential];
 }
 
-+ (RTCICEServer *)serverFromCEODJSONDictionary:(NSDictionary *)dictionary {
++ (NSArray *)serversFromCEODJSONDictionary:(NSDictionary *)dictionary {
   NSString *username = dictionary[kRTCICEServerUsernameKey];
   NSString *password = dictionary[kRTCICEServerPasswordKey];
   NSArray *uris = dictionary[kRTCICEServerUrisKey];
-  NSParameterAssert(uris.count > 0);
-  return [[RTCICEServer alloc] initWithURI:[NSURL URLWithString:uris[0]]
-                                  username:username
-                                  password:password];
+  NSMutableArray *servers = [NSMutableArray arrayWithCapacity:uris.count];
+  for (NSString *uri in uris) {
+    RTCICEServer *server =
+        [[RTCICEServer alloc] initWithURI:[NSURL URLWithString:uri]
+                                 username:username
+                                 password:password];
+    [servers addObject:server];
+  }
+  return servers;
 }
 
 @end
diff --git a/talk/examples/objc/AppRTCDemo/channel.html b/talk/examples/objc/AppRTCDemo/channel.html
deleted file mode 100644
index 86846dd..0000000
--- a/talk/examples/objc/AppRTCDemo/channel.html
+++ /dev/null
@@ -1,94 +0,0 @@
-<html>
-  <head>
-    <script src="http://apprtc.appspot.com/_ah/channel/jsapi"></script>
-  </head>
-  <!--
-  Helper HTML that redirects Google AppEngine's Channel API to Objective C.
-  This is done by hosting this page in an iOS application.  The hosting
-  class creates a UIWebView control and implements the UIWebViewDelegate
-  protocol.  Then when there is a channel message it is queued in JS,
-  and an IFRAME is added to the DOM, triggering a navigation event
-  |shouldStartLoadWithRequest| in Objective C which can then fetch the
-  message using |popQueuedMessage|.  This queuing is necessary to avoid URL
-  length limits in UIWebView (which are undocumented).
-  -->
-  <body onbeforeunload="closeSocket()" onload="openSocket()">
-    <script type="text/javascript">
-      // QueryString is copy/pasta from
-      // chromium's chrome/test/data/media/html/utils.js.
-      var QueryString = function () {
-        // Allows access to query parameters on the URL; e.g., given a URL like:
-        //    http://<url>/my.html?test=123&bob=123
-        // parameters can now be accessed via QueryString.test or
-        // QueryString.bob.
-        var params = {};
-
-        // RegEx to split out values by &.
-        var r = /([^&=]+)=?([^&]*)/g;
-
-        // Lambda function for decoding extracted match values. Replaces '+'
-        // with space so decodeURIComponent functions properly.
-        function d(s) { return decodeURIComponent(s.replace(/\+/g, ' ')); }
-
-        var match;
-        while (match = r.exec(window.location.search.substring(1)))
-          params[d(match[1])] = d(match[2]);
-
-        return params;
-      } ();
-
-      var channel = null;
-      var socket = null;
-      // In-order queue of messages to be delivered to ObjectiveC.
-      // Each is a JSON.stringify()'d dictionary containing a 'type'
-      // field and optionally a 'payload'.
-      var messageQueue = [];
-
-      function openSocket() {
-        if (!QueryString.token || !QueryString.token.match(/^[A-z0-9_-]+$/)) {
-          // Send error back to ObjC.  This will assert in GAEChannelClient.m.
-          sendMessageToObjC("JSError:Missing/malformed token parameter " +
-                            QueryString.token);
-          throw "Missing/malformed token parameter: " + QueryString.token;
-        }
-        channel = new goog.appengine.Channel(QueryString.token);
-        socket = channel.open({
-          'onopen': function() {
-            sendMessageToObjC("onopen");
-          },
-          'onmessage': function(msg) {
-            sendMessageToObjC("onmessage", msg);
-          },
-          'onclose': function() {
-            sendMessageToObjC("onclose");
-          },
-          'onerror': function(err) {
-            sendMessageToObjC("onerror", err);
-          }
-        });
-      }
-
-      function closeSocket() {
-        socket.close();
-      }
-
-      // Add an IFRAME to the DOM to trigger a navigation event.  Then remove
-      // it as it is no longer needed.  Only one event is generated.
-      function sendMessageToObjC(type, payload) {
-        messageQueue.push(JSON.stringify({'type': type, 'payload': payload}));
-        var iframe = document.createElement("IFRAME");
-        iframe.setAttribute("src", "js-frame:");
-        // For some reason we need to set a non-empty size for the iOS6
-        // simulator...
-        iframe.setAttribute("height", "1px");
-        iframe.setAttribute("width", "1px");
-        document.documentElement.appendChild(iframe);
-        iframe.parentNode.removeChild(iframe);
-      }
-
-      function popQueuedMessage() {
-        return messageQueue.shift();
-      }
-    </script>
-  </body>
-</html>
diff --git a/talk/examples/objc/AppRTCDemo/ios/APPRTCViewController.m b/talk/examples/objc/AppRTCDemo/ios/APPRTCViewController.m
index d8d9714..3d60ee7 100644
--- a/talk/examples/objc/AppRTCDemo/ios/APPRTCViewController.m
+++ b/talk/examples/objc/AppRTCDemo/ios/APPRTCViewController.m
@@ -32,38 +32,28 @@
 #import "APPRTCViewController.h"
 
 #import <AVFoundation/AVFoundation.h>
-#import "APPRTCConnectionManager.h"
+#import "ARDAppClient.h"
 #import "RTCEAGLVideoView.h"
 #import "RTCVideoTrack.h"
 
 // Padding space for local video view with its parent.
 static CGFloat const kLocalViewPadding = 20;
 
-@interface APPRTCViewController ()
-<APPRTCConnectionManagerDelegate, APPRTCLogger, RTCEAGLVideoViewDelegate>
+@interface APPRTCViewController () <ARDAppClientDelegate,
+    RTCEAGLVideoViewDelegate>
 @property(nonatomic, assign) UIInterfaceOrientation statusBarOrientation;
 @property(nonatomic, strong) RTCEAGLVideoView* localVideoView;
 @property(nonatomic, strong) RTCEAGLVideoView* remoteVideoView;
 @end
 
 @implementation APPRTCViewController {
-  APPRTCConnectionManager* _connectionManager;
+  ARDAppClient *_client;
   RTCVideoTrack* _localVideoTrack;
   RTCVideoTrack* _remoteVideoTrack;
   CGSize _localVideoSize;
   CGSize _remoteVideoSize;
 }
 
-- (instancetype)initWithNibName:(NSString*)nibName
-                         bundle:(NSBundle*)bundle {
-  if (self = [super initWithNibName:nibName bundle:bundle]) {
-    _connectionManager =
-        [[APPRTCConnectionManager alloc] initWithDelegate:self
-                                                   logger:self];
-  }
-  return self;
-}
-
 - (void)viewDidLoad {
   [super viewDidLoad];
 
@@ -96,48 +86,46 @@
 }
 
 - (void)applicationWillResignActive:(UIApplication*)application {
-  [self logMessage:@"Application lost focus, connection broken."];
   [self disconnect];
 }
 
-#pragma mark - APPRTCConnectionManagerDelegate
+#pragma mark - ARDAppClientDelegate
 
-- (void)connectionManager:(APPRTCConnectionManager*)manager
-    didReceiveLocalVideoTrack:(RTCVideoTrack*)localVideoTrack {
+- (void)appClient:(ARDAppClient *)client
+    didChangeState:(ARDAppClientState)state {
+  switch (state) {
+    case kARDAppClientStateConnected:
+      NSLog(@"Client connected.");
+      break;
+    case kARDAppClientStateConnecting:
+      NSLog(@"Client connecting.");
+      break;
+    case kARDAppClientStateDisconnected:
+      NSLog(@"Client disconnected.");
+      [self resetUI];
+      break;
+  }
+}
+
+- (void)appClient:(ARDAppClient *)client
+    didReceiveLocalVideoTrack:(RTCVideoTrack *)localVideoTrack {
   _localVideoTrack = localVideoTrack;
   [_localVideoTrack addRenderer:self.localVideoView];
   self.localVideoView.hidden = NO;
 }
 
-- (void)connectionManager:(APPRTCConnectionManager*)manager
-    didReceiveRemoteVideoTrack:(RTCVideoTrack*)remoteVideoTrack {
+- (void)appClient:(ARDAppClient *)client
+    didReceiveRemoteVideoTrack:(RTCVideoTrack *)remoteVideoTrack {
   _remoteVideoTrack = remoteVideoTrack;
   [_remoteVideoTrack addRenderer:self.remoteVideoView];
 }
 
-- (void)connectionManagerDidReceiveHangup:(APPRTCConnectionManager*)manager {
-  [self showAlertWithMessage:@"Remote hung up."];
+- (void)appClient:(ARDAppClient *)client
+         didError:(NSError *)error {
+  [self showAlertWithMessage:[NSString stringWithFormat:@"%@", error]];
   [self disconnect];
 }
 
-- (void)connectionManager:(APPRTCConnectionManager*)manager
-      didErrorWithMessage:(NSString*)message {
-  [self showAlertWithMessage:message];
-  [self disconnect];
-}
-
-#pragma mark - APPRTCLogger
-
-- (void)logMessage:(NSString*)message {
-  dispatch_async(dispatch_get_main_queue(), ^{
-    NSString* output =
-        [NSString stringWithFormat:@"%@\n%@", self.logView.text, message];
-    self.logView.text = output;
-    [self.logView
-        scrollRangeToVisible:NSMakeRange([self.logView.text length], 0)];
-  });
-}
-
 #pragma mark - RTCEAGLVideoViewDelegate
 
 - (void)videoView:(RTCEAGLVideoView*)videoView
@@ -162,9 +150,10 @@
   textField.hidden = YES;
   self.instructionsView.hidden = YES;
   self.logView.hidden = NO;
-  NSString* url =
-      [NSString stringWithFormat:@"https://apprtc.appspot.com/?r=%@", room];
-  [_connectionManager connectToRoomWithURL:[NSURL URLWithString:url]];
+  [_client disconnect];
+  // TODO(tkchin): support reusing the same client object.
+  _client = [[ARDAppClient alloc] initWithDelegate:self];
+  [_client connectToRoomWithId:room options:nil];
   [self setupCaptureSession];
 }
 
@@ -179,7 +168,7 @@
 
 - (void)disconnect {
   [self resetUI];
-  [_connectionManager disconnect];
+  [_client disconnect];
 }
 
 - (void)showAlertWithMessage:(NSString*)message {
diff --git a/talk/examples/objc/AppRTCDemo/mac/APPRTCViewController.m b/talk/examples/objc/AppRTCDemo/mac/APPRTCViewController.m
index 08acac9..40d1307 100644
--- a/talk/examples/objc/AppRTCDemo/mac/APPRTCViewController.m
+++ b/talk/examples/objc/AppRTCDemo/mac/APPRTCViewController.m
@@ -28,7 +28,7 @@
 #import "APPRTCViewController.h"
 
 #import <AVFoundation/AVFoundation.h>
-#import "APPRTCConnectionManager.h"
+#import "ARDAppClient.h"
 #import "RTCNSGLVideoView.h"
 #import "RTCVideoTrack.h"
 
@@ -222,26 +222,16 @@
 @end
 
 @interface APPRTCViewController ()
-    <APPRTCConnectionManagerDelegate, APPRTCMainViewDelegate, APPRTCLogger>
+    <ARDAppClientDelegate, APPRTCMainViewDelegate>
 @property(nonatomic, readonly) APPRTCMainView* mainView;
 @end
 
 @implementation APPRTCViewController {
-  APPRTCConnectionManager* _connectionManager;
+  ARDAppClient* _client;
   RTCVideoTrack* _localVideoTrack;
   RTCVideoTrack* _remoteVideoTrack;
 }
 
-- (instancetype)initWithNibName:(NSString*)nibName
-                         bundle:(NSBundle*)bundle {
-  if (self = [super initWithNibName:nibName bundle:bundle]) {
-    _connectionManager =
-        [[APPRTCConnectionManager alloc] initWithDelegate:self
-                                                   logger:self];
-  }
-  return self;
-}
-
 - (void)dealloc {
   [self disconnect];
 }
@@ -257,43 +247,50 @@
   [self disconnect];
 }
 
-#pragma mark - APPRTCConnectionManagerDelegate
+#pragma mark - ARDAppClientDelegate
 
-- (void)connectionManager:(APPRTCConnectionManager*)manager
-    didReceiveLocalVideoTrack:(RTCVideoTrack*)localVideoTrack {
+- (void)appClient:(ARDAppClient *)client
+    didChangeState:(ARDAppClientState)state {
+  switch (state) {
+    case kARDAppClientStateConnected:
+      NSLog(@"Client connected.");
+      break;
+    case kARDAppClientStateConnecting:
+      NSLog(@"Client connecting.");
+      break;
+    case kARDAppClientStateDisconnected:
+      NSLog(@"Client disconnected.");
+      [self resetUI];
+      _client = nil;
+      break;
+  }
+}
+
+- (void)appClient:(ARDAppClient *)client
+    didReceiveLocalVideoTrack:(RTCVideoTrack *)localVideoTrack {
   _localVideoTrack = localVideoTrack;
 }
 
-- (void)connectionManager:(APPRTCConnectionManager*)manager
-    didReceiveRemoteVideoTrack:(RTCVideoTrack*)remoteVideoTrack {
+- (void)appClient:(ARDAppClient *)client
+    didReceiveRemoteVideoTrack:(RTCVideoTrack *)remoteVideoTrack {
   _remoteVideoTrack = remoteVideoTrack;
   [_remoteVideoTrack addRenderer:self.mainView.remoteVideoView];
 }
 
-- (void)connectionManagerDidReceiveHangup:(APPRTCConnectionManager*)manager {
-  [self showAlertWithMessage:@"Remote closed connection"];
+- (void)appClient:(ARDAppClient *)client
+         didError:(NSError *)error {
+  [self showAlertWithMessage:[NSString stringWithFormat:@"%@", error]];
   [self disconnect];
 }
 
-- (void)connectionManager:(APPRTCConnectionManager*)manager
-      didErrorWithMessage:(NSString*)message {
-  [self showAlertWithMessage:message];
-  [self disconnect];
-}
-
-#pragma mark - APPRTCLogger
-
-- (void)logMessage:(NSString*)message {
-  [self.mainView displayLogMessage:message];
-}
-
 #pragma mark - APPRTCMainViewDelegate
 
 - (void)appRTCMainView:(APPRTCMainView*)mainView
         didEnterRoomId:(NSString*)roomId {
-  NSString* urlString =
-      [NSString stringWithFormat:@"https://apprtc.appspot.com/?r=%@", roomId];
-  [_connectionManager connectToRoomWithURL:[NSURL URLWithString:urlString]];
+  [_client disconnect];
+  ARDAppClient *client = [[ARDAppClient alloc] initWithDelegate:self];
+  [client connectToRoomWithId:roomId options:nil];
+  _client = client;
 }
 
 #pragma mark - Private
@@ -308,11 +305,15 @@
   [alert runModal];
 }
 
-- (void)disconnect {
+- (void)resetUI {
   [_remoteVideoTrack removeRenderer:self.mainView.remoteVideoView];
   _remoteVideoTrack = nil;
   [self.mainView.remoteVideoView renderFrame:nil];
-  [_connectionManager disconnect];
+}
+
+- (void)disconnect {
+  [self resetUI];
+  [_client disconnect];
 }
 
 @end
diff --git a/talk/examples/objc/AppRTCDemo/third_party/SocketRocket/LICENSE b/talk/examples/objc/AppRTCDemo/third_party/SocketRocket/LICENSE
new file mode 100644
index 0000000..c01a79c
--- /dev/null
+++ b/talk/examples/objc/AppRTCDemo/third_party/SocketRocket/LICENSE
@@ -0,0 +1,15 @@
+
+   Copyright 2012 Square Inc.
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+
diff --git a/talk/examples/objc/AppRTCDemo/third_party/SocketRocket/SRWebSocket.h b/talk/examples/objc/AppRTCDemo/third_party/SocketRocket/SRWebSocket.h
new file mode 100644
index 0000000..5cce725
--- /dev/null
+++ b/talk/examples/objc/AppRTCDemo/third_party/SocketRocket/SRWebSocket.h
@@ -0,0 +1,132 @@
+//
+//   Copyright 2012 Square Inc.
+//
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//       http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+//
+
+#import <Foundation/Foundation.h>
+#import <Security/SecCertificate.h>
+
+typedef enum {
+    SR_CONNECTING   = 0,
+    SR_OPEN         = 1,
+    SR_CLOSING      = 2,
+    SR_CLOSED       = 3,
+} SRReadyState;
+
+typedef enum SRStatusCode : NSInteger {
+    SRStatusCodeNormal = 1000,
+    SRStatusCodeGoingAway = 1001,
+    SRStatusCodeProtocolError = 1002,
+    SRStatusCodeUnhandledType = 1003,
+    // 1004 reserved.
+    SRStatusNoStatusReceived = 1005,
+    // 1004-1006 reserved.
+    SRStatusCodeInvalidUTF8 = 1007,
+    SRStatusCodePolicyViolated = 1008,
+    SRStatusCodeMessageTooBig = 1009,
+} SRStatusCode;
+
+@class SRWebSocket;
+
+extern NSString *const SRWebSocketErrorDomain;
+extern NSString *const SRHTTPResponseErrorKey;
+
+#pragma mark - SRWebSocketDelegate
+
+@protocol SRWebSocketDelegate;
+
+#pragma mark - SRWebSocket
+
+@interface SRWebSocket : NSObject <NSStreamDelegate>
+
+@property (nonatomic, weak) id <SRWebSocketDelegate> delegate;
+
+@property (nonatomic, readonly) SRReadyState readyState;
+@property (nonatomic, readonly, retain) NSURL *url;
+
+// This returns the negotiated protocol.
+// It will be nil until after the handshake completes.
+@property (nonatomic, readonly, copy) NSString *protocol;
+
+// Protocols should be an array of strings that turn into Sec-WebSocket-Protocol.
+- (id)initWithURLRequest:(NSURLRequest *)request protocols:(NSArray *)protocols;
+- (id)initWithURLRequest:(NSURLRequest *)request;
+
+// Some helper constructors.
+- (id)initWithURL:(NSURL *)url protocols:(NSArray *)protocols;
+- (id)initWithURL:(NSURL *)url;
+
+// Delegate queue will be dispatch_main_queue by default.
+// You cannot set both OperationQueue and dispatch_queue.
+- (void)setDelegateOperationQueue:(NSOperationQueue*) queue;
+- (void)setDelegateDispatchQueue:(dispatch_queue_t) queue;
+
+// By default, it will schedule itself on +[NSRunLoop SR_networkRunLoop] using defaultModes.
+- (void)scheduleInRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode;
+- (void)unscheduleFromRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode;
+
+// SRWebSockets are intended for one-time-use only.  Open should be called once and only once.
+- (void)open;
+
+- (void)close;
+- (void)closeWithCode:(NSInteger)code reason:(NSString *)reason;
+
+// Send a UTF8 String or Data.
+- (void)send:(id)data;
+
+// Send Data (can be nil) in a ping message.
+- (void)sendPing:(NSData *)data;
+
+@end
+
+#pragma mark - SRWebSocketDelegate
+
+@protocol SRWebSocketDelegate <NSObject>
+
+// message will either be an NSString if the server is using text
+// or NSData if the server is using binary.
+- (void)webSocket:(SRWebSocket *)webSocket didReceiveMessage:(id)message;
+
+@optional
+
+- (void)webSocketDidOpen:(SRWebSocket *)webSocket;
+- (void)webSocket:(SRWebSocket *)webSocket didFailWithError:(NSError *)error;
+- (void)webSocket:(SRWebSocket *)webSocket didCloseWithCode:(NSInteger)code reason:(NSString *)reason wasClean:(BOOL)wasClean;
+- (void)webSocket:(SRWebSocket *)webSocket didReceivePong:(NSData *)pongPayload;
+
+@end
+
+#pragma mark - NSURLRequest (CertificateAdditions)
+
+@interface NSURLRequest (CertificateAdditions)
+
+@property (nonatomic, retain, readonly) NSArray *SR_SSLPinnedCertificates;
+
+@end
+
+#pragma mark - NSMutableURLRequest (CertificateAdditions)
+
+@interface NSMutableURLRequest (CertificateAdditions)
+
+@property (nonatomic, retain) NSArray *SR_SSLPinnedCertificates;
+
+@end
+
+#pragma mark - NSRunLoop (SRWebSocket)
+
+@interface NSRunLoop (SRWebSocket)
+
++ (NSRunLoop *)SR_networkRunLoop;
+
+@end
diff --git a/talk/examples/objc/AppRTCDemo/third_party/SocketRocket/SRWebSocket.m b/talk/examples/objc/AppRTCDemo/third_party/SocketRocket/SRWebSocket.m
new file mode 100644
index 0000000..b8add7f
--- /dev/null
+++ b/talk/examples/objc/AppRTCDemo/third_party/SocketRocket/SRWebSocket.m
@@ -0,0 +1,1761 @@
+//
+//   Copyright 2012 Square Inc.
+//
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//       http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+//
+
+
+#import "SRWebSocket.h"
+
+#if TARGET_OS_IPHONE
+#define HAS_ICU
+#endif
+
+#ifdef HAS_ICU
+#import <unicode/utf8.h>
+#endif
+
+#if TARGET_OS_IPHONE
+#import <Endian.h>
+#else
+#import <CoreServices/CoreServices.h>
+#endif
+
+#import <CommonCrypto/CommonDigest.h>
+#import <Security/SecRandom.h>
+
+#if OS_OBJECT_USE_OBJC_RETAIN_RELEASE
+#define sr_dispatch_retain(x)
+#define sr_dispatch_release(x)
+#define maybe_bridge(x) ((__bridge void *) x)
+#else
+#define sr_dispatch_retain(x) dispatch_retain(x)
+#define sr_dispatch_release(x) dispatch_release(x)
+#define maybe_bridge(x) (x)
+#endif
+
+#if !__has_feature(objc_arc) 
+#error SocketRocket must be compiled with ARC enabled
+#endif
+
+
+typedef enum  {
+    SROpCodeTextFrame = 0x1,
+    SROpCodeBinaryFrame = 0x2,
+    // 3-7 reserved.
+    SROpCodeConnectionClose = 0x8,
+    SROpCodePing = 0x9,
+    SROpCodePong = 0xA,
+    // B-F reserved.
+} SROpCode;
+
+typedef struct {
+    BOOL fin;
+//  BOOL rsv1;
+//  BOOL rsv2;
+//  BOOL rsv3;
+    uint8_t opcode;
+    BOOL masked;
+    uint64_t payload_length;
+} frame_header;
+
+static NSString *const SRWebSocketAppendToSecKeyString = @"258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
+
+static inline int32_t validate_dispatch_data_partial_string(NSData *data);
+static inline void SRFastLog(NSString *format, ...);
+
+@interface NSData (SRWebSocket)
+
+- (NSString *)stringBySHA1ThenBase64Encoding;
+
+@end
+
+
+@interface NSString (SRWebSocket)
+
+- (NSString *)stringBySHA1ThenBase64Encoding;
+
+@end
+
+
+@interface NSURL (SRWebSocket)
+
+// The origin isn't really applicable for a native application.
+// So instead, just map ws -> http and wss -> https.
+- (NSString *)SR_origin;
+
+@end
+
+
+@interface _SRRunLoopThread : NSThread
+
+@property (nonatomic, readonly) NSRunLoop *runLoop;
+
+@end
+
+
+static NSString *newSHA1String(const char *bytes, size_t length) {
+    uint8_t md[CC_SHA1_DIGEST_LENGTH];
+
+    assert(length >= 0);
+    assert(length <= UINT32_MAX);
+    CC_SHA1(bytes, (CC_LONG)length, md);
+    
+    NSData *data = [NSData dataWithBytes:md length:CC_SHA1_DIGEST_LENGTH];
+    
+    if ([data respondsToSelector:@selector(base64EncodedStringWithOptions:)]) {
+        return [data base64EncodedStringWithOptions:0];
+    }
+    
+    return [data base64Encoding];
+}
+
+@implementation NSData (SRWebSocket)
+
+- (NSString *)stringBySHA1ThenBase64Encoding;
+{
+    return newSHA1String(self.bytes, self.length);
+}
+
+@end
+
+
+@implementation NSString (SRWebSocket)
+
+- (NSString *)stringBySHA1ThenBase64Encoding;
+{
+    return newSHA1String(self.UTF8String, self.length);
+}
+
+@end
+
+NSString *const SRWebSocketErrorDomain = @"SRWebSocketErrorDomain";
+NSString *const SRHTTPResponseErrorKey = @"HTTPResponseStatusCode";
+
+// Returns number of bytes consumed. Returning 0 means you didn't match.
+// Sends bytes to callback handler;
+typedef size_t (^stream_scanner)(NSData *collected_data);
+
+typedef void (^data_callback)(SRWebSocket *webSocket,  NSData *data);
+
+@interface SRIOConsumer : NSObject {
+    stream_scanner _scanner;
+    data_callback _handler;
+    size_t _bytesNeeded;
+    BOOL _readToCurrentFrame;
+    BOOL _unmaskBytes;
+}
+@property (nonatomic, copy, readonly) stream_scanner consumer;
+@property (nonatomic, copy, readonly) data_callback handler;
+@property (nonatomic, assign) size_t bytesNeeded;
+@property (nonatomic, assign, readonly) BOOL readToCurrentFrame;
+@property (nonatomic, assign, readonly) BOOL unmaskBytes;
+
+@end
+
+// This class is not thread-safe, and is expected to always be run on the same queue.
+@interface SRIOConsumerPool : NSObject
+
+- (id)initWithBufferCapacity:(NSUInteger)poolSize;
+
+- (SRIOConsumer *)consumerWithScanner:(stream_scanner)scanner handler:(data_callback)handler bytesNeeded:(size_t)bytesNeeded readToCurrentFrame:(BOOL)readToCurrentFrame unmaskBytes:(BOOL)unmaskBytes;
+- (void)returnConsumer:(SRIOConsumer *)consumer;
+
+@end
+
+@interface SRWebSocket ()  <NSStreamDelegate>
+
+- (void)_writeData:(NSData *)data;
+- (void)_closeWithProtocolError:(NSString *)message;
+- (void)_failWithError:(NSError *)error;
+
+- (void)_disconnect;
+
+- (void)_readFrameNew;
+- (void)_readFrameContinue;
+
+- (void)_pumpScanner;
+
+- (void)_pumpWriting;
+
+- (void)_addConsumerWithScanner:(stream_scanner)consumer callback:(data_callback)callback;
+- (void)_addConsumerWithDataLength:(size_t)dataLength callback:(data_callback)callback readToCurrentFrame:(BOOL)readToCurrentFrame unmaskBytes:(BOOL)unmaskBytes;
+- (void)_addConsumerWithScanner:(stream_scanner)consumer callback:(data_callback)callback dataLength:(size_t)dataLength;
+- (void)_readUntilBytes:(const void *)bytes length:(size_t)length callback:(data_callback)dataHandler;
+- (void)_readUntilHeaderCompleteWithCallback:(data_callback)dataHandler;
+
+- (void)_sendFrameWithOpcode:(SROpCode)opcode data:(id)data;
+
+- (BOOL)_checkHandshake:(CFHTTPMessageRef)httpMessage;
+- (void)_SR_commonInit;
+
+- (void)_initializeStreams;
+- (void)_connect;
+
+@property (nonatomic) SRReadyState readyState;
+
+@property (nonatomic) NSOperationQueue *delegateOperationQueue;
+@property (nonatomic) dispatch_queue_t delegateDispatchQueue;
+
+@end
+
+
+@implementation SRWebSocket {
+    NSInteger _webSocketVersion;
+    
+    NSOperationQueue *_delegateOperationQueue;
+    dispatch_queue_t _delegateDispatchQueue;
+    
+    dispatch_queue_t _workQueue;
+    NSMutableArray *_consumers;
+
+    NSInputStream *_inputStream;
+    NSOutputStream *_outputStream;
+   
+    NSMutableData *_readBuffer;
+    NSUInteger _readBufferOffset;
+ 
+    NSMutableData *_outputBuffer;
+    NSUInteger _outputBufferOffset;
+
+    uint8_t _currentFrameOpcode;
+    size_t _currentFrameCount;
+    size_t _readOpCount;
+    uint32_t _currentStringScanPosition;
+    NSMutableData *_currentFrameData;
+    
+    NSString *_closeReason;
+    
+    NSString *_secKey;
+    
+    BOOL _pinnedCertFound;
+    
+    uint8_t _currentReadMaskKey[4];
+    size_t _currentReadMaskOffset;
+
+    BOOL _consumerStopped;
+    
+    BOOL _closeWhenFinishedWriting;
+    BOOL _failed;
+
+    BOOL _secure;
+    NSURLRequest *_urlRequest;
+
+    CFHTTPMessageRef _receivedHTTPHeaders;
+    
+    BOOL _sentClose;
+    BOOL _didFail;
+    int _closeCode;
+    
+    BOOL _isPumping;
+    
+    NSMutableSet *_scheduledRunloops;
+    
+    // We use this to retain ourselves.
+    __strong SRWebSocket *_selfRetain;
+    
+    NSArray *_requestedProtocols;
+    SRIOConsumerPool *_consumerPool;
+}
+
+@synthesize delegate = _delegate;
+@synthesize url = _url;
+@synthesize readyState = _readyState;
+@synthesize protocol = _protocol;
+
+static __strong NSData *CRLFCRLF;
+
++ (void)initialize;
+{
+    CRLFCRLF = [[NSData alloc] initWithBytes:"\r\n\r\n" length:4];
+}
+
+- (id)initWithURLRequest:(NSURLRequest *)request protocols:(NSArray *)protocols;
+{
+    self = [super init];
+    if (self) {
+        assert(request.URL);
+        _url = request.URL;
+        _urlRequest = request;
+        
+        _requestedProtocols = [protocols copy];
+        
+        [self _SR_commonInit];
+    }
+    
+    return self;
+}
+
+- (id)initWithURLRequest:(NSURLRequest *)request;
+{
+    return [self initWithURLRequest:request protocols:nil];
+}
+
+- (id)initWithURL:(NSURL *)url;
+{
+    return [self initWithURL:url protocols:nil];
+}
+
+- (id)initWithURL:(NSURL *)url protocols:(NSArray *)protocols;
+{
+    NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url];    
+    return [self initWithURLRequest:request protocols:protocols];
+}
+
+- (void)_SR_commonInit;
+{
+    
+    NSString *scheme = _url.scheme.lowercaseString;
+    assert([scheme isEqualToString:@"ws"] || [scheme isEqualToString:@"http"] || [scheme isEqualToString:@"wss"] || [scheme isEqualToString:@"https"]);
+    
+    if ([scheme isEqualToString:@"wss"] || [scheme isEqualToString:@"https"]) {
+        _secure = YES;
+    }
+    
+    _readyState = SR_CONNECTING;
+    _consumerStopped = YES;
+    _webSocketVersion = 13;
+    
+    _workQueue = dispatch_queue_create(NULL, DISPATCH_QUEUE_SERIAL);
+    
+    // Going to set a specific on the queue so we can validate we're on the work queue
+    dispatch_queue_set_specific(_workQueue, (__bridge void *)self, maybe_bridge(_workQueue), NULL);
+    
+    _delegateDispatchQueue = dispatch_get_main_queue();
+    sr_dispatch_retain(_delegateDispatchQueue);
+    
+    _readBuffer = [[NSMutableData alloc] init];
+    _outputBuffer = [[NSMutableData alloc] init];
+    
+    _currentFrameData = [[NSMutableData alloc] init];
+
+    _consumers = [[NSMutableArray alloc] init];
+    
+    _consumerPool = [[SRIOConsumerPool alloc] init];
+    
+    _scheduledRunloops = [[NSMutableSet alloc] init];
+    
+    [self _initializeStreams];
+    
+    // default handlers
+}
+
+- (void)assertOnWorkQueue;
+{
+    assert(dispatch_get_specific((__bridge void *)self) == maybe_bridge(_workQueue));
+}
+
+- (void)dealloc
+{
+    _inputStream.delegate = nil;
+    _outputStream.delegate = nil;
+
+    [_inputStream close];
+    [_outputStream close];
+    
+    sr_dispatch_release(_workQueue);
+    _workQueue = NULL;
+    
+    if (_receivedHTTPHeaders) {
+        CFRelease(_receivedHTTPHeaders);
+        _receivedHTTPHeaders = NULL;
+    }
+    
+    if (_delegateDispatchQueue) {
+        sr_dispatch_release(_delegateDispatchQueue);
+        _delegateDispatchQueue = NULL;
+    }
+}
+
+#ifndef NDEBUG
+
+- (void)setReadyState:(SRReadyState)aReadyState;
+{
+    [self willChangeValueForKey:@"readyState"];
+    assert(aReadyState > _readyState);
+    _readyState = aReadyState;
+    [self didChangeValueForKey:@"readyState"];
+}
+
+#endif
+
+- (void)open;
+{
+    assert(_url);
+    NSAssert(_readyState == SR_CONNECTING, @"Cannot call -(void)open on SRWebSocket more than once");
+
+    _selfRetain = self;
+    
+    [self _connect];
+}
+
+// Calls block on delegate queue
+- (void)_performDelegateBlock:(dispatch_block_t)block;
+{
+    if (_delegateOperationQueue) {
+        [_delegateOperationQueue addOperationWithBlock:block];
+    } else {
+        assert(_delegateDispatchQueue);
+        dispatch_async(_delegateDispatchQueue, block);
+    }
+}
+
+- (void)setDelegateDispatchQueue:(dispatch_queue_t)queue;
+{
+    if (queue) {
+        sr_dispatch_retain(queue);
+    }
+    
+    if (_delegateDispatchQueue) {
+        sr_dispatch_release(_delegateDispatchQueue);
+    }
+    
+    _delegateDispatchQueue = queue;
+}
+
+- (BOOL)_checkHandshake:(CFHTTPMessageRef)httpMessage;
+{
+    NSString *acceptHeader = CFBridgingRelease(CFHTTPMessageCopyHeaderFieldValue(httpMessage, CFSTR("Sec-WebSocket-Accept")));
+
+    if (acceptHeader == nil) {
+        return NO;
+    }
+    
+    NSString *concattedString = [_secKey stringByAppendingString:SRWebSocketAppendToSecKeyString];
+    NSString *expectedAccept = [concattedString stringBySHA1ThenBase64Encoding];
+    
+    return [acceptHeader isEqualToString:expectedAccept];
+}
+
+- (void)_HTTPHeadersDidFinish;
+{
+    NSInteger responseCode = CFHTTPMessageGetResponseStatusCode(_receivedHTTPHeaders);
+    
+    if (responseCode >= 400) {
+        SRFastLog(@"Request failed with response code %d", responseCode);
+        [self _failWithError:[NSError errorWithDomain:SRWebSocketErrorDomain code:2132 userInfo:@{NSLocalizedDescriptionKey:[NSString stringWithFormat:@"received bad response code from server %ld", (long)responseCode], SRHTTPResponseErrorKey:@(responseCode)}]];
+        return;
+    }
+    
+    if(![self _checkHandshake:_receivedHTTPHeaders]) {
+        [self _failWithError:[NSError errorWithDomain:SRWebSocketErrorDomain code:2133 userInfo:[NSDictionary dictionaryWithObject:[NSString stringWithFormat:@"Invalid Sec-WebSocket-Accept response"] forKey:NSLocalizedDescriptionKey]]];
+        return;
+    }
+    
+    NSString *negotiatedProtocol = CFBridgingRelease(CFHTTPMessageCopyHeaderFieldValue(_receivedHTTPHeaders, CFSTR("Sec-WebSocket-Protocol")));
+    if (negotiatedProtocol) {
+        // Make sure we requested the protocol
+        if ([_requestedProtocols indexOfObject:negotiatedProtocol] == NSNotFound) {
+            [self _failWithError:[NSError errorWithDomain:SRWebSocketErrorDomain code:2133 userInfo:[NSDictionary dictionaryWithObject:[NSString stringWithFormat:@"Server specified Sec-WebSocket-Protocol that wasn't requested"] forKey:NSLocalizedDescriptionKey]]];
+            return;
+        }
+        
+        _protocol = negotiatedProtocol;
+    }
+    
+    self.readyState = SR_OPEN;
+    
+    if (!_didFail) {
+        [self _readFrameNew];
+    }
+
+    [self _performDelegateBlock:^{
+        if ([self.delegate respondsToSelector:@selector(webSocketDidOpen:)]) {
+            [self.delegate webSocketDidOpen:self];
+        };
+    }];
+}
+
+
+- (void)_readHTTPHeader;
+{
+    if (_receivedHTTPHeaders == NULL) {
+        _receivedHTTPHeaders = CFHTTPMessageCreateEmpty(NULL, NO);
+    }
+                        
+    [self _readUntilHeaderCompleteWithCallback:^(SRWebSocket *self,  NSData *data) {
+        CFHTTPMessageAppendBytes(_receivedHTTPHeaders, (const UInt8 *)data.bytes, data.length);
+        
+        if (CFHTTPMessageIsHeaderComplete(_receivedHTTPHeaders)) {
+            SRFastLog(@"Finished reading headers %@", CFBridgingRelease(CFHTTPMessageCopyAllHeaderFields(_receivedHTTPHeaders)));
+            [self _HTTPHeadersDidFinish];
+        } else {
+            [self _readHTTPHeader];
+        }
+    }];
+}
+
+- (void)didConnect
+{
+    SRFastLog(@"Connected");
+    CFHTTPMessageRef request = CFHTTPMessageCreateRequest(NULL, CFSTR("GET"), (__bridge CFURLRef)_url, kCFHTTPVersion1_1);
+    
+    // Set host first so it defaults
+    CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Host"), (__bridge CFStringRef)(_url.port ? [NSString stringWithFormat:@"%@:%@", _url.host, _url.port] : _url.host));
+        
+    NSMutableData *keyBytes = [[NSMutableData alloc] initWithLength:16];
+    SecRandomCopyBytes(kSecRandomDefault, keyBytes.length, keyBytes.mutableBytes);
+    
+    if ([keyBytes respondsToSelector:@selector(base64EncodedStringWithOptions:)]) {
+        _secKey = [keyBytes base64EncodedStringWithOptions:0];
+    } else {
+        _secKey = [keyBytes base64Encoding];
+    }
+    
+    assert([_secKey length] == 24);
+    
+    CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Upgrade"), CFSTR("websocket"));
+    CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Connection"), CFSTR("Upgrade"));
+    CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Sec-WebSocket-Key"), (__bridge CFStringRef)_secKey);
+    CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Sec-WebSocket-Version"), (__bridge CFStringRef)[NSString stringWithFormat:@"%ld", (long)_webSocketVersion]);
+    
+    CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Origin"), (__bridge CFStringRef)_url.SR_origin);
+    
+    if (_requestedProtocols) {
+        CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Sec-WebSocket-Protocol"), (__bridge CFStringRef)[_requestedProtocols componentsJoinedByString:@", "]);
+    }
+
+    [_urlRequest.allHTTPHeaderFields enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
+        CFHTTPMessageSetHeaderFieldValue(request, (__bridge CFStringRef)key, (__bridge CFStringRef)obj);
+    }];
+    
+    NSData *message = CFBridgingRelease(CFHTTPMessageCopySerializedMessage(request));
+    
+    CFRelease(request);
+
+    [self _writeData:message];
+    [self _readHTTPHeader];
+}
+
+- (void)_initializeStreams;
+{
+    assert(_url.port.unsignedIntValue <= UINT32_MAX);
+    uint32_t port = _url.port.unsignedIntValue;
+    if (port == 0) {
+        if (!_secure) {
+            port = 80;
+        } else {
+            port = 443;
+        }
+    }
+    NSString *host = _url.host;
+    
+    CFReadStreamRef readStream = NULL;
+    CFWriteStreamRef writeStream = NULL;
+    
+    CFStreamCreatePairWithSocketToHost(NULL, (__bridge CFStringRef)host, port, &readStream, &writeStream);
+    
+    _outputStream = CFBridgingRelease(writeStream);
+    _inputStream = CFBridgingRelease(readStream);
+    
+    
+    if (_secure) {
+        NSMutableDictionary *SSLOptions = [[NSMutableDictionary alloc] init];
+        
+        [_outputStream setProperty:(__bridge id)kCFStreamSocketSecurityLevelNegotiatedSSL forKey:(__bridge id)kCFStreamPropertySocketSecurityLevel];
+        
+        // If we're using pinned certs, don't validate the certificate chain
+        if ([_urlRequest SR_SSLPinnedCertificates].count) {
+            [SSLOptions setValue:[NSNumber numberWithBool:NO] forKey:(__bridge id)kCFStreamSSLValidatesCertificateChain];
+        }
+        
+#if DEBUG
+        [SSLOptions setValue:[NSNumber numberWithBool:NO] forKey:(__bridge id)kCFStreamSSLValidatesCertificateChain];
+        NSLog(@"SocketRocket: In debug mode.  Allowing connection to any root cert");
+#endif
+        
+        [_outputStream setProperty:SSLOptions
+                            forKey:(__bridge id)kCFStreamPropertySSLSettings];
+    }
+    
+    _inputStream.delegate = self;
+    _outputStream.delegate = self;
+}
+
+- (void)_connect;
+{
+    if (!_scheduledRunloops.count) {
+        [self scheduleInRunLoop:[NSRunLoop SR_networkRunLoop] forMode:NSDefaultRunLoopMode];
+    }
+    
+    
+    [_outputStream open];
+    [_inputStream open];
+}
+
+- (void)scheduleInRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode;
+{
+    [_outputStream scheduleInRunLoop:aRunLoop forMode:mode];
+    [_inputStream scheduleInRunLoop:aRunLoop forMode:mode];
+    
+    [_scheduledRunloops addObject:@[aRunLoop, mode]];
+}
+
+- (void)unscheduleFromRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode;
+{
+    [_outputStream removeFromRunLoop:aRunLoop forMode:mode];
+    [_inputStream removeFromRunLoop:aRunLoop forMode:mode];
+    
+    [_scheduledRunloops removeObject:@[aRunLoop, mode]];
+}
+
+- (void)close;
+{
+    [self closeWithCode:SRStatusCodeNormal reason:nil];
+}
+
+- (void)closeWithCode:(NSInteger)code reason:(NSString *)reason;
+{
+    assert(code);
+    dispatch_async(_workQueue, ^{
+        if (self.readyState == SR_CLOSING || self.readyState == SR_CLOSED) {
+            return;
+        }
+        
+        BOOL wasConnecting = self.readyState == SR_CONNECTING;
+        
+        self.readyState = SR_CLOSING;
+        
+        SRFastLog(@"Closing with code %d reason %@", code, reason);
+        
+        if (wasConnecting) {
+            [self _disconnect];
+            return;
+        }
+
+        size_t maxMsgSize = [reason maximumLengthOfBytesUsingEncoding:NSUTF8StringEncoding];
+        NSMutableData *mutablePayload = [[NSMutableData alloc] initWithLength:sizeof(uint16_t) + maxMsgSize];
+        NSData *payload = mutablePayload;
+        
+        ((uint16_t *)mutablePayload.mutableBytes)[0] = EndianU16_BtoN(code);
+        
+        if (reason) {
+            NSRange remainingRange = {0};
+            
+            NSUInteger usedLength = 0;
+            
+            BOOL success = [reason getBytes:(char *)mutablePayload.mutableBytes + sizeof(uint16_t) maxLength:payload.length - sizeof(uint16_t) usedLength:&usedLength encoding:NSUTF8StringEncoding options:NSStringEncodingConversionExternalRepresentation range:NSMakeRange(0, reason.length) remainingRange:&remainingRange];
+            
+            assert(success);
+            assert(remainingRange.length == 0);
+
+            if (usedLength != maxMsgSize) {
+                payload = [payload subdataWithRange:NSMakeRange(0, usedLength + sizeof(uint16_t))];
+            }
+        }
+        
+        
+        [self _sendFrameWithOpcode:SROpCodeConnectionClose data:payload];
+    });
+}
+
+- (void)_closeWithProtocolError:(NSString *)message;
+{
+    // Need to shunt this on the _callbackQueue first to see if they received any messages 
+    [self _performDelegateBlock:^{
+        [self closeWithCode:SRStatusCodeProtocolError reason:message];
+        dispatch_async(_workQueue, ^{
+            [self _disconnect];
+        });
+    }];
+}
+
+- (void)_failWithError:(NSError *)error;
+{
+    dispatch_async(_workQueue, ^{
+        if (self.readyState != SR_CLOSED) {
+            _failed = YES;
+            [self _performDelegateBlock:^{
+                if ([self.delegate respondsToSelector:@selector(webSocket:didFailWithError:)]) {
+                    [self.delegate webSocket:self didFailWithError:error];
+                }
+            }];
+
+            self.readyState = SR_CLOSED;
+            _selfRetain = nil;
+
+            SRFastLog(@"Failing with error %@", error.localizedDescription);
+            
+            [self _disconnect];
+        }
+    });
+}
+
+- (void)_writeData:(NSData *)data;
+{    
+    [self assertOnWorkQueue];
+
+    if (_closeWhenFinishedWriting) {
+            return;
+    }
+    [_outputBuffer appendData:data];
+    [self _pumpWriting];
+}
+
+- (void)send:(id)data;
+{
+    NSAssert(self.readyState != SR_CONNECTING, @"Invalid State: Cannot call send: until connection is open");
+    // TODO: maybe not copy this for performance
+    data = [data copy];
+    dispatch_async(_workQueue, ^{
+        if ([data isKindOfClass:[NSString class]]) {
+            [self _sendFrameWithOpcode:SROpCodeTextFrame data:[(NSString *)data dataUsingEncoding:NSUTF8StringEncoding]];
+        } else if ([data isKindOfClass:[NSData class]]) {
+            [self _sendFrameWithOpcode:SROpCodeBinaryFrame data:data];
+        } else if (data == nil) {
+            [self _sendFrameWithOpcode:SROpCodeTextFrame data:data];
+        } else {
+            assert(NO);
+        }
+    });
+}
+
+- (void)sendPing:(NSData *)data;
+{
+    NSAssert(self.readyState == SR_OPEN, @"Invalid State: Cannot call send: until connection is open");
+    // TODO: maybe not copy this for performance
+    data = [data copy] ?: [NSData data]; // It's okay for a ping to be empty
+    dispatch_async(_workQueue, ^{
+        [self _sendFrameWithOpcode:SROpCodePing data:data];
+    });
+}
+
+- (void)handlePing:(NSData *)pingData;
+{
+    // Need to pingpong this off _callbackQueue first to make sure messages happen in order
+    [self _performDelegateBlock:^{
+        dispatch_async(_workQueue, ^{
+            [self _sendFrameWithOpcode:SROpCodePong data:pingData];
+        });
+    }];
+}
+
+- (void)handlePong:(NSData *)pongData;
+{
+    SRFastLog(@"Received pong");
+    [self _performDelegateBlock:^{
+        if ([self.delegate respondsToSelector:@selector(webSocket:didReceivePong:)]) {
+            [self.delegate webSocket:self didReceivePong:pongData];
+        }
+    }];
+}
+
+- (void)_handleMessage:(id)message
+{
+    SRFastLog(@"Received message");
+    [self _performDelegateBlock:^{
+        [self.delegate webSocket:self didReceiveMessage:message];
+    }];
+}
+
+
+static inline BOOL closeCodeIsValid(int closeCode) {
+    if (closeCode < 1000) {
+        return NO;
+    }
+    
+    if (closeCode >= 1000 && closeCode <= 1011) {
+        if (closeCode == 1004 ||
+            closeCode == 1005 ||
+            closeCode == 1006) {
+            return NO;
+        }
+        return YES;
+    }
+    
+    if (closeCode >= 3000 && closeCode <= 3999) {
+        return YES;
+    }
+    
+    if (closeCode >= 4000 && closeCode <= 4999) {
+        return YES;
+    }
+
+    return NO;
+}
+
+//  Note from RFC:
+//
+//  If there is a body, the first two
+//  bytes of the body MUST be a 2-byte unsigned integer (in network byte
+//  order) representing a status code with value /code/ defined in
+//  Section 7.4.  Following the 2-byte integer the body MAY contain UTF-8
+//  encoded data with value /reason/, the interpretation of which is not
+//  defined by this specification.
+
+- (void)handleCloseWithData:(NSData *)data;
+{
+    size_t dataSize = data.length;
+    __block uint16_t closeCode = 0;
+    
+    SRFastLog(@"Received close frame");
+    
+    if (dataSize == 1) {
+        // TODO handle error
+        [self _closeWithProtocolError:@"Payload for close must be larger than 2 bytes"];
+        return;
+    } else if (dataSize >= 2) {
+        [data getBytes:&closeCode length:sizeof(closeCode)];
+        _closeCode = EndianU16_BtoN(closeCode);
+        if (!closeCodeIsValid(_closeCode)) {
+            [self _closeWithProtocolError:[NSString stringWithFormat:@"Cannot have close code of %d", _closeCode]];
+            return;
+        }
+        if (dataSize > 2) {
+            _closeReason = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(2, dataSize - 2)] encoding:NSUTF8StringEncoding];
+            if (!_closeReason) {
+                [self _closeWithProtocolError:@"Close reason MUST be valid UTF-8"];
+                return;
+            }
+        }
+    } else {
+        _closeCode = SRStatusNoStatusReceived;
+    }
+    
+    [self assertOnWorkQueue];
+    
+    if (self.readyState == SR_OPEN) {
+        [self closeWithCode:1000 reason:nil];
+    }
+    dispatch_async(_workQueue, ^{
+        [self _disconnect];
+    });
+}
+
+- (void)_disconnect;
+{
+    [self assertOnWorkQueue];
+    SRFastLog(@"Trying to disconnect");
+    _closeWhenFinishedWriting = YES;
+    [self _pumpWriting];
+}
+
+- (void)_handleFrameWithData:(NSData *)frameData opCode:(NSInteger)opcode;
+{                
+    // Check that the current data is valid UTF8
+    
+    BOOL isControlFrame = (opcode == SROpCodePing || opcode == SROpCodePong || opcode == SROpCodeConnectionClose);
+    if (!isControlFrame) {
+        [self _readFrameNew];
+    } else {
+        dispatch_async(_workQueue, ^{
+            [self _readFrameContinue];
+        });
+    }
+    
+    switch (opcode) {
+        case SROpCodeTextFrame: {
+            NSString *str = [[NSString alloc] initWithData:frameData encoding:NSUTF8StringEncoding];
+            if (str == nil && frameData) {
+                [self closeWithCode:SRStatusCodeInvalidUTF8 reason:@"Text frames must be valid UTF-8"];
+                dispatch_async(_workQueue, ^{
+                    [self _disconnect];
+                });
+
+                return;
+            }
+            [self _handleMessage:str];
+            break;
+        }
+        case SROpCodeBinaryFrame:
+            [self _handleMessage:[frameData copy]];
+            break;
+        case SROpCodeConnectionClose:
+            [self handleCloseWithData:frameData];
+            break;
+        case SROpCodePing:
+            [self handlePing:frameData];
+            break;
+        case SROpCodePong:
+            [self handlePong:frameData];
+            break;
+        default:
+            [self _closeWithProtocolError:[NSString stringWithFormat:@"Unknown opcode %ld", (long)opcode]];
+            // TODO: Handle invalid opcode
+            break;
+    }
+}
+
+- (void)_handleFrameHeader:(frame_header)frame_header curData:(NSData *)curData;
+{
+    assert(frame_header.opcode != 0);
+    
+    if (self.readyState != SR_OPEN) {
+        return;
+    }
+    
+    
+    BOOL isControlFrame = (frame_header.opcode == SROpCodePing || frame_header.opcode == SROpCodePong || frame_header.opcode == SROpCodeConnectionClose);
+    
+    if (isControlFrame && !frame_header.fin) {
+        [self _closeWithProtocolError:@"Fragmented control frames not allowed"];
+        return;
+    }
+    
+    if (isControlFrame && frame_header.payload_length >= 126) {
+        [self _closeWithProtocolError:@"Control frames cannot have payloads larger than 126 bytes"];
+        return;
+    }
+    
+    if (!isControlFrame) {
+        _currentFrameOpcode = frame_header.opcode;
+        _currentFrameCount += 1;
+    }
+    
+    if (frame_header.payload_length == 0) {
+        if (isControlFrame) {
+            [self _handleFrameWithData:curData opCode:frame_header.opcode];
+        } else {
+            if (frame_header.fin) {
+                [self _handleFrameWithData:_currentFrameData opCode:frame_header.opcode];
+            } else {
+                // TODO add assert that opcode is not a control;
+                [self _readFrameContinue];
+            }
+        }
+    } else {
+        assert(frame_header.payload_length <= SIZE_T_MAX);
+        [self _addConsumerWithDataLength:(size_t)frame_header.payload_length callback:^(SRWebSocket *self, NSData *newData) {
+            if (isControlFrame) {
+                [self _handleFrameWithData:newData opCode:frame_header.opcode];
+            } else {
+                if (frame_header.fin) {
+                    [self _handleFrameWithData:self->_currentFrameData opCode:frame_header.opcode];
+                } else {
+                    // TODO add assert that opcode is not a control;
+                    [self _readFrameContinue];
+                }
+                
+            }
+        } readToCurrentFrame:!isControlFrame unmaskBytes:frame_header.masked];
+    }
+}
+
+/* From RFC:
+
+ 0                   1                   2                   3
+ 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+ +-+-+-+-+-------+-+-------------+-------------------------------+
+ |F|R|R|R| opcode|M| Payload len |    Extended payload length    |
+ |I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
+ |N|V|V|V|       |S|             |   (if payload len==126/127)   |
+ | |1|2|3|       |K|             |                               |
+ +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
+ |     Extended payload length continued, if payload len == 127  |
+ + - - - - - - - - - - - - - - - +-------------------------------+
+ |                               |Masking-key, if MASK set to 1  |
+ +-------------------------------+-------------------------------+
+ | Masking-key (continued)       |          Payload Data         |
+ +-------------------------------- - - - - - - - - - - - - - - - +
+ :                     Payload Data continued ...                :
+ + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
+ |                     Payload Data continued ...                |
+ +---------------------------------------------------------------+
+ */
+
+static const uint8_t SRFinMask          = 0x80;
+static const uint8_t SROpCodeMask       = 0x0F;
+static const uint8_t SRRsvMask          = 0x70;
+static const uint8_t SRMaskMask         = 0x80;
+static const uint8_t SRPayloadLenMask   = 0x7F;
+
+
+- (void)_readFrameContinue;
+{
+    assert((_currentFrameCount == 0 && _currentFrameOpcode == 0) || (_currentFrameCount > 0 && _currentFrameOpcode > 0));
+
+    [self _addConsumerWithDataLength:2 callback:^(SRWebSocket *self, NSData *data) {
+        __block frame_header header = {0};
+        
+        const uint8_t *headerBuffer = data.bytes;
+        assert(data.length >= 2);
+        
+        if (headerBuffer[0] & SRRsvMask) {
+            [self _closeWithProtocolError:@"Server used RSV bits"];
+            return;
+        }
+        
+        uint8_t receivedOpcode = (SROpCodeMask & headerBuffer[0]);
+        
+        BOOL isControlFrame = (receivedOpcode == SROpCodePing || receivedOpcode == SROpCodePong || receivedOpcode == SROpCodeConnectionClose);
+        
+        if (!isControlFrame && receivedOpcode != 0 && self->_currentFrameCount > 0) {
+            [self _closeWithProtocolError:@"all data frames after the initial data frame must have opcode 0"];
+            return;
+        }
+        
+        if (receivedOpcode == 0 && self->_currentFrameCount == 0) {
+            [self _closeWithProtocolError:@"cannot continue a message"];
+            return;
+        }
+        
+        header.opcode = receivedOpcode == 0 ? self->_currentFrameOpcode : receivedOpcode;
+        
+        header.fin = !!(SRFinMask & headerBuffer[0]);
+        
+        
+        header.masked = !!(SRMaskMask & headerBuffer[1]);
+        header.payload_length = SRPayloadLenMask & headerBuffer[1];
+        
+        headerBuffer = NULL;
+        
+        if (header.masked) {
+            [self _closeWithProtocolError:@"Client must receive unmasked data"];
+        }
+        
+        size_t extra_bytes_needed = header.masked ? sizeof(_currentReadMaskKey) : 0;
+        
+        if (header.payload_length == 126) {
+            extra_bytes_needed += sizeof(uint16_t);
+        } else if (header.payload_length == 127) {
+            extra_bytes_needed += sizeof(uint64_t);
+        }
+        
+        if (extra_bytes_needed == 0) {
+            [self _handleFrameHeader:header curData:self->_currentFrameData];
+        } else {
+            [self _addConsumerWithDataLength:extra_bytes_needed callback:^(SRWebSocket *self, NSData *data) {
+                size_t mapped_size = data.length;
+                const void *mapped_buffer = data.bytes;
+                size_t offset = 0;
+                
+                if (header.payload_length == 126) {
+                    assert(mapped_size >= sizeof(uint16_t));
+                    uint16_t newLen = EndianU16_BtoN(*(uint16_t *)(mapped_buffer));
+                    header.payload_length = newLen;
+                    offset += sizeof(uint16_t);
+                } else if (header.payload_length == 127) {
+                    assert(mapped_size >= sizeof(uint64_t));
+                    header.payload_length = EndianU64_BtoN(*(uint64_t *)(mapped_buffer));
+                    offset += sizeof(uint64_t);
+                } else {
+                    assert(header.payload_length < 126 && header.payload_length >= 0);
+                }
+                
+                
+                if (header.masked) {
+                    assert(mapped_size >= sizeof(_currentReadMaskOffset) + offset);
+                    memcpy(self->_currentReadMaskKey, ((uint8_t *)mapped_buffer) + offset, sizeof(self->_currentReadMaskKey));
+                }
+                
+                [self _handleFrameHeader:header curData:self->_currentFrameData];
+            } readToCurrentFrame:NO unmaskBytes:NO];
+        }
+    } readToCurrentFrame:NO unmaskBytes:NO];
+}
+
+- (void)_readFrameNew;
+{
+    dispatch_async(_workQueue, ^{
+        [_currentFrameData setLength:0];
+        
+        _currentFrameOpcode = 0;
+        _currentFrameCount = 0;
+        _readOpCount = 0;
+        _currentStringScanPosition = 0;
+        
+        [self _readFrameContinue];
+    });
+}
+
+- (void)_pumpWriting;
+{
+    [self assertOnWorkQueue];
+    
+    NSUInteger dataLength = _outputBuffer.length;
+    if (dataLength - _outputBufferOffset > 0 && _outputStream.hasSpaceAvailable) {
+        NSInteger bytesWritten = [_outputStream write:_outputBuffer.bytes + _outputBufferOffset maxLength:dataLength - _outputBufferOffset];
+        if (bytesWritten == -1) {
+            [self _failWithError:[NSError errorWithDomain:SRWebSocketErrorDomain code:2145 userInfo:[NSDictionary dictionaryWithObject:@"Error writing to stream" forKey:NSLocalizedDescriptionKey]]];
+             return;
+        }
+        
+        _outputBufferOffset += bytesWritten;
+        
+        if (_outputBufferOffset > 4096 && _outputBufferOffset > (_outputBuffer.length >> 1)) {
+            _outputBuffer = [[NSMutableData alloc] initWithBytes:(char *)_outputBuffer.bytes + _outputBufferOffset length:_outputBuffer.length - _outputBufferOffset];
+            _outputBufferOffset = 0;
+        }
+    }
+    
+    if (_closeWhenFinishedWriting && 
+        _outputBuffer.length - _outputBufferOffset == 0 && 
+        (_inputStream.streamStatus != NSStreamStatusNotOpen &&
+         _inputStream.streamStatus != NSStreamStatusClosed) &&
+        !_sentClose) {
+        _sentClose = YES;
+            
+        [_outputStream close];
+        [_inputStream close];
+        
+        
+        for (NSArray *runLoop in [_scheduledRunloops copy]) {
+            [self unscheduleFromRunLoop:[runLoop objectAtIndex:0] forMode:[runLoop objectAtIndex:1]];
+        }
+        
+        if (!_failed) {
+            [self _performDelegateBlock:^{
+                if ([self.delegate respondsToSelector:@selector(webSocket:didCloseWithCode:reason:wasClean:)]) {
+                    [self.delegate webSocket:self didCloseWithCode:_closeCode reason:_closeReason wasClean:YES];
+                }
+            }];
+        }
+        
+        _selfRetain = nil;
+    }
+}
+
+- (void)_addConsumerWithScanner:(stream_scanner)consumer callback:(data_callback)callback;
+{
+    [self assertOnWorkQueue];
+    [self _addConsumerWithScanner:consumer callback:callback dataLength:0];
+}
+
+- (void)_addConsumerWithDataLength:(size_t)dataLength callback:(data_callback)callback readToCurrentFrame:(BOOL)readToCurrentFrame unmaskBytes:(BOOL)unmaskBytes;
+{   
+    [self assertOnWorkQueue];
+    assert(dataLength);
+    
+    [_consumers addObject:[_consumerPool consumerWithScanner:nil handler:callback bytesNeeded:dataLength readToCurrentFrame:readToCurrentFrame unmaskBytes:unmaskBytes]];
+    [self _pumpScanner];
+}
+
+- (void)_addConsumerWithScanner:(stream_scanner)consumer callback:(data_callback)callback dataLength:(size_t)dataLength;
+{    
+    [self assertOnWorkQueue];
+    [_consumers addObject:[_consumerPool consumerWithScanner:consumer handler:callback bytesNeeded:dataLength readToCurrentFrame:NO unmaskBytes:NO]];
+    [self _pumpScanner];
+}
+
+
+static const char CRLFCRLFBytes[] = {'\r', '\n', '\r', '\n'};
+
+- (void)_readUntilHeaderCompleteWithCallback:(data_callback)dataHandler;
+{
+    [self _readUntilBytes:CRLFCRLFBytes length:sizeof(CRLFCRLFBytes) callback:dataHandler];
+}
+
+- (void)_readUntilBytes:(const void *)bytes length:(size_t)length callback:(data_callback)dataHandler;
+{
+    // TODO optimize so this can continue from where we last searched
+    stream_scanner consumer = ^size_t(NSData *data) {
+        __block size_t found_size = 0;
+        __block size_t match_count = 0;
+        
+        size_t size = data.length;
+        const unsigned char *buffer = data.bytes;
+        for (size_t i = 0; i < size; i++ ) {
+            if (((const unsigned char *)buffer)[i] == ((const unsigned char *)bytes)[match_count]) {
+                match_count += 1;
+                if (match_count == length) {
+                    found_size = i + 1;
+                    break;
+                }
+            } else {
+                match_count = 0;
+            }
+        }
+        return found_size;
+    };
+    [self _addConsumerWithScanner:consumer callback:dataHandler];
+}
+
+
+// Returns true if did work
+- (BOOL)_innerPumpScanner {
+    
+    BOOL didWork = NO;
+    
+    if (self.readyState >= SR_CLOSING) {
+        return didWork;
+    }
+    
+    if (!_consumers.count) {
+        return didWork;
+    }
+    
+    size_t curSize = _readBuffer.length - _readBufferOffset;
+    if (!curSize) {
+        return didWork;
+    }
+    
+    SRIOConsumer *consumer = [_consumers objectAtIndex:0];
+    
+    size_t bytesNeeded = consumer.bytesNeeded;
+    
+    size_t foundSize = 0;
+    if (consumer.consumer) {
+        NSData *tempView = [NSData dataWithBytesNoCopy:(char *)_readBuffer.bytes + _readBufferOffset length:_readBuffer.length - _readBufferOffset freeWhenDone:NO];  
+        foundSize = consumer.consumer(tempView);
+    } else {
+        assert(consumer.bytesNeeded);
+        if (curSize >= bytesNeeded) {
+            foundSize = bytesNeeded;
+        } else if (consumer.readToCurrentFrame) {
+            foundSize = curSize;
+        }
+    }
+    
+    NSData *slice = nil;
+    if (consumer.readToCurrentFrame || foundSize) {
+        NSRange sliceRange = NSMakeRange(_readBufferOffset, foundSize);
+        slice = [_readBuffer subdataWithRange:sliceRange];
+        
+        _readBufferOffset += foundSize;
+        
+        if (_readBufferOffset > 4096 && _readBufferOffset > (_readBuffer.length >> 1)) {
+            _readBuffer = [[NSMutableData alloc] initWithBytes:(char *)_readBuffer.bytes + _readBufferOffset length:_readBuffer.length - _readBufferOffset];            _readBufferOffset = 0;
+        }
+        
+        if (consumer.unmaskBytes) {
+            NSMutableData *mutableSlice = [slice mutableCopy];
+            
+            NSUInteger len = mutableSlice.length;
+            uint8_t *bytes = mutableSlice.mutableBytes;
+            
+            for (NSUInteger i = 0; i < len; i++) {
+                bytes[i] = bytes[i] ^ _currentReadMaskKey[_currentReadMaskOffset % sizeof(_currentReadMaskKey)];
+                _currentReadMaskOffset += 1;
+            }
+            
+            slice = mutableSlice;
+        }
+        
+        if (consumer.readToCurrentFrame) {
+            [_currentFrameData appendData:slice];
+            
+            _readOpCount += 1;
+            
+            if (_currentFrameOpcode == SROpCodeTextFrame) {
+                // Validate UTF8 stuff.
+                size_t currentDataSize = _currentFrameData.length;
+                if (_currentFrameOpcode == SROpCodeTextFrame && currentDataSize > 0) {
+                    // TODO: Optimize the crap out of this.  Don't really have to copy all the data each time
+                    
+                    size_t scanSize = currentDataSize - _currentStringScanPosition;
+                    
+                    NSData *scan_data = [_currentFrameData subdataWithRange:NSMakeRange(_currentStringScanPosition, scanSize)];
+                    int32_t valid_utf8_size = validate_dispatch_data_partial_string(scan_data);
+                    
+                    if (valid_utf8_size == -1) {
+                        [self closeWithCode:SRStatusCodeInvalidUTF8 reason:@"Text frames must be valid UTF-8"];
+                        dispatch_async(_workQueue, ^{
+                            [self _disconnect];
+                        });
+                        return didWork;
+                    } else {
+                        _currentStringScanPosition += valid_utf8_size;
+                    }
+                } 
+                
+            }
+            
+            consumer.bytesNeeded -= foundSize;
+            
+            if (consumer.bytesNeeded == 0) {
+                [_consumers removeObjectAtIndex:0];
+                consumer.handler(self, nil);
+                [_consumerPool returnConsumer:consumer];
+                didWork = YES;
+            }
+        } else if (foundSize) {
+            [_consumers removeObjectAtIndex:0];
+            consumer.handler(self, slice);
+            [_consumerPool returnConsumer:consumer];
+            didWork = YES;
+        }
+    }
+    return didWork;
+}
+
+-(void)_pumpScanner;
+{
+    [self assertOnWorkQueue];
+    
+    if (!_isPumping) {
+        _isPumping = YES;
+    } else {
+        return;
+    }
+    
+    while ([self _innerPumpScanner]) {
+        
+    }
+    
+    _isPumping = NO;
+}
+
+//#define NOMASK
+
+static const size_t SRFrameHeaderOverhead = 32;
+
+- (void)_sendFrameWithOpcode:(SROpCode)opcode data:(id)data;
+{
+    [self assertOnWorkQueue];
+    
+    if (nil == data) {
+        return;
+    }
+    
+    NSAssert([data isKindOfClass:[NSData class]] || [data isKindOfClass:[NSString class]], @"NSString or NSData");
+    
+    size_t payloadLength = [data isKindOfClass:[NSString class]] ? [(NSString *)data lengthOfBytesUsingEncoding:NSUTF8StringEncoding] : [data length];
+        
+    NSMutableData *frame = [[NSMutableData alloc] initWithLength:payloadLength + SRFrameHeaderOverhead];
+    if (!frame) {
+        [self closeWithCode:SRStatusCodeMessageTooBig reason:@"Message too big"];
+        return;
+    }
+    uint8_t *frame_buffer = (uint8_t *)[frame mutableBytes];
+    
+    // set fin
+    frame_buffer[0] = SRFinMask | opcode;
+    
+    BOOL useMask = YES;
+#ifdef NOMASK
+    useMask = NO;
+#endif
+    
+    if (useMask) {
+    // set the mask and header
+        frame_buffer[1] |= SRMaskMask;
+    }
+    
+    size_t frame_buffer_size = 2;
+    
+    const uint8_t *unmasked_payload = NULL;
+    if ([data isKindOfClass:[NSData class]]) {
+        unmasked_payload = (uint8_t *)[data bytes];
+    } else if ([data isKindOfClass:[NSString class]]) {
+        unmasked_payload =  (const uint8_t *)[data UTF8String];
+    } else {
+        return;
+    }
+    
+    if (payloadLength < 126) {
+        frame_buffer[1] |= payloadLength;
+    } else if (payloadLength <= UINT16_MAX) {
+        frame_buffer[1] |= 126;
+        *((uint16_t *)(frame_buffer + frame_buffer_size)) = EndianU16_BtoN((uint16_t)payloadLength);
+        frame_buffer_size += sizeof(uint16_t);
+    } else {
+        frame_buffer[1] |= 127;
+        *((uint64_t *)(frame_buffer + frame_buffer_size)) = EndianU64_BtoN((uint64_t)payloadLength);
+        frame_buffer_size += sizeof(uint64_t);
+    }
+        
+    if (!useMask) {
+        for (size_t i = 0; i < payloadLength; i++) {
+            frame_buffer[frame_buffer_size] = unmasked_payload[i];
+            frame_buffer_size += 1;
+        }
+    } else {
+        uint8_t *mask_key = frame_buffer + frame_buffer_size;
+        SecRandomCopyBytes(kSecRandomDefault, sizeof(uint32_t), (uint8_t *)mask_key);
+        frame_buffer_size += sizeof(uint32_t);
+        
+        // TODO: could probably optimize this with SIMD
+        for (size_t i = 0; i < payloadLength; i++) {
+            frame_buffer[frame_buffer_size] = unmasked_payload[i] ^ mask_key[i % sizeof(uint32_t)];
+            frame_buffer_size += 1;
+        }
+    }
+
+    assert(frame_buffer_size <= [frame length]);
+    frame.length = frame_buffer_size;
+    
+    [self _writeData:frame];
+}
+
+- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode;
+{
+    if (_secure && !_pinnedCertFound && (eventCode == NSStreamEventHasBytesAvailable || eventCode == NSStreamEventHasSpaceAvailable)) {
+        
+        NSArray *sslCerts = [_urlRequest SR_SSLPinnedCertificates];
+        if (sslCerts) {
+            SecTrustRef secTrust = (__bridge SecTrustRef)[aStream propertyForKey:(__bridge id)kCFStreamPropertySSLPeerTrust];
+            if (secTrust) {
+                NSInteger numCerts = SecTrustGetCertificateCount(secTrust);
+                for (NSInteger i = 0; i < numCerts && !_pinnedCertFound; i++) {
+                    SecCertificateRef cert = SecTrustGetCertificateAtIndex(secTrust, i);
+                    NSData *certData = CFBridgingRelease(SecCertificateCopyData(cert));
+                    
+                    for (id ref in sslCerts) {
+                        SecCertificateRef trustedCert = (__bridge SecCertificateRef)ref;
+                        NSData *trustedCertData = CFBridgingRelease(SecCertificateCopyData(trustedCert));
+                        
+                        if ([trustedCertData isEqualToData:certData]) {
+                            _pinnedCertFound = YES;
+                            break;
+                        }
+                    }
+                }
+            }
+            
+            if (!_pinnedCertFound) {
+                dispatch_async(_workQueue, ^{
+                    [self _failWithError:[NSError errorWithDomain:SRWebSocketErrorDomain code:23556 userInfo:[NSDictionary dictionaryWithObject:[NSString stringWithFormat:@"Invalid server cert"] forKey:NSLocalizedDescriptionKey]]];
+                });
+                return;
+            }
+        }
+    }
+
+    dispatch_async(_workQueue, ^{
+        switch (eventCode) {
+            case NSStreamEventOpenCompleted: {
+                SRFastLog(@"NSStreamEventOpenCompleted %@", aStream);
+                if (self.readyState >= SR_CLOSING) {
+                    return;
+                }
+                assert(_readBuffer);
+                
+                if (self.readyState == SR_CONNECTING && aStream == _inputStream) {
+                    [self didConnect];
+                }
+                [self _pumpWriting];
+                [self _pumpScanner];
+                break;
+            }
+                
+            case NSStreamEventErrorOccurred: {
+                SRFastLog(@"NSStreamEventErrorOccurred %@ %@", aStream, [[aStream streamError] copy]);
+                /// TODO specify error better!
+                [self _failWithError:aStream.streamError];
+                _readBufferOffset = 0;
+                [_readBuffer setLength:0];
+                break;
+                
+            }
+                
+            case NSStreamEventEndEncountered: {
+                [self _pumpScanner];
+                SRFastLog(@"NSStreamEventEndEncountered %@", aStream);
+                if (aStream.streamError) {
+                    [self _failWithError:aStream.streamError];
+                } else {
+                    if (self.readyState != SR_CLOSED) {
+                        self.readyState = SR_CLOSED;
+                        _selfRetain = nil;
+                    }
+
+                    if (!_sentClose && !_failed) {
+                        _sentClose = YES;
+                        // If we get closed in this state it's probably not clean because we should be sending this when we send messages
+                        [self _performDelegateBlock:^{
+                            if ([self.delegate respondsToSelector:@selector(webSocket:didCloseWithCode:reason:wasClean:)]) {
+                                [self.delegate webSocket:self didCloseWithCode:SRStatusCodeGoingAway reason:@"Stream end encountered" wasClean:NO];
+                            }
+                        }];
+                    }
+                }
+                
+                break;
+            }
+                
+            case NSStreamEventHasBytesAvailable: {
+                SRFastLog(@"NSStreamEventHasBytesAvailable %@", aStream);
+                const int bufferSize = 2048;
+                uint8_t buffer[bufferSize];
+                
+                while (_inputStream.hasBytesAvailable) {
+                    NSInteger bytes_read = [_inputStream read:buffer maxLength:bufferSize];
+                    
+                    if (bytes_read > 0) {
+                        [_readBuffer appendBytes:buffer length:bytes_read];
+                    } else if (bytes_read < 0) {
+                        [self _failWithError:_inputStream.streamError];
+                    }
+                    
+                    if (bytes_read != bufferSize) {
+                        break;
+                    }
+                };
+                [self _pumpScanner];
+                break;
+            }
+                
+            case NSStreamEventHasSpaceAvailable: {
+                SRFastLog(@"NSStreamEventHasSpaceAvailable %@", aStream);
+                [self _pumpWriting];
+                break;
+            }
+                
+            default:
+                SRFastLog(@"(default)  %@", aStream);
+                break;
+        }
+    });
+}
+
+@end
+
+
+@implementation SRIOConsumer
+
+@synthesize bytesNeeded = _bytesNeeded;
+@synthesize consumer = _scanner;
+@synthesize handler = _handler;
+@synthesize readToCurrentFrame = _readToCurrentFrame;
+@synthesize unmaskBytes = _unmaskBytes;
+
+- (void)setupWithScanner:(stream_scanner)scanner handler:(data_callback)handler bytesNeeded:(size_t)bytesNeeded readToCurrentFrame:(BOOL)readToCurrentFrame unmaskBytes:(BOOL)unmaskBytes;
+{
+    _scanner = [scanner copy];
+    _handler = [handler copy];
+    _bytesNeeded = bytesNeeded;
+    _readToCurrentFrame = readToCurrentFrame;
+    _unmaskBytes = unmaskBytes;
+    assert(_scanner || _bytesNeeded);
+}
+
+
+@end
+
+
+@implementation SRIOConsumerPool {
+    NSUInteger _poolSize;
+    NSMutableArray *_bufferedConsumers;
+}
+
+- (id)initWithBufferCapacity:(NSUInteger)poolSize;
+{
+    self = [super init];
+    if (self) {
+        _poolSize = poolSize;
+        _bufferedConsumers = [[NSMutableArray alloc] initWithCapacity:poolSize];
+    }
+    return self;
+}
+
+- (id)init
+{
+    return [self initWithBufferCapacity:8];
+}
+
+- (SRIOConsumer *)consumerWithScanner:(stream_scanner)scanner handler:(data_callback)handler bytesNeeded:(size_t)bytesNeeded readToCurrentFrame:(BOOL)readToCurrentFrame unmaskBytes:(BOOL)unmaskBytes;
+{
+    SRIOConsumer *consumer = nil;
+    if (_bufferedConsumers.count) {
+        consumer = [_bufferedConsumers lastObject];
+        [_bufferedConsumers removeLastObject];
+    } else {
+        consumer = [[SRIOConsumer alloc] init];
+    }
+    
+    [consumer setupWithScanner:scanner handler:handler bytesNeeded:bytesNeeded readToCurrentFrame:readToCurrentFrame unmaskBytes:unmaskBytes];
+    
+    return consumer;
+}
+
+- (void)returnConsumer:(SRIOConsumer *)consumer;
+{
+    if (_bufferedConsumers.count < _poolSize) {
+        [_bufferedConsumers addObject:consumer];
+    }
+}
+
+@end
+
+
+@implementation  NSURLRequest (CertificateAdditions)
+
+- (NSArray *)SR_SSLPinnedCertificates;
+{
+    return [NSURLProtocol propertyForKey:@"SR_SSLPinnedCertificates" inRequest:self];
+}
+
+@end
+
+@implementation  NSMutableURLRequest (CertificateAdditions)
+
+- (NSArray *)SR_SSLPinnedCertificates;
+{
+    return [NSURLProtocol propertyForKey:@"SR_SSLPinnedCertificates" inRequest:self];
+}
+
+- (void)setSR_SSLPinnedCertificates:(NSArray *)SR_SSLPinnedCertificates;
+{
+    [NSURLProtocol setProperty:SR_SSLPinnedCertificates forKey:@"SR_SSLPinnedCertificates" inRequest:self];
+}
+
+@end
+
+@implementation NSURL (SRWebSocket)
+
+- (NSString *)SR_origin;
+{
+    NSString *scheme = [self.scheme lowercaseString];
+        
+    if ([scheme isEqualToString:@"wss"]) {
+        scheme = @"https";
+    } else if ([scheme isEqualToString:@"ws"]) {
+        scheme = @"http";
+    }
+    
+    if (self.port) {
+        return [NSString stringWithFormat:@"%@://%@:%@/", scheme, self.host, self.port];
+    } else {
+        return [NSString stringWithFormat:@"%@://%@/", scheme, self.host];
+    }
+}
+
+@end
+
+//#define SR_ENABLE_LOG
+
+static inline void SRFastLog(NSString *format, ...)  {
+#ifdef SR_ENABLE_LOG
+    __block va_list arg_list;
+    va_start (arg_list, format);
+    
+    NSString *formattedString = [[NSString alloc] initWithFormat:format arguments:arg_list];
+    
+    va_end(arg_list);
+    
+    NSLog(@"[SR] %@", formattedString);
+#endif
+}
+
+
+#ifdef HAS_ICU
+
+static inline int32_t validate_dispatch_data_partial_string(NSData *data) {
+    if ([data length] > INT32_MAX) {
+        // INT32_MAX is the limit so long as this Framework is using 32 bit ints everywhere.
+        return -1;
+    }
+
+    int32_t size = (int32_t)[data length];
+
+    const void * contents = [data bytes];
+    const uint8_t *str = (const uint8_t *)contents;
+    
+    UChar32 codepoint = 1;
+    int32_t offset = 0;
+    int32_t lastOffset = 0;
+    while(offset < size && codepoint > 0)  {
+        lastOffset = offset;
+        U8_NEXT(str, offset, size, codepoint);
+    }
+    
+    if (codepoint == -1) {
+        // Check to see if the last byte is valid or whether it was just continuing
+        if (!U8_IS_LEAD(str[lastOffset]) || U8_COUNT_TRAIL_BYTES(str[lastOffset]) + lastOffset < (int32_t)size) {
+            
+            size = -1;
+        } else {
+            uint8_t leadByte = str[lastOffset];
+            U8_MASK_LEAD_BYTE(leadByte, U8_COUNT_TRAIL_BYTES(leadByte));
+            
+            for (int i = lastOffset + 1; i < offset; i++) {
+                if (U8_IS_SINGLE(str[i]) || U8_IS_LEAD(str[i]) || !U8_IS_TRAIL(str[i])) {
+                    size = -1;
+                }
+            }
+            
+            if (size != -1) {
+                size = lastOffset;
+            }
+        }
+    }
+    
+    if (size != -1 && ![[NSString alloc] initWithBytesNoCopy:(char *)[data bytes] length:size encoding:NSUTF8StringEncoding freeWhenDone:NO]) {
+        size = -1;
+    }
+    
+    return size;
+}
+
+#else
+
+// This is a hack, and probably not optimal
+static inline int32_t validate_dispatch_data_partial_string(NSData *data) {
+    static const int maxCodepointSize = 3;
+    
+    for (int i = 0; i < maxCodepointSize; i++) {
+        NSString *str = [[NSString alloc] initWithBytesNoCopy:(char *)data.bytes length:data.length - i encoding:NSUTF8StringEncoding freeWhenDone:NO];
+        if (str) {
+            return data.length - i;
+        }
+    }
+    
+    return -1;
+}
+
+#endif
+
+static _SRRunLoopThread *networkThread = nil;
+static NSRunLoop *networkRunLoop = nil;
+
+@implementation NSRunLoop (SRWebSocket)
+
++ (NSRunLoop *)SR_networkRunLoop {
+    static dispatch_once_t onceToken;
+    dispatch_once(&onceToken, ^{
+        networkThread = [[_SRRunLoopThread alloc] init];
+        networkThread.name = @"com.squareup.SocketRocket.NetworkThread";
+        [networkThread start];
+        networkRunLoop = networkThread.runLoop;
+    });
+    
+    return networkRunLoop;
+}
+
+@end
+
+
+@implementation _SRRunLoopThread {
+    dispatch_group_t _waitGroup;
+}
+
+@synthesize runLoop = _runLoop;
+
+- (void)dealloc
+{
+    sr_dispatch_release(_waitGroup);
+}
+
+- (id)init
+{
+    self = [super init];
+    if (self) {
+        _waitGroup = dispatch_group_create();
+        dispatch_group_enter(_waitGroup);
+    }
+    return self;
+}
+
+- (void)main;
+{
+    @autoreleasepool {
+        _runLoop = [NSRunLoop currentRunLoop];
+        dispatch_group_leave(_waitGroup);
+        
+        NSTimer *timer = [[NSTimer alloc] initWithFireDate:[NSDate distantFuture] interval:0.0 target:nil selector:nil userInfo:nil repeats:NO];
+        [_runLoop addTimer:timer forMode:NSDefaultRunLoopMode];
+        
+        while ([_runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]) {
+            
+        }
+        assert(NO);
+    }
+}
+
+- (NSRunLoop *)runLoop;
+{
+    dispatch_group_wait(_waitGroup, DISPATCH_TIME_FOREVER);
+    return _runLoop;
+}
+
+@end