iOS AppRTC: First unit test.

Tests basic session ICE connection by stubbing out network components, which have been refactored to faciliate testing.

BUG=3994
R=jiayl@webrtc.org, kjellander@webrtc.org, phoglund@webrtc.org

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

git-svn-id: http://webrtc.googlecode.com/svn/trunk@8002 4adac7df-926f-26a2-2b94-8c16560cd09d
diff --git a/.gitignore b/.gitignore
index 1082352..cb684c9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -94,6 +94,7 @@
 /third_party/modp_b64
 /third_party/nss
 /third_party/oauth2
+/third_party/ocmock
 /third_party/openmax_dl
 /third_party/opus
 /third_party/protobuf
diff --git a/setup_links.py b/setup_links.py
index 0dd453c..5891270 100755
--- a/setup_links.py
+++ b/setup_links.py
@@ -55,6 +55,7 @@
   'third_party/libyuv',
   'third_party/llvm-build',
   'third_party/nss',
+  'third_party/ocmock',
   'third_party/openmax_dl',
   'third_party/opus',
   'third_party/protobuf',
diff --git a/talk/build/ios_tests.gypi b/talk/build/objc_app.gypi
similarity index 68%
rename from talk/build/ios_tests.gypi
rename to talk/build/objc_app.gypi
index baf1f10..a479802 100644
--- a/talk/build/ios_tests.gypi
+++ b/talk/build/objc_app.gypi
@@ -29,27 +29,20 @@
 # used as an iOS or OS/X application.
 
 {
-  'conditions': [
-    ['OS=="ios"', {
-      'variables': {
-        'infoplist_file': './ios_test.plist',
-      },
-      'mac_bundle': 1,
-      'mac_bundle_resources': [
-        '<(infoplist_file)',
-      ],
-      # The plist is listed above so that it appears in XCode's file list,
-      # but we don't actually want to bundle it.
-      'mac_bundle_resources!': [
-        '<(infoplist_file)',
-      ],
-      'xcode_settings': {
-        'CLANG_ENABLE_OBJC_ARC': 'YES',
-        # common.gypi enables this for mac but we want this to be disabled
-        # like it is for ios.
-        'CLANG_WARN_OBJC_MISSING_PROPERTY_SYNTHESIS': 'NO',
-        'INFOPLIST_FILE': '<(infoplist_file)',
-      },
-    }],
-  ],  # conditions
+  'variables': {
+    'infoplist_file': './objc_app.plist',
+  },
+  'mac_bundle': 1,
+  'mac_bundle_resources': [
+    '<(infoplist_file)',
+  ],
+  # The plist is listed above so that it appears in XCode's file list,
+  # but we don't actually want to bundle it.
+  'mac_bundle_resources!': [
+    '<(infoplist_file)',
+  ],
+  'xcode_settings': {
+    'CLANG_ENABLE_OBJC_ARC': 'YES',
+    'INFOPLIST_FILE': '<(infoplist_file)',
+  },
 }
diff --git a/talk/build/ios_test.plist b/talk/build/objc_app.plist
similarity index 100%
rename from talk/build/ios_test.plist
rename to talk/build/objc_app.plist
diff --git a/talk/examples/objc/AppRTCDemo/ARDAppClient+Internal.h b/talk/examples/objc/AppRTCDemo/ARDAppClient+Internal.h
new file mode 100644
index 0000000..068c041
--- /dev/null
+++ b/talk/examples/objc/AppRTCDemo/ARDAppClient+Internal.h
@@ -0,0 +1,68 @@
+/*
+ * 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 "ARDRoomServerClient.h"
+#import "ARDSignalingChannel.h"
+#import "ARDTURNClient.h"
+#import "RTCPeerConnection.h"
+#import "RTCPeerConnectionDelegate.h"
+#import "RTCPeerConnectionFactory.h"
+#import "RTCSessionDescriptionDelegate.h"
+
+@interface ARDAppClient () <ARDSignalingChannelDelegate,
+    RTCPeerConnectionDelegate, RTCSessionDescriptionDelegate>
+
+@property(nonatomic, strong) id<ARDRoomServerClient> roomServerClient;
+@property(nonatomic, strong) id<ARDSignalingChannel> channel;
+@property(nonatomic, strong) id<ARDTURNClient> turnClient;
+
+@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;
+
+@property(nonatomic, strong)
+    RTCMediaConstraints *defaultPeerConnectionConstraints;
+
+- (instancetype)initWithRoomServerClient:(id<ARDRoomServerClient>)rsClient
+                        signalingChannel:(id<ARDSignalingChannel>)channel
+                              turnClient:(id<ARDTURNClient>)turnClient
+                                delegate:(id<ARDAppClientDelegate>)delegate;
+
+@end
diff --git a/talk/examples/objc/AppRTCDemo/ARDAppClient.h b/talk/examples/objc/AppRTCDemo/ARDAppClient.h
index d742ea3..fa3a634 100644
--- a/talk/examples/objc/AppRTCDemo/ARDAppClient.h
+++ b/talk/examples/objc/AppRTCDemo/ARDAppClient.h
@@ -45,6 +45,9 @@
     didChangeState:(ARDAppClientState)state;
 
 - (void)appClient:(ARDAppClient *)client
+    didChangeConnectionState:(RTCICEConnectionState)state;
+
+- (void)appClient:(ARDAppClient *)client
     didReceiveLocalVideoTrack:(RTCVideoTrack *)localVideoTrack;
 
 - (void)appClient:(ARDAppClient *)client
diff --git a/talk/examples/objc/AppRTCDemo/ARDAppClient.m b/talk/examples/objc/AppRTCDemo/ARDAppClient.m
index d72e8bb..fef7727 100644
--- a/talk/examples/objc/AppRTCDemo/ARDAppClient.m
+++ b/talk/examples/objc/AppRTCDemo/ARDAppClient.m
@@ -25,38 +25,26 @@
  * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */
 
-#import "ARDAppClient.h"
+#import "ARDAppClient+Internal.h"
 
 #import <AVFoundation/AVFoundation.h>
 
+#import "ARDAppEngineClient.h"
+#import "ARDCEODTURNClient.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 "RTCICEServer.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://apprtc.appspot.com";
-static NSString *kARDRoomServerRegisterFormat =
-    @"https://apprtc.appspot.com/register/%@";
-static NSString *kARDRoomServerMessageFormat =
-    @"https://apprtc.appspot.com/message/%@/%@";
-static NSString *kARDRoomServerByeFormat =
-    @"https://apprtc.appspot.com/bye/%@/%@";
-
 static NSString *kARDDefaultSTUNServerUrl =
     @"stun:stun.l.google.com:19302";
 // TODO(tkchin): figure out a better username for CEOD statistics.
@@ -69,34 +57,16 @@
 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
+static NSInteger kARDAppClientErrorInvalidClient = -5;
+static NSInteger kARDAppClientErrorInvalidRoom = -6;
 
 @implementation ARDAppClient
 
 @synthesize delegate = _delegate;
 @synthesize state = _state;
+@synthesize roomServerClient = _roomServerClient;
 @synthesize channel = _channel;
+@synthesize turnClient = _turnClient;
 @synthesize peerConnection = _peerConnection;
 @synthesize factory = _factory;
 @synthesize messageQueue = _messageQueue;
@@ -108,17 +78,46 @@
 @synthesize iceServers = _iceServers;
 @synthesize webSocketURL = _websocketURL;
 @synthesize webSocketRestURL = _websocketRestURL;
+@synthesize defaultPeerConnectionConstraints =
+    _defaultPeerConnectionConstraints;
 
 - (instancetype)initWithDelegate:(id<ARDAppClientDelegate>)delegate {
   if (self = [super init]) {
+    _roomServerClient = [[ARDAppEngineClient alloc] init];
     _delegate = delegate;
-    _factory = [[RTCPeerConnectionFactory alloc] init];
-    _messageQueue = [NSMutableArray array];
-    _iceServers = [NSMutableArray arrayWithObject:[self defaultSTUNServer]];
+    NSURL *turnRequestURL = [NSURL URLWithString:kARDTurnRequestUrl];
+    _turnClient = [[ARDCEODTURNClient alloc] initWithURL:turnRequestURL];
+    [self configure];
   }
   return self;
 }
 
+// TODO(tkchin): Provide signaling channel factory interface so we can recreate
+// channel if we need to on network failure. Also, make this the default public
+// constructor.
+- (instancetype)initWithRoomServerClient:(id<ARDRoomServerClient>)rsClient
+                        signalingChannel:(id<ARDSignalingChannel>)channel
+                              turnClient:(id<ARDTURNClient>)turnClient
+                                delegate:(id<ARDAppClientDelegate>)delegate {
+  NSParameterAssert(rsClient);
+  NSParameterAssert(channel);
+  NSParameterAssert(turnClient);
+  if (self = [super init]) {
+    _roomServerClient = rsClient;
+    _channel = channel;
+    _turnClient = turnClient;
+    _delegate = delegate;
+    [self configure];
+  }
+  return self;
+}
+
+- (void)configure {
+  _factory = [[RTCPeerConnectionFactory alloc] init];
+  _messageQueue = [NSMutableArray array];
+  _iceServers = [NSMutableArray arrayWithObject:[self defaultSTUNServer]];
+}
+
 - (void)dealloc {
   [self disconnect];
 }
@@ -139,9 +138,11 @@
 
   // Request TURN.
   __weak ARDAppClient *weakSelf = self;
-  NSURL *turnRequestURL = [NSURL URLWithString:kARDTurnRequestUrl];
-  [self requestTURNServersWithURL:turnRequestURL
-                completionHandler:^(NSArray *turnServers) {
+  [_turnClient requestServersWithCompletionHandler:^(NSArray *turnServers,
+                                                     NSError *error) {
+    if (error) {
+      NSLog(@"Error retrieving TURN servers: %@", error);
+    }
     ARDAppClient *strongSelf = weakSelf;
     [strongSelf.iceServers addObjectsFromArray:turnServers];
     strongSelf.isTurnComplete = YES;
@@ -149,23 +150,21 @@
   }];
 
   // Register with room server.
-  [self registerWithRoomServerForRoomId:roomId
-                      completionHandler:^(ARDRegisterResponse *response) {
+  [_roomServerClient registerForRoomId:roomId
+      completionHandler:^(ARDRegisterResponse *response, NSError *error) {
     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];
+    if (error) {
       [strongSelf.delegate appClient:strongSelf didError:error];
       return;
     }
+    NSError *registerError =
+        [[strongSelf class] errorForRegisterResultType:response.result];
+    if (registerError) {
+      NSLog(@"Failed to register with room server.");
+      [strongSelf disconnect];
+      [strongSelf.delegate appClient:strongSelf didError:registerError];
+      return;
+    }
     NSLog(@"Registered with room server.");
     strongSelf.roomId = response.roomId;
     strongSelf.clientId = response.clientId;
@@ -191,14 +190,15 @@
     return;
   }
   if (self.isRegisteredWithRoomServer) {
-    [self unregisterWithRoomServer];
+    [_roomServerClient deregisterForRoomId:_roomId
+                                  clientId:_clientId
+                         completionHandler:nil];
   }
   if (_channel) {
-    if (_channel.state == kARDWebSocketChannelStateRegistered) {
+    if (_channel.state == kARDSignalingChannelStateRegistered) {
       // Tell the other client we're hanging up.
       ARDByeMessage *byeMessage = [[ARDByeMessage alloc] init];
-      NSData *byeData = [byeMessage JSONData];
-      [_channel sendData:byeData];
+      [_channel sendMessage:byeMessage];
     }
     // Disconnect from collider.
     _channel = nil;
@@ -212,9 +212,9 @@
   self.state = kARDAppClientStateDisconnected;
 }
 
-#pragma mark - ARDWebSocketChannelDelegate
+#pragma mark - ARDSignalingChannelDelegate
 
-- (void)channel:(ARDWebSocketChannel *)channel
+- (void)channel:(id<ARDSignalingChannel>)channel
     didReceiveMessage:(ARDSignalingMessage *)message {
   switch (message.type) {
     case kARDSignalingMessageTypeOffer:
@@ -232,15 +232,15 @@
   [self drainMessageQueueIfReady];
 }
 
-- (void)channel:(ARDWebSocketChannel *)channel
-    didChangeState:(ARDWebSocketChannelState)state {
+- (void)channel:(id<ARDSignalingChannel>)channel
+    didChangeState:(ARDSignalingChannelState)state {
   switch (state) {
-    case kARDWebSocketChannelStateOpen:
+    case kARDSignalingChannelStateOpen:
       break;
-    case kARDWebSocketChannelStateRegistered:
+    case kARDSignalingChannelStateRegistered:
       break;
-    case kARDWebSocketChannelStateClosed:
-    case kARDWebSocketChannelStateError:
+    case kARDSignalingChannelStateClosed:
+    case kARDSignalingChannelStateError:
       // TODO(tkchin): reconnection scenarios. Right now we just disconnect
       // completely if the websocket connection fails.
       [self disconnect];
@@ -281,6 +281,9 @@
 - (void)peerConnection:(RTCPeerConnection *)peerConnection
     iceConnectionChanged:(RTCICEConnectionState)newState {
   NSLog(@"ICE state changed: %d", newState);
+  dispatch_async(dispatch_get_main_queue(), ^{
+    [_delegate appClient:self didChangeConnectionState:newState];
+  });
 }
 
 - (void)peerConnection:(RTCPeerConnection *)peerConnection
@@ -430,9 +433,26 @@
 
 - (void)sendSignalingMessage:(ARDSignalingMessage *)message {
   if (_isInitiator) {
-    [self sendSignalingMessageToRoomServer:message completionHandler:nil];
+    __weak ARDAppClient *weakSelf = self;
+    [_roomServerClient sendMessage:message
+                         forRoomId:_roomId
+                          clientId:_clientId
+                 completionHandler:^(ARDMessageResponse *response,
+                                     NSError *error) {
+      ARDAppClient *strongSelf = weakSelf;
+      if (error) {
+        [strongSelf.delegate appClient:strongSelf didError:error];
+        return;
+      }
+      NSError *messageError =
+          [[strongSelf class] errorForMessageResultType:response.result];
+      if (messageError) {
+        [strongSelf.delegate appClient:strongSelf didError:messageError];
+        return;
+      }
+    }];
   } else {
-    [self sendSignalingMessageToCollider:message];
+    [_channel sendMessage:message];
   }
 }
 
@@ -473,142 +493,6 @@
   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 {
@@ -616,18 +500,15 @@
     return;
   }
   // Open WebSocket connection.
-  _channel =
-      [[ARDWebSocketChannel alloc] initWithURL:_websocketURL
-                                       restURL:_websocketRestURL
-                                      delegate:self];
+  if (!_channel) {
+    _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 {
@@ -655,6 +536,9 @@
 }
 
 - (RTCMediaConstraints *)defaultPeerConnectionConstraints {
+  if (_defaultPeerConnectionConstraints) {
+    return _defaultPeerConnectionConstraints;
+  }
   NSArray *optionalConstraints = @[
       [[RTCPair alloc] initWithKey:@"DtlsSrtpKeyAgreement" value:@"true"]
   ];
@@ -672,4 +556,61 @@
                                   password:@""];
 }
 
+#pragma mark - Errors
+
++ (NSError *)errorForRegisterResultType:(ARDRegisterResultType)resultType {
+  NSError *error = nil;
+  switch (resultType) {
+    case kARDRegisterResultTypeSuccess:
+      break;
+    case kARDRegisterResultTypeUnknown: {
+      error = [[NSError alloc] initWithDomain:kARDAppClientErrorDomain
+                                         code:kARDAppClientErrorUnknown
+                                     userInfo:@{
+        NSLocalizedDescriptionKey: @"Unknown error.",
+      }];
+      break;
+    }
+    case kARDRegisterResultTypeFull: {
+      error = [[NSError alloc] initWithDomain:kARDAppClientErrorDomain
+                                         code:kARDAppClientErrorRoomFull
+                                     userInfo:@{
+        NSLocalizedDescriptionKey: @"Room is full.",
+      }];
+      break;
+    }
+  }
+  return error;
+}
+
++ (NSError *)errorForMessageResultType:(ARDMessageResultType)resultType {
+  NSError *error = nil;
+  switch (resultType) {
+    case kARDMessageResultTypeSuccess:
+      break;
+    case kARDMessageResultTypeUnknown:
+      error = [[NSError alloc] initWithDomain:kARDAppClientErrorDomain
+                                         code:kARDAppClientErrorUnknown
+                                     userInfo:@{
+        NSLocalizedDescriptionKey: @"Unknown error.",
+      }];
+      break;
+    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;
+  }
+  return error;
+}
+
 @end
diff --git a/talk/examples/objc/AppRTCDemo/ARDAppEngineClient.h b/talk/examples/objc/AppRTCDemo/ARDAppEngineClient.h
new file mode 100644
index 0000000..2ec2ef8
--- /dev/null
+++ b/talk/examples/objc/AppRTCDemo/ARDAppEngineClient.h
@@ -0,0 +1,31 @@
+/*
+ * 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 "ARDRoomServerClient.h"
+
+@interface ARDAppEngineClient : NSObject <ARDRoomServerClient>
+@end
diff --git a/talk/examples/objc/AppRTCDemo/ARDAppEngineClient.m b/talk/examples/objc/AppRTCDemo/ARDAppEngineClient.m
new file mode 100644
index 0000000..44d3801
--- /dev/null
+++ b/talk/examples/objc/AppRTCDemo/ARDAppEngineClient.m
@@ -0,0 +1,178 @@
+/*
+ * 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 "ARDAppEngineClient.h"
+
+#import "ARDMessageResponse.h"
+#import "ARDRegisterResponse.h"
+#import "ARDSignalingMessage.h"
+#import "ARDUtilities.h"
+
+// TODO(tkchin): move these to a configuration object.
+static NSString *kARDRoomServerHostUrl =
+    @"https://apprtc.appspot.com";
+static NSString *kARDRoomServerRegisterFormat =
+    @"https://apprtc.appspot.com/register/%@";
+static NSString *kARDRoomServerMessageFormat =
+    @"https://apprtc.appspot.com/message/%@/%@";
+static NSString *kARDRoomServerByeFormat =
+    @"https://apprtc.appspot.com/bye/%@/%@";
+
+static NSString *kARDAppEngineClientErrorDomain = @"ARDAppEngineClient";
+static NSInteger kARDAppEngineClientErrorBadResponse = -1;
+
+@implementation ARDAppEngineClient
+
+#pragma mark - ARDRoomServerClient
+
+- (void)registerForRoomId:(NSString *)roomId
+    completionHandler:(void (^)(ARDRegisterResponse *response,
+                                NSError *error))completionHandler {
+  NSParameterAssert(roomId.length);
+
+  NSString *urlString =
+      [NSString stringWithFormat:kARDRoomServerRegisterFormat, roomId];
+  NSURL *roomURL = [NSURL URLWithString:urlString];
+  NSLog(@"Registering with room server.");
+  NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:roomURL];
+  request.HTTPMethod = @"POST";
+  __weak ARDAppEngineClient *weakSelf = self;
+  [NSURLConnection sendAsyncRequest:request
+                  completionHandler:^(NSURLResponse *response,
+                                      NSData *data,
+                                      NSError *error) {
+    ARDAppEngineClient *strongSelf = weakSelf;
+    if (error) {
+      if (completionHandler) {
+        completionHandler(nil, error);
+      }
+      return;
+    }
+    ARDRegisterResponse *registerResponse =
+        [ARDRegisterResponse responseFromJSONData:data];
+    if (!registerResponse) {
+      if (completionHandler) {
+        NSError *error = [[self class] badResponseError];
+        completionHandler(nil, error);
+      }
+      return;
+    }
+    if (completionHandler) {
+      completionHandler(registerResponse, nil);
+    }
+  }];
+}
+
+- (void)sendMessage:(ARDSignalingMessage *)message
+            forRoomId:(NSString *)roomId
+             clientId:(NSString *)clientId
+    completionHandler:(void (^)(ARDMessageResponse *response,
+                                NSError *error))completionHandler {
+  NSParameterAssert(message);
+  NSParameterAssert(roomId.length);
+  NSParameterAssert(clientId.length);
+
+  NSData *data = [message JSONData];
+  NSString *urlString =
+      [NSString stringWithFormat:
+          kARDRoomServerMessageFormat, roomId, clientId];
+  NSURL *url = [NSURL URLWithString:urlString];
+  NSLog(@"C->RS POST: %@", message);
+  NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
+  request.HTTPMethod = @"POST";
+  request.HTTPBody = data;
+  __weak ARDAppEngineClient *weakSelf = self;
+  [NSURLConnection sendAsyncRequest:request
+                  completionHandler:^(NSURLResponse *response,
+                                      NSData *data,
+                                      NSError *error) {
+    ARDAppEngineClient *strongSelf = weakSelf;
+    if (error) {
+      if (completionHandler) {
+        completionHandler(nil, error);
+      }
+      return;
+    }
+    ARDMessageResponse *messageResponse =
+        [ARDMessageResponse responseFromJSONData:data];
+    if (!messageResponse) {
+      if (completionHandler) {
+        NSError *error = [[self class] badResponseError];
+        completionHandler(nil, error);
+      }
+      return;
+    }
+    if (completionHandler) {
+      completionHandler(messageResponse, nil);
+    }
+  }];
+}
+
+- (void)deregisterForRoomId:(NSString *)roomId
+                   clientId:(NSString *)clientId
+          completionHandler:(void (^)(NSError *error))completionHandler {
+  NSParameterAssert(roomId.length);
+  NSParameterAssert(clientId.length);
+
+  NSString *urlString =
+      [NSString stringWithFormat:kARDRoomServerByeFormat, roomId, clientId];
+  NSURL *url = [NSURL URLWithString:urlString];
+  NSURLRequest *request = [NSURLRequest requestWithURL:url];
+  NSURLResponse *response = nil;
+  NSError *error = 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");
+  [NSURLConnection sendSynchronousRequest:request
+                        returningResponse:&response
+                                    error:&error];
+  if (error) {
+    NSLog(@"Error unregistering from room server: %@", error);
+    if (completionHandler) {
+      completionHandler(error);
+    }
+    return;
+  }
+  NSLog(@"Unregistered from room server.");
+  if (completionHandler) {
+    completionHandler(nil);
+  }
+}
+
+#pragma mark - Private
+
++ (NSError *)badResponseError {
+  NSError *error =
+      [[NSError alloc] initWithDomain:kARDAppEngineClientErrorDomain
+                                 code:kARDAppEngineClientErrorBadResponse
+                             userInfo:@{
+    NSLocalizedDescriptionKey: @"Error parsing response.",
+  }];
+  return error;
+}
+
+@end
diff --git a/talk/examples/objc/AppRTCDemo/ARDCEODTURNClient.h b/talk/examples/objc/AppRTCDemo/ARDCEODTURNClient.h
new file mode 100644
index 0000000..81be9e8
--- /dev/null
+++ b/talk/examples/objc/AppRTCDemo/ARDCEODTURNClient.h
@@ -0,0 +1,35 @@
+/*
+ * 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 "ARDTURNClient.h"
+
+// Requests TURN server urls from compute engine on demand.
+@interface ARDCEODTURNClient : NSObject <ARDTURNClient>
+
+- (instancetype)initWithURL:(NSURL *)url;
+
+@end
diff --git a/talk/examples/objc/AppRTCDemo/ARDCEODTURNClient.m b/talk/examples/objc/AppRTCDemo/ARDCEODTURNClient.m
new file mode 100644
index 0000000..f63a289
--- /dev/null
+++ b/talk/examples/objc/AppRTCDemo/ARDCEODTURNClient.m
@@ -0,0 +1,83 @@
+/*
+ * 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 "ARDCEODTURNClient.h"
+
+#import "ARDUtilities.h"
+#import "RTCICEServer+JSON.h"
+
+// TODO(tkchin): move this to a configuration object.
+static NSString *kTURNOriginURLString = @"https://apprtc.appspot.com";
+static NSString *kARDCEODTURNClientErrorDomain = @"ARDCEODTURNClient";
+static NSInteger kARDCEODTURNClientErrorBadResponse = -1;
+
+@implementation ARDCEODTURNClient {
+  NSURL *_url;
+}
+
+- (instancetype)initWithURL:(NSURL *)url {
+  NSParameterAssert([url absoluteString].length);
+  if (self = [super init]) {
+    _url = url;
+  }
+  return self;
+}
+
+- (void)requestServersWithCompletionHandler:
+    (void (^)(NSArray *turnServers,
+              NSError *error))completionHandler {
+  NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:_url];
+  // We need to set origin because TURN provider whitelists requests based on
+  // origin.
+  [request addValue:@"Mozilla/5.0" forHTTPHeaderField:@"user-agent"];
+  [request addValue:kTURNOriginURLString forHTTPHeaderField:@"origin"];
+  [NSURLConnection sendAsyncRequest:request
+                  completionHandler:^(NSURLResponse *response,
+                                      NSData *data,
+                                      NSError *error) {
+    NSArray *turnServers = [NSArray array];
+    if (error) {
+      completionHandler(turnServers, error);
+      return;
+    }
+    NSDictionary *dict = [NSDictionary dictionaryWithJSONData:data];
+    turnServers = [RTCICEServer serversFromCEODJSONDictionary:dict];
+    if (!turnServers) {
+      NSError *responseError =
+          [[NSError alloc] initWithDomain:kARDCEODTURNClientErrorDomain
+                                     code:kARDCEODTURNClientErrorBadResponse
+                                 userInfo:@{
+            NSLocalizedDescriptionKey: @"Bad TURN response.",
+          }];
+      completionHandler(turnServers, responseError);
+      return;
+    }
+    completionHandler(turnServers, nil);
+  }];
+}
+
+@end
diff --git a/talk/examples/objc/AppRTCDemo/ARDMessageResponse+Internal.h b/talk/examples/objc/AppRTCDemo/ARDMessageResponse+Internal.h
new file mode 100644
index 0000000..e214ff3
--- /dev/null
+++ b/talk/examples/objc/AppRTCDemo/ARDMessageResponse+Internal.h
@@ -0,0 +1,34 @@
+/*
+ * 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"
+
+@interface ARDMessageResponse ()
+
+@property(nonatomic, assign) ARDMessageResultType result;
+
+@end
diff --git a/talk/examples/objc/AppRTCDemo/ARDMessageResponse.m b/talk/examples/objc/AppRTCDemo/ARDMessageResponse.m
index c6ab1d4..496a068 100644
--- a/talk/examples/objc/AppRTCDemo/ARDMessageResponse.m
+++ b/talk/examples/objc/AppRTCDemo/ARDMessageResponse.m
@@ -25,18 +25,12 @@
  * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */
 
-#import "ARDMessageResponse.h"
+#import "ARDMessageResponse+Internal.h"
 
 #import "ARDUtilities.h"
 
 static NSString const *kARDMessageResultKey = @"result";
 
-@interface ARDMessageResponse ()
-
-@property(nonatomic, assign) ARDMessageResultType result;
-
-@end
-
 @implementation ARDMessageResponse
 
 @synthesize result = _result;
diff --git a/talk/examples/objc/AppRTCDemo/ARDRegisterResponse+Internal.h b/talk/examples/objc/AppRTCDemo/ARDRegisterResponse+Internal.h
new file mode 100644
index 0000000..eb2b0b3
--- /dev/null
+++ b/talk/examples/objc/AppRTCDemo/ARDRegisterResponse+Internal.h
@@ -0,0 +1,40 @@
+/*
+ * 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"
+
+@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
diff --git a/talk/examples/objc/AppRTCDemo/ARDRegisterResponse.m b/talk/examples/objc/AppRTCDemo/ARDRegisterResponse.m
index 76eb15c..55c467e 100644
--- a/talk/examples/objc/AppRTCDemo/ARDRegisterResponse.m
+++ b/talk/examples/objc/AppRTCDemo/ARDRegisterResponse.m
@@ -25,7 +25,7 @@
  * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */
 
-#import "ARDRegisterResponse.h"
+#import "ARDRegisterResponse+Internal.h"
 
 #import "ARDSignalingMessage.h"
 #import "ARDUtilities.h"
@@ -40,18 +40,6 @@
 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;
diff --git a/talk/examples/objc/AppRTCDemo/ARDRoomServerClient.h b/talk/examples/objc/AppRTCDemo/ARDRoomServerClient.h
new file mode 100644
index 0000000..a650187
--- /dev/null
+++ b/talk/examples/objc/AppRTCDemo/ARDRoomServerClient.h
@@ -0,0 +1,50 @@
+/*
+ * 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>
+
+@class ARDMessageResponse;
+@class ARDRegisterResponse;
+@class ARDSignalingMessage;
+
+@protocol ARDRoomServerClient <NSObject>
+
+- (void)registerForRoomId:(NSString *)roomId
+    completionHandler:(void (^)(ARDRegisterResponse *response,
+                                NSError *error))completionHandler;
+
+- (void)sendMessage:(ARDSignalingMessage *)message
+            forRoomId:(NSString *)roomId
+             clientId:(NSString *)clientId
+    completionHandler:(void (^)(ARDMessageResponse *response,
+                                NSError *error))completionHandler;
+
+- (void)deregisterForRoomId:(NSString *)roomId
+                   clientId:(NSString *)clientId
+          completionHandler:(void (^)(NSError *error))completionHandler;
+
+@end
diff --git a/talk/examples/objc/AppRTCDemo/ARDSignalingChannel.h b/talk/examples/objc/AppRTCDemo/ARDSignalingChannel.h
new file mode 100644
index 0000000..aedbb42
--- /dev/null
+++ b/talk/examples/objc/AppRTCDemo/ARDSignalingChannel.h
@@ -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 <Foundation/Foundation.h>
+
+#import "ARDSignalingMessage.h"
+
+typedef NS_ENUM(NSInteger, ARDSignalingChannelState) {
+  // State when disconnected.
+  kARDSignalingChannelStateClosed,
+  // State when connection is established but not ready for use.
+  kARDSignalingChannelStateOpen,
+  // State when connection is established and registered.
+  kARDSignalingChannelStateRegistered,
+  // State when connection encounters a fatal error.
+  kARDSignalingChannelStateError
+};
+
+@protocol ARDSignalingChannel;
+@protocol ARDSignalingChannelDelegate <NSObject>
+
+- (void)channel:(id<ARDSignalingChannel>)channel
+    didChangeState:(ARDSignalingChannelState)state;
+
+- (void)channel:(id<ARDSignalingChannel>)channel
+    didReceiveMessage:(ARDSignalingMessage *)message;
+
+@end
+
+@protocol ARDSignalingChannel <NSObject>
+
+@property(nonatomic, readonly) NSString *roomId;
+@property(nonatomic, readonly) NSString *clientId;
+@property(nonatomic, readonly) ARDSignalingChannelState state;
+@property(nonatomic, weak) id<ARDSignalingChannelDelegate> delegate;
+
+// Registers the channel for the given room and client id.
+- (void)registerForRoomId:(NSString *)roomId
+                 clientId:(NSString *)clientId;
+
+// Sends signaling message over the channel.
+- (void)sendMessage:(ARDSignalingMessage *)message;
+
+@end
+
diff --git a/talk/examples/objc/AppRTCDemo/ARDTURNClient.h b/talk/examples/objc/AppRTCDemo/ARDTURNClient.h
new file mode 100644
index 0000000..96856ea
--- /dev/null
+++ b/talk/examples/objc/AppRTCDemo/ARDTURNClient.h
@@ -0,0 +1,37 @@
+/*
+ * 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>
+
+@protocol ARDTURNClient <NSObject>
+
+// Returns TURN server urls if successful.
+- (void)requestServersWithCompletionHandler:
+    (void (^)(NSArray *turnServers,
+              NSError *error))completionHandler;
+
+@end
diff --git a/talk/examples/objc/AppRTCDemo/ARDWebSocketChannel.h b/talk/examples/objc/AppRTCDemo/ARDWebSocketChannel.h
index 06c6520..c3af1d4 100644
--- a/talk/examples/objc/AppRTCDemo/ARDWebSocketChannel.h
+++ b/talk/examples/objc/AppRTCDemo/ARDWebSocketChannel.h
@@ -27,49 +27,22 @@
 
 #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
+#import "ARDSignalingChannel.h"
 
 // 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;
+@interface ARDWebSocketChannel : NSObject <ARDSignalingChannel>
 
 - (instancetype)initWithURL:(NSURL *)url
                     restURL:(NSURL *)restURL
-                   delegate:(id<ARDWebSocketChannelDelegate>)delegate;
+                   delegate:(id<ARDSignalingChannelDelegate>)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
+// Sends message over the WebSocket connection if registered, otherwise POSTs to
 // the web socket server instead.
-- (void)sendData:(NSData *)data;
+- (void)sendMessage:(ARDSignalingMessage *)message;
 
 @end
diff --git a/talk/examples/objc/AppRTCDemo/ARDWebSocketChannel.m b/talk/examples/objc/AppRTCDemo/ARDWebSocketChannel.m
index 1707201..688f66d 100644
--- a/talk/examples/objc/AppRTCDemo/ARDWebSocketChannel.m
+++ b/talk/examples/objc/AppRTCDemo/ARDWebSocketChannel.m
@@ -50,7 +50,7 @@
 
 - (instancetype)initWithURL:(NSURL *)url
                     restURL:(NSURL *)restURL
-                   delegate:(id<ARDWebSocketChannelDelegate>)delegate {
+                   delegate:(id<ARDSignalingChannelDelegate>)delegate {
   if (self = [super init]) {
     _url = url;
     _restURL = restURL;
@@ -67,7 +67,7 @@
   [self disconnect];
 }
 
-- (void)setState:(ARDWebSocketChannelState)state {
+- (void)setState:(ARDSignalingChannelState)state {
   if (_state == state) {
     return;
   }
@@ -81,15 +81,16 @@
   NSParameterAssert(clientId.length);
   _roomId = roomId;
   _clientId = clientId;
-  if (_state == kARDWebSocketChannelStateOpen) {
+  if (_state == kARDSignalingChannelStateOpen) {
     [self registerWithCollider];
   }
 }
 
-- (void)sendData:(NSData *)data {
+- (void)sendMessage:(ARDSignalingMessage *)message {
   NSParameterAssert(_clientId.length);
   NSParameterAssert(_roomId.length);
-  if (_state == kARDWebSocketChannelStateRegistered) {
+  NSData *data = [message JSONData];
+  if (_state == kARDSignalingChannelStateRegistered) {
     NSString *payload =
         [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
     NSDictionary *message = @{
@@ -120,8 +121,8 @@
 }
 
 - (void)disconnect {
-  if (_state == kARDWebSocketChannelStateClosed ||
-      _state == kARDWebSocketChannelStateError) {
+  if (_state == kARDSignalingChannelStateClosed ||
+      _state == kARDSignalingChannelStateError) {
     return;
   }
   [_socket close];
@@ -140,7 +141,7 @@
 
 - (void)webSocketDidOpen:(SRWebSocket *)webSocket {
   NSLog(@"WebSocket connection opened.");
-  self.state = kARDWebSocketChannelStateOpen;
+  self.state = kARDSignalingChannelStateOpen;
   if (_roomId.length && _clientId.length) {
     [self registerWithCollider];
   }
@@ -171,7 +172,7 @@
 
 - (void)webSocket:(SRWebSocket *)webSocket didFailWithError:(NSError *)error {
   NSLog(@"WebSocket error: %@", error);
-  self.state = kARDWebSocketChannelStateError;
+  self.state = kARDSignalingChannelStateError;
 }
 
 - (void)webSocket:(SRWebSocket *)webSocket
@@ -180,14 +181,14 @@
             wasClean:(BOOL)wasClean {
   NSLog(@"WebSocket closed with code: %ld reason:%@ wasClean:%d",
       (long)code, reason, wasClean);
-  NSParameterAssert(_state != kARDWebSocketChannelStateError);
-  self.state = kARDWebSocketChannelStateClosed;
+  NSParameterAssert(_state != kARDSignalingChannelStateError);
+  self.state = kARDSignalingChannelStateClosed;
 }
 
 #pragma mark - Private
 
 - (void)registerWithCollider {
-  if (_state == kARDWebSocketChannelStateRegistered) {
+  if (_state == kARDSignalingChannelStateRegistered) {
     return;
   }
   NSParameterAssert(_roomId.length);
@@ -207,7 +208,7 @@
   // Registration can fail if server rejects it. For example, if the room is
   // full.
   [_socket send:messageString];
-  self.state = kARDWebSocketChannelStateRegistered;
+  self.state = kARDSignalingChannelStateRegistered;
 }
 
 @end
diff --git a/talk/examples/objc/AppRTCDemo/ios/APPRTCViewController.m b/talk/examples/objc/AppRTCDemo/ios/APPRTCViewController.m
index 3d60ee7..e6e19b0 100644
--- a/talk/examples/objc/AppRTCDemo/ios/APPRTCViewController.m
+++ b/talk/examples/objc/AppRTCDemo/ios/APPRTCViewController.m
@@ -108,6 +108,10 @@
 }
 
 - (void)appClient:(ARDAppClient *)client
+    didChangeConnectionState:(RTCICEConnectionState)state {
+}
+
+- (void)appClient:(ARDAppClient *)client
     didReceiveLocalVideoTrack:(RTCVideoTrack *)localVideoTrack {
   _localVideoTrack = localVideoTrack;
   [_localVideoTrack addRenderer:self.localVideoView];
diff --git a/talk/examples/objc/AppRTCDemo/mac/APPRTCViewController.m b/talk/examples/objc/AppRTCDemo/mac/APPRTCViewController.m
index 40d1307..41bfd35 100644
--- a/talk/examples/objc/AppRTCDemo/mac/APPRTCViewController.m
+++ b/talk/examples/objc/AppRTCDemo/mac/APPRTCViewController.m
@@ -267,6 +267,10 @@
 }
 
 - (void)appClient:(ARDAppClient *)client
+    didChangeConnectionState:(RTCICEConnectionState)state {
+}
+
+- (void)appClient:(ARDAppClient *)client
     didReceiveLocalVideoTrack:(RTCVideoTrack *)localVideoTrack {
   _localVideoTrack = localVideoTrack;
 }
diff --git a/talk/examples/objc/AppRTCDemo/tests/ARDAppClientTest.mm b/talk/examples/objc/AppRTCDemo/tests/ARDAppClientTest.mm
new file mode 100644
index 0000000..171d244
--- /dev/null
+++ b/talk/examples/objc/AppRTCDemo/tests/ARDAppClientTest.mm
@@ -0,0 +1,323 @@
+/*
+ * 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 <OCMock/OCMock.h>
+
+#import "ARDAppClient+Internal.h"
+#import "ARDRegisterResponse+Internal.h"
+#import "ARDMessageResponse+Internal.h"
+#import "RTCMediaConstraints.h"
+#import "RTCPeerConnectionFactory.h"
+
+#include "webrtc/base/gunit.h"
+#include "webrtc/base/ssladapter.h"
+
+// These classes mimic XCTest APIs, to make eventual conversion to XCTest
+// easier. Conversion will happen once XCTest is supported well on build bots.
+@interface ARDTestExpectation : NSObject
+
+@property(nonatomic, readonly) NSString *description;
+@property(nonatomic, readonly) BOOL isFulfilled;
+
+- (instancetype)initWithDescription:(NSString *)description;
+- (void)fulfill;
+
+@end
+
+@implementation ARDTestExpectation
+
+@synthesize description = _description;
+@synthesize isFulfilled = _isFulfilled;
+
+- (instancetype)initWithDescription:(NSString *)description {
+  if (self = [super init]) {
+    _description = description;
+  }
+  return self;
+}
+
+- (void)fulfill {
+  _isFulfilled = YES;
+}
+
+@end
+
+@interface ARDTestCase : NSObject
+
+- (ARDTestExpectation *)expectationWithDescription:(NSString *)description;
+- (void)waitForExpectationsWithTimeout:(NSTimeInterval)timeout
+                               handler:(void (^)(NSError *error))handler;
+
+@end
+
+@implementation ARDTestCase {
+  NSMutableArray *_expectations;
+}
+
+- (instancetype)init {
+  if (self = [super init]) {
+   _expectations = [NSMutableArray array];
+  }
+  return self;
+}
+
+- (ARDTestExpectation *)expectationWithDescription:(NSString *)description {
+  ARDTestExpectation *expectation =
+      [[ARDTestExpectation alloc] initWithDescription:description];
+  [_expectations addObject:expectation];
+  return expectation;
+}
+
+- (void)waitForExpectationsWithTimeout:(NSTimeInterval)timeout
+                               handler:(void (^)(NSError *error))handler {
+  NSDate *startDate = [NSDate date];
+  while (![self areExpectationsFulfilled]) {
+    NSTimeInterval duration = [[NSDate date] timeIntervalSinceDate:startDate];
+    if (duration > timeout) {
+      NSAssert(NO, @"Expectation timed out.");
+      break;
+    }
+    [[NSRunLoop currentRunLoop]
+        runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]];
+  }
+  handler(nil);
+}
+
+- (BOOL)areExpectationsFulfilled {
+  for (ARDTestExpectation *expectation in _expectations) {
+    if (!expectation.isFulfilled) {
+      return NO;
+    }
+  }
+  return YES;
+}
+
+@end
+
+@interface ARDAppClientTest : ARDTestCase
+@end
+
+@implementation ARDAppClientTest
+
+#pragma mark - Mock helpers
+
+- (id)mockRoomServerClientForRoomId:(NSString *)roomId
+                           clientId:(NSString *)clientId
+                        isInitiator:(BOOL)isInitiator
+                           messages:(NSArray *)messages
+                     messageHandler:
+    (void (^)(ARDSignalingMessage *))messageHandler {
+  id mockRoomServerClient =
+      [OCMockObject mockForProtocol:@protocol(ARDRoomServerClient)];
+
+  // Successful register response.
+  ARDRegisterResponse *registerResponse = [[ARDRegisterResponse alloc] init];
+  registerResponse.result = kARDRegisterResultTypeSuccess;
+  registerResponse.roomId = roomId;
+  registerResponse.clientId = clientId;
+  registerResponse.isInitiator = isInitiator;
+  registerResponse.messages = messages;
+
+  // Successful message response.
+  ARDMessageResponse *messageResponse = [[ARDMessageResponse alloc] init];
+  messageResponse.result = kARDMessageResultTypeSuccess;
+
+  // Return register response from above on register.
+  [[[mockRoomServerClient stub] andDo:^(NSInvocation *invocation) {
+    __unsafe_unretained void (^completionHandler)(ARDRegisterResponse *response,
+                                                  NSError *error);
+    [invocation getArgument:&completionHandler atIndex:3];
+    completionHandler(registerResponse, nil);
+  }] registerForRoomId:roomId completionHandler:[OCMArg any]];
+
+  // Return message response from above on register.
+  [[[mockRoomServerClient stub] andDo:^(NSInvocation *invocation) {
+    __unsafe_unretained ARDSignalingMessage *message;
+    __unsafe_unretained void (^completionHandler)(ARDMessageResponse *response,
+                                                  NSError *error);
+    [invocation getArgument:&message atIndex:2];
+    [invocation getArgument:&completionHandler atIndex:5];
+    messageHandler(message);
+    completionHandler(messageResponse, nil);
+  }] sendMessage:[OCMArg any]
+            forRoomId:roomId
+             clientId:clientId
+    completionHandler:[OCMArg any]];
+
+  // Do nothing on deregister.
+  [[[mockRoomServerClient stub] andDo:^(NSInvocation *invocation) {
+    __unsafe_unretained void (^completionHandler)(NSError *error);
+    [invocation getArgument:&completionHandler atIndex:4];
+    if (completionHandler) {
+      completionHandler(nil);
+    }
+  }] deregisterForRoomId:roomId
+                clientId:clientId
+       completionHandler:[OCMArg any]];
+
+  return mockRoomServerClient;
+}
+
+- (id)mockSignalingChannelForRoomId:(NSString *)roomId
+                           clientId:(NSString *)clientId
+                     messageHandler:
+    (void (^)(ARDSignalingMessage *message))messageHandler {
+  id mockSignalingChannel =
+      [OCMockObject niceMockForProtocol:@protocol(ARDSignalingChannel)];
+  [[mockSignalingChannel stub] registerForRoomId:roomId clientId:clientId];
+  [[[mockSignalingChannel stub] andDo:^(NSInvocation *invocation) {
+    __unsafe_unretained ARDSignalingMessage *message;
+    [invocation getArgument:&message atIndex:2];
+    messageHandler(message);
+  }] sendMessage:[OCMArg any]];
+  return mockSignalingChannel;
+}
+
+- (id)mockTURNClient {
+  id mockTURNClient =
+      [OCMockObject mockForProtocol:@protocol(ARDTURNClient)];
+  [[[mockTURNClient stub] andDo:^(NSInvocation *invocation) {
+    // Don't return anything in TURN response.
+    __unsafe_unretained void (^completionHandler)(NSArray *turnServers,
+                                                  NSError *error);
+    [invocation getArgument:&completionHandler atIndex:2];
+    completionHandler([NSArray array], nil);
+  }] requestServersWithCompletionHandler:[OCMArg any]];
+  return mockTURNClient;
+}
+
+- (ARDAppClient *)createAppClientForRoomId:(NSString *)roomId
+                                  clientId:(NSString *)clientId
+                               isInitiator:(BOOL)isInitiator
+                                  messages:(NSArray *)messages
+                            messageHandler:
+    (void (^)(ARDSignalingMessage *message))messageHandler
+                          connectedHandler:(void (^)(void))connectedHandler {
+  id turnClient = [self mockTURNClient];
+  id signalingChannel = [self mockSignalingChannelForRoomId:roomId
+                                                   clientId:clientId
+                                             messageHandler:messageHandler];
+  id roomServerClient =
+      [self mockRoomServerClientForRoomId:roomId
+                                 clientId:clientId
+                              isInitiator:isInitiator
+                                 messages:messages
+                           messageHandler:messageHandler];
+  id delegate =
+      [OCMockObject niceMockForProtocol:@protocol(ARDAppClientDelegate)];
+  [[[delegate stub] andDo:^(NSInvocation *invocation) {
+    connectedHandler();
+  }] appClient:[OCMArg any] didChangeConnectionState:RTCICEConnectionConnected];
+
+  return [[ARDAppClient alloc] initWithRoomServerClient:roomServerClient
+                                       signalingChannel:signalingChannel
+                                             turnClient:turnClient
+                                               delegate:delegate];
+}
+
+// Tests that an ICE connection is established between two ARDAppClient objects
+// where one is set up as a caller and the other the answerer. Network
+// components are mocked out and messages are relayed directly from object to
+// object. It's expected that both clients reach the RTCICEConnectionConnected
+// state within a reasonable amount of time.
+- (void)testSession {
+  // Need block arguments here because we're setting up a callbacks before we
+  // create the clients.
+  ARDAppClient *caller = nil;
+  ARDAppClient *answerer = nil;
+  __block __weak ARDAppClient *weakCaller = nil;
+  __block __weak ARDAppClient *weakAnswerer = nil;
+  NSString *roomId = @"testRoom";
+  NSString *callerId = @"testCallerId";
+  NSString *answererId = @"testAnswererId";
+
+  ARDTestExpectation *callerConnectionExpectation =
+      [self expectationWithDescription:@"Caller PC connected."];
+  ARDTestExpectation *answererConnectionExpectation =
+      [self expectationWithDescription:@"Answerer PC connected."];
+
+  caller = [self createAppClientForRoomId:roomId
+                                 clientId:callerId
+                              isInitiator:YES
+                                 messages:[NSArray array]
+                           messageHandler:^(ARDSignalingMessage *message) {
+    ARDAppClient *strongAnswerer = weakAnswerer;
+    [strongAnswerer channel:strongAnswerer.channel didReceiveMessage:message];
+  } connectedHandler:^{
+    [callerConnectionExpectation fulfill];
+  }];
+  // TODO(tkchin): Figure out why DTLS-SRTP constraint causes thread assertion
+  // crash in Debug.
+  caller.defaultPeerConnectionConstraints = [[RTCMediaConstraints alloc] init];
+  weakCaller = caller;
+
+  answerer = [self createAppClientForRoomId:roomId
+                                   clientId:answererId
+                                isInitiator:NO
+                                   messages:[NSArray array]
+                             messageHandler:^(ARDSignalingMessage *message) {
+    ARDAppClient *strongCaller = weakCaller;
+    [strongCaller channel:strongCaller.channel didReceiveMessage:message];
+  } connectedHandler:^{
+    [answererConnectionExpectation fulfill];
+  }];
+  // TODO(tkchin): Figure out why DTLS-SRTP constraint causes thread assertion
+  // crash in Debug.
+  answerer.defaultPeerConnectionConstraints =
+      [[RTCMediaConstraints alloc] init];
+  weakAnswerer = answerer;
+
+  // Kick off connection.
+  [caller connectToRoomWithId:roomId options:nil];
+  [answerer connectToRoomWithId:roomId options:nil];
+  [self waitForExpectationsWithTimeout:20 handler:^(NSError *error) {
+    if (error) {
+      NSLog(@"Expectations error: %@", error);
+    }
+  }];
+}
+
+@end
+
+class SignalingTest : public ::testing::Test {
+ protected:
+  static void SetUpTestCase() {
+    rtc::InitializeSSL();
+  }
+  static void TearDownTestCase() {
+    rtc::CleanupSSL();
+  }
+};
+
+TEST_F(SignalingTest, SessionTest) {
+  @autoreleasepool {
+    ARDAppClientTest *test = [[ARDAppClientTest alloc] init];
+    [test testSession];
+  }
+}
diff --git a/talk/libjingle.gyp b/talk/libjingle.gyp
index 56d29af..29a23bd 100755
--- a/talk/libjingle.gyp
+++ b/talk/libjingle.gyp
@@ -278,6 +278,11 @@
               '-lstdc++',
             ],
           },
+          'all_dependent_settings': {
+            'xcode_settings': {
+              'CLANG_ENABLE_OBJC_ARC': 'YES',
+            },
+          },
           'xcode_settings': {
             'CLANG_ENABLE_OBJC_ARC': 'YES',
             # common.gypi enables this for mac but we want this to be disabled
diff --git a/talk/libjingle_examples.gyp b/talk/libjingle_examples.gyp
index aba386b..a94610f 100755
--- a/talk/libjingle_examples.gyp
+++ b/talk/libjingle_examples.gyp
@@ -148,14 +148,70 @@
 
     ['OS=="ios" or (OS=="mac" and target_arch!="ia32" and mac_sdk>="10.8")', {
       'targets': [
+        { 'target_name': 'apprtc_signaling',
+          'type': 'static_library',
+          'dependencies': [
+            'libjingle.gyp:libjingle_peerconnection_objc',
+            'socketrocket',
+          ],
+          'sources': [
+            'examples/objc/AppRTCDemo/ARDAppClient.h',
+            'examples/objc/AppRTCDemo/ARDAppClient.m',
+            'examples/objc/AppRTCDemo/ARDAppClient+Internal.h',
+            'examples/objc/AppRTCDemo/ARDAppEngineClient.h',
+            'examples/objc/AppRTCDemo/ARDAppEngineClient.m',
+            'examples/objc/AppRTCDemo/ARDCEODTURNClient.h',
+            'examples/objc/AppRTCDemo/ARDCEODTURNClient.m',
+            'examples/objc/AppRTCDemo/ARDMessageResponse.h',
+            'examples/objc/AppRTCDemo/ARDMessageResponse.m',
+            'examples/objc/AppRTCDemo/ARDMessageResponse+Internal.h',
+            'examples/objc/AppRTCDemo/ARDRegisterResponse.h',
+            'examples/objc/AppRTCDemo/ARDRegisterResponse.m',
+            'examples/objc/AppRTCDemo/ARDRegisterResponse+Internal.h',
+            'examples/objc/AppRTCDemo/ARDRoomServerClient.h',
+            'examples/objc/AppRTCDemo/ARDSignalingChannel.h',
+            'examples/objc/AppRTCDemo/ARDSignalingMessage.h',
+            'examples/objc/AppRTCDemo/ARDSignalingMessage.m',
+            'examples/objc/AppRTCDemo/ARDTURNClient.h',
+            'examples/objc/AppRTCDemo/ARDUtilities.h',
+            'examples/objc/AppRTCDemo/ARDUtilities.m',
+            'examples/objc/AppRTCDemo/ARDWebSocketChannel.h',
+            'examples/objc/AppRTCDemo/ARDWebSocketChannel.m',
+            'examples/objc/AppRTCDemo/RTCICECandidate+JSON.h',
+            'examples/objc/AppRTCDemo/RTCICECandidate+JSON.m',
+            'examples/objc/AppRTCDemo/RTCICEServer+JSON.h',
+            'examples/objc/AppRTCDemo/RTCICEServer+JSON.m',
+            'examples/objc/AppRTCDemo/RTCMediaConstraints+JSON.h',
+            'examples/objc/AppRTCDemo/RTCMediaConstraints+JSON.m',
+            'examples/objc/AppRTCDemo/RTCSessionDescription+JSON.h',
+            'examples/objc/AppRTCDemo/RTCSessionDescription+JSON.m',
+          ],
+          'include_dirs': [
+            'examples/objc/APPRTCDemo',
+          ],
+          'direct_dependent_settings': {
+            'include_dirs': [
+              'examples/objc/APPRTCDemo',
+            ],
+          },
+          'export_dependent_settings': [
+            'libjingle.gyp:libjingle_peerconnection_objc',
+          ],
+          'conditions': [
+            ['OS=="mac"', {
+              'xcode_settings': {
+                'MACOSX_DEPLOYMENT_TARGET' : '10.8',
+              },
+            }],
+          ],
+        },
         {
           'target_name': 'AppRTCDemo',
           'type': 'executable',
           'product_name': 'AppRTCDemo',
           'mac_bundle': 1,
           'dependencies': [
-            'libjingle.gyp:libjingle_peerconnection_objc',
-            'socketrocket',
+            'apprtc_signaling',
           ],
           'conditions': [
             ['OS=="ios"', {
@@ -199,34 +255,6 @@
               ],
             }],
           ],
-          'include_dirs': [
-            'examples/objc/APPRTCDemo',
-          ],
-          'sources': [
-            'examples/objc/AppRTCDemo/ARDAppClient.h',
-            'examples/objc/AppRTCDemo/ARDAppClient.m',
-            'examples/objc/AppRTCDemo/ARDMessageResponse.h',
-            'examples/objc/AppRTCDemo/ARDMessageResponse.m',
-            'examples/objc/AppRTCDemo/ARDRegisterResponse.h',
-            'examples/objc/AppRTCDemo/ARDRegisterResponse.m',
-            'examples/objc/AppRTCDemo/ARDSignalingMessage.h',
-            'examples/objc/AppRTCDemo/ARDSignalingMessage.m',
-            'examples/objc/AppRTCDemo/ARDUtilities.h',
-            'examples/objc/AppRTCDemo/ARDUtilities.m',
-            'examples/objc/AppRTCDemo/ARDWebSocketChannel.h',
-            'examples/objc/AppRTCDemo/ARDWebSocketChannel.m',
-            'examples/objc/AppRTCDemo/RTCICECandidate+JSON.h',
-            'examples/objc/AppRTCDemo/RTCICECandidate+JSON.m',
-            'examples/objc/AppRTCDemo/RTCICEServer+JSON.h',
-            'examples/objc/AppRTCDemo/RTCICEServer+JSON.m',
-            'examples/objc/AppRTCDemo/RTCMediaConstraints+JSON.h',
-            'examples/objc/AppRTCDemo/RTCMediaConstraints+JSON.m',
-            'examples/objc/AppRTCDemo/RTCSessionDescription+JSON.h',
-            'examples/objc/AppRTCDemo/RTCSessionDescription+JSON.m',
-          ],
-          'xcode_settings': {
-            'CLANG_ENABLE_OBJC_ARC': 'YES',
-          },
         },  # target AppRTCDemo
         {
           # TODO(tkchin): move this into the real third party location and
diff --git a/talk/libjingle_tests.gyp b/talk/libjingle_tests.gyp
index c8d78c2..ff120f3 100755
--- a/talk/libjingle_tests.gyp
+++ b/talk/libjingle_tests.gyp
@@ -72,7 +72,7 @@
     {
       'target_name': 'libjingle_unittest',
       'type': 'executable',
-      'includes': [ 'build/ios_tests.gypi', ],
+      'includes': [ 'build/objc_app.gypi', ],
       'dependencies': [
         '<(webrtc_root)/base/base.gyp:rtc_base',
         '<(webrtc_root)/base/base_tests.gyp:rtc_base_tests_utils',
@@ -341,7 +341,7 @@
         {
           'target_name': 'libjingle_peerconnection_objc_test',
           'type': 'executable',
-          'includes': [ 'build/ios_tests.gypi', ],
+          'includes': [ 'build/objc_app.gypi' ],
           'dependencies': [
             '<(webrtc_root)/base/base_tests.gyp:rtc_base_tests_utils',
             'libjingle.gyp:libjingle_peerconnection_objc',
@@ -356,46 +356,40 @@
             # needs a GUI driver.
             'app/webrtc/objctests/mac/main.mm',
           ],
-          'FRAMEWORK_SEARCH_PATHS': [
-            '$(inherited)',
-            '$(SDKROOT)/Developer/Library/Frameworks',
-            '$(DEVELOPER_LIBRARY_DIR)/Frameworks',
-          ],
-
-          # TODO(fischman): there is duplication here with
-          # build/ios_tests.gypi, because for historical reasons the
-          # mac x64 bots expect this unittest to be in a bundle
-          # directory (.app).  Once the bots don't expect this
-          # anymore, remove this duplication.
-          'variables': {
-            'infoplist_file': 'build/ios_test.plist',
-          },
-          'mac_bundle': 1,
-          'mac_bundle_resources': [
-            '<(infoplist_file)',
-          ],
-          # The plist is listed above so that it appears in XCode's file list,
-          # but we don't actually want to bundle it.
-          'mac_bundle_resources!': [
-            '<(infoplist_file)',
-          ],
-          'xcode_settings': {
-            'CLANG_ENABLE_OBJC_ARC': 'YES',
-            # common.gypi enables this for mac but we want this to be disabled
-            # like it is for ios.
-            'CLANG_WARN_OBJC_MISSING_PROPERTY_SYNTHESIS': 'NO',
-            'INFOPLIST_FILE': '<(infoplist_file)',
-          },
           'conditions': [
             ['OS=="mac"', {
               'xcode_settings': {
                 # Need to build against 10.7 framework for full ARC support
                 # on OSX.
                 'MACOSX_DEPLOYMENT_TARGET' : '10.7',
+                # common.gypi enables this for mac but we want this to be
+                # disabled like it is for ios.
+                'CLANG_WARN_OBJC_MISSING_PROPERTY_SYNTHESIS': 'NO',
               },
             }],
           ],
         },  # target libjingle_peerconnection_objc_test
+        {
+          'target_name': 'apprtc_signaling_gunit_test',
+          'type': 'executable',
+          'includes': [ 'build/objc_app.gypi' ],
+          'dependencies': [
+            '<(webrtc_root)/base/base_tests.gyp:rtc_base_tests_utils',
+            '<(DEPTH)/third_party/ocmock/ocmock.gyp:ocmock',
+            'libjingle_examples.gyp:apprtc_signaling',
+          ],
+          'sources': [
+            'app/webrtc/objctests/mac/main.mm',
+            'examples/objc/AppRTCDemo/tests/ARDAppClientTest.mm',
+          ],
+          'conditions': [
+            ['OS=="mac"', {
+              'xcode_settings': {
+                'MACOSX_DEPLOYMENT_TARGET' : '10.8',
+              },
+            }],
+          ],
+        },  # target apprtc_signaling_gunit_test
       ],
     }],
     ['test_isolation_mode != "noop"', {