Reland of name AppRTCDemo on Android and iOS to AppRTCMobile (patchset #1 id:1 of https://codereview.webrtc.org/2358133003/ )

Reason for revert:
Internal project is updated.

Original issue's description:
> Revert of Rename AppRTCDemo on Android and iOS to AppRTCMobile (patchset #2 id:20001 of https://codereview.webrtc.org/2343403002/ )
>
> Reason for revert:
> Breaks internal project.
>
> Original issue's description:
> > Rename AppRTCDemo on Android and iOS to AppRTCMobile
> >
> > The purpose is to make it clearer it is a mobile application.
> >
> > BUG=webrtc:6359
> > NOPRESUBMIT=true
> >
> > Committed: https://crrev.com/d3af58bdab5b25acd62cd816363becc7003d3e5a
> > Cr-Commit-Position: refs/heads/master@{#14356}
>
> TBR=sakal@webrtc.org,kthelgason@webrtc.org,tommi@webrtc.org
> # Skipping CQ checks because original CL landed less than 1 days ago.
> NOPRESUBMIT=true
> NOTREECHECKS=true
> NOTRY=true
> BUG=webrtc:6359
>
> Committed: https://crrev.com/87ef6f750126f9f17f4714d696a8e77a2dd0a3f1
> Cr-Commit-Position: refs/heads/master@{#14358}

TBR=sakal@webrtc.org,kthelgason@webrtc.org,tommi@webrtc.org
# Not skipping CQ checks because original CL landed more than 1 days ago.
BUG=webrtc:6359

Review URL: https://codereview.webrtc.org/2373443005 .

Cr-Commit-Position: refs/heads/master@{#14391}
diff --git a/webrtc/examples/objc/AppRTCMobile/ARDAppClient+Internal.h b/webrtc/examples/objc/AppRTCMobile/ARDAppClient+Internal.h
new file mode 100644
index 0000000..a21ac72
--- /dev/null
+++ b/webrtc/examples/objc/AppRTCMobile/ARDAppClient+Internal.h
@@ -0,0 +1,57 @@
+/*
+ *  Copyright 2014 The WebRTC Project Authors. All rights reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#import "ARDAppClient.h"
+
+#import "WebRTC/RTCPeerConnection.h"
+
+#import "ARDRoomServerClient.h"
+#import "ARDSignalingChannel.h"
+#import "ARDTURNClient.h"
+
+@class RTCPeerConnectionFactory;
+
+@interface ARDAppClient () <ARDSignalingChannelDelegate,
+  RTCPeerConnectionDelegate>
+
+// All properties should only be mutated from the main queue.
+@property(nonatomic, strong) id<ARDRoomServerClient> roomServerClient;
+@property(nonatomic, strong) id<ARDSignalingChannel> channel;
+@property(nonatomic, strong) id<ARDSignalingChannel> loopbackChannel;
+@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 hasJoinedRoomServerRoom;
+
+@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, readonly) BOOL isLoopback;
+@property(nonatomic, readonly) BOOL isAudioOnly;
+@property(nonatomic, readonly) BOOL shouldMakeAecDump;
+@property(nonatomic, readonly) BOOL shouldUseLevelControl;
+
+@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/webrtc/examples/objc/AppRTCMobile/ARDAppClient.h b/webrtc/examples/objc/AppRTCMobile/ARDAppClient.h
new file mode 100644
index 0000000..2186abe
--- /dev/null
+++ b/webrtc/examples/objc/AppRTCMobile/ARDAppClient.h
@@ -0,0 +1,79 @@
+/*
+ *  Copyright 2014 The WebRTC Project Authors. All rights reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "WebRTC/RTCPeerConnection.h"
+#import "WebRTC/RTCVideoTrack.h"
+
+typedef NS_ENUM(NSInteger, ARDAppClientState) {
+  // Disconnected from servers.
+  kARDAppClientStateDisconnected,
+  // Connecting to servers.
+  kARDAppClientStateConnecting,
+  // Connected to servers.
+  kARDAppClientStateConnected,
+};
+
+@class ARDAppClient;
+// The delegate is informed of pertinent events and will be called on the
+// main queue.
+@protocol ARDAppClientDelegate <NSObject>
+
+- (void)appClient:(ARDAppClient *)client
+    didChangeState:(ARDAppClientState)state;
+
+- (void)appClient:(ARDAppClient *)client
+    didChangeConnectionState:(RTCIceConnectionState)state;
+
+- (void)appClient:(ARDAppClient *)client
+    didReceiveLocalVideoTrack:(RTCVideoTrack *)localVideoTrack;
+
+- (void)appClient:(ARDAppClient *)client
+    didReceiveRemoteVideoTrack:(RTCVideoTrack *)remoteVideoTrack;
+
+- (void)appClient:(ARDAppClient *)client
+         didError:(NSError *)error;
+
+- (void)appClient:(ARDAppClient *)client
+      didGetStats:(NSArray *)stats;
+
+@end
+
+// Handles connections to the AppRTC server for a given room. Methods on this
+// class should only be called from the main queue.
+@interface ARDAppClient : NSObject
+
+// If |shouldGetStats| is true, stats will be reported in 1s intervals through
+// the delegate.
+@property(nonatomic, assign) BOOL shouldGetStats;
+@property(nonatomic, readonly) ARDAppClientState state;
+@property(nonatomic, weak) id<ARDAppClientDelegate> delegate;
+
+// Convenience constructor since all expected use cases will need a delegate
+// in order to receive remote tracks.
+- (instancetype)initWithDelegate:(id<ARDAppClientDelegate>)delegate;
+
+// Establishes a connection with the AppRTC servers for the given room id.
+// If |isLoopback| is true, the call will connect to itself.
+// If |isAudioOnly| is true, video will be disabled for the call.
+// If |shouldMakeAecDump| is true, an aecdump will be created for the call.
+// If |shouldUseLevelControl| is true, the level controller will be used
+// in the call.
+- (void)connectToRoomWithId:(NSString *)roomId
+                 isLoopback:(BOOL)isLoopback
+                isAudioOnly:(BOOL)isAudioOnly
+          shouldMakeAecDump:(BOOL)shouldMakeAecDump
+      shouldUseLevelControl:(BOOL)shouldUseLevelControl;
+
+// Disconnects from the AppRTC servers and any connected clients.
+- (void)disconnect;
+
+@end
diff --git a/webrtc/examples/objc/AppRTCMobile/ARDAppClient.m b/webrtc/examples/objc/AppRTCMobile/ARDAppClient.m
new file mode 100644
index 0000000..1020621
--- /dev/null
+++ b/webrtc/examples/objc/AppRTCMobile/ARDAppClient.m
@@ -0,0 +1,846 @@
+/*
+ *  Copyright 2014 The WebRTC Project Authors. All rights reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#import "ARDAppClient+Internal.h"
+
+#import "WebRTC/RTCAVFoundationVideoSource.h"
+#import "WebRTC/RTCAudioTrack.h"
+#import "WebRTC/RTCConfiguration.h"
+#import "WebRTC/RTCFileLogger.h"
+#import "WebRTC/RTCIceServer.h"
+#import "WebRTC/RTCLogging.h"
+#import "WebRTC/RTCMediaConstraints.h"
+#import "WebRTC/RTCMediaStream.h"
+#import "WebRTC/RTCPeerConnectionFactory.h"
+#import "WebRTC/RTCRtpSender.h"
+#import "WebRTC/RTCTracing.h"
+
+#import "ARDAppEngineClient.h"
+#import "ARDCEODTURNClient.h"
+#import "ARDJoinResponse.h"
+#import "ARDMessageResponse.h"
+#import "ARDSDPUtils.h"
+#import "ARDSignalingMessage.h"
+#import "ARDUtilities.h"
+#import "ARDWebSocketChannel.h"
+#import "RTCIceCandidate+JSON.h"
+#import "RTCSessionDescription+JSON.h"
+
+static NSString * const kARDDefaultSTUNServerUrl =
+    @"stun:stun.l.google.com:19302";
+// TODO(tkchin): figure out a better username for CEOD statistics.
+static NSString * const kARDTurnRequestUrl =
+    @"https://computeengineondemand.appspot.com"
+    @"/turn?username=iapprtc&key=4080218913";
+
+static NSString * const kARDAppClientErrorDomain = @"ARDAppClient";
+static NSInteger const kARDAppClientErrorUnknown = -1;
+static NSInteger const kARDAppClientErrorRoomFull = -2;
+static NSInteger const kARDAppClientErrorCreateSDP = -3;
+static NSInteger const kARDAppClientErrorSetSDP = -4;
+static NSInteger const kARDAppClientErrorInvalidClient = -5;
+static NSInteger const kARDAppClientErrorInvalidRoom = -6;
+static NSString * const kARDMediaStreamId = @"ARDAMS";
+static NSString * const kARDAudioTrackId = @"ARDAMSa0";
+static NSString * const kARDVideoTrackId = @"ARDAMSv0";
+
+// TODO(tkchin): Add these as UI options.
+static BOOL const kARDAppClientEnableTracing = NO;
+static BOOL const kARDAppClientEnableRtcEventLog = YES;
+static int64_t const kARDAppClientAecDumpMaxSizeInBytes = 5e6;  // 5 MB.
+static int64_t const kARDAppClientRtcEventLogMaxSizeInBytes = 5e6;  // 5 MB.
+
+// We need a proxy to NSTimer because it causes a strong retain cycle. When
+// using the proxy, |invalidate| must be called before it properly deallocs.
+@interface ARDTimerProxy : NSObject
+
+- (instancetype)initWithInterval:(NSTimeInterval)interval
+                         repeats:(BOOL)repeats
+                    timerHandler:(void (^)(void))timerHandler;
+- (void)invalidate;
+
+@end
+
+@implementation ARDTimerProxy {
+  NSTimer *_timer;
+  void (^_timerHandler)(void);
+}
+
+- (instancetype)initWithInterval:(NSTimeInterval)interval
+                         repeats:(BOOL)repeats
+                    timerHandler:(void (^)(void))timerHandler {
+  NSParameterAssert(timerHandler);
+  if (self = [super init]) {
+    _timerHandler = timerHandler;
+    _timer = [NSTimer scheduledTimerWithTimeInterval:interval
+                                              target:self
+                                            selector:@selector(timerDidFire:)
+                                            userInfo:nil
+                                             repeats:repeats];
+  }
+  return self;
+}
+
+- (void)invalidate {
+  [_timer invalidate];
+}
+
+- (void)timerDidFire:(NSTimer *)timer {
+  _timerHandler();
+}
+
+@end
+
+@implementation ARDAppClient {
+  RTCFileLogger *_fileLogger;
+  ARDTimerProxy *_statsTimer;
+}
+
+@synthesize shouldGetStats = _shouldGetStats;
+@synthesize state = _state;
+@synthesize delegate = _delegate;
+@synthesize roomServerClient = _roomServerClient;
+@synthesize channel = _channel;
+@synthesize loopbackChannel = _loopbackChannel;
+@synthesize turnClient = _turnClient;
+@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;
+@synthesize defaultPeerConnectionConstraints =
+    _defaultPeerConnectionConstraints;
+@synthesize isLoopback = _isLoopback;
+@synthesize isAudioOnly = _isAudioOnly;
+@synthesize shouldMakeAecDump = _shouldMakeAecDump;
+@synthesize shouldUseLevelControl = _shouldUseLevelControl;
+
+- (instancetype)init {
+  if (self = [super init]) {
+    _roomServerClient = [[ARDAppEngineClient alloc] init];
+    NSURL *turnRequestURL = [NSURL URLWithString:kARDTurnRequestUrl];
+    _turnClient = [[ARDCEODTURNClient alloc] initWithURL:turnRequestURL];
+    [self configure];
+  }
+  return self;
+}
+
+- (instancetype)initWithDelegate:(id<ARDAppClientDelegate>)delegate {
+  if (self = [super init]) {
+    _roomServerClient = [[ARDAppEngineClient alloc] init];
+    _delegate = delegate;
+    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]];
+  _fileLogger = [[RTCFileLogger alloc] init];
+  [_fileLogger start];
+}
+
+- (void)dealloc {
+  self.shouldGetStats = NO;
+  [self disconnect];
+}
+
+- (void)setShouldGetStats:(BOOL)shouldGetStats {
+  if (_shouldGetStats == shouldGetStats) {
+    return;
+  }
+  if (shouldGetStats) {
+    __weak ARDAppClient *weakSelf = self;
+    _statsTimer = [[ARDTimerProxy alloc] initWithInterval:1
+                                                  repeats:YES
+                                             timerHandler:^{
+      ARDAppClient *strongSelf = weakSelf;
+      [strongSelf.peerConnection statsForTrack:nil
+                              statsOutputLevel:RTCStatsOutputLevelDebug
+                             completionHandler:^(NSArray *stats) {
+        dispatch_async(dispatch_get_main_queue(), ^{
+          ARDAppClient *strongSelf = weakSelf;
+          [strongSelf.delegate appClient:strongSelf didGetStats:stats];
+        });
+      }];
+    }];
+  } else {
+    [_statsTimer invalidate];
+    _statsTimer = nil;
+  }
+  _shouldGetStats = shouldGetStats;
+}
+
+- (void)setState:(ARDAppClientState)state {
+  if (_state == state) {
+    return;
+  }
+  _state = state;
+  [_delegate appClient:self didChangeState:_state];
+}
+
+- (void)connectToRoomWithId:(NSString *)roomId
+                 isLoopback:(BOOL)isLoopback
+                isAudioOnly:(BOOL)isAudioOnly
+          shouldMakeAecDump:(BOOL)shouldMakeAecDump
+      shouldUseLevelControl:(BOOL)shouldUseLevelControl {
+  NSParameterAssert(roomId.length);
+  NSParameterAssert(_state == kARDAppClientStateDisconnected);
+  _isLoopback = isLoopback;
+  _isAudioOnly = isAudioOnly;
+  _shouldMakeAecDump = shouldMakeAecDump;
+  _shouldUseLevelControl = shouldUseLevelControl;
+  self.state = kARDAppClientStateConnecting;
+
+#if defined(WEBRTC_IOS)
+  if (kARDAppClientEnableTracing) {
+    NSString *filePath = [self documentsFilePathForFileName:@"webrtc-trace.txt"];
+    RTCStartInternalCapture(filePath);
+  }
+#endif
+
+  // Request TURN.
+  __weak ARDAppClient *weakSelf = self;
+  [_turnClient requestServersWithCompletionHandler:^(NSArray *turnServers,
+                                                     NSError *error) {
+    if (error) {
+      RTCLogError("Error retrieving TURN servers: %@",
+                  error.localizedDescription);
+    }
+    ARDAppClient *strongSelf = weakSelf;
+    [strongSelf.iceServers addObjectsFromArray:turnServers];
+    strongSelf.isTurnComplete = YES;
+    [strongSelf startSignalingIfReady];
+  }];
+
+  // Join room on room server.
+  [_roomServerClient joinRoomWithRoomId:roomId
+                             isLoopback:isLoopback
+      completionHandler:^(ARDJoinResponse *response, NSError *error) {
+    ARDAppClient *strongSelf = weakSelf;
+    if (error) {
+      [strongSelf.delegate appClient:strongSelf didError:error];
+      return;
+    }
+    NSError *joinError =
+        [[strongSelf class] errorForJoinResultType:response.result];
+    if (joinError) {
+      RTCLogError(@"Failed to join room:%@ on room server.", roomId);
+      [strongSelf disconnect];
+      [strongSelf.delegate appClient:strongSelf didError:joinError];
+      return;
+    }
+    RTCLog(@"Joined room:%@ on room server.", roomId);
+    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.hasJoinedRoomServerRoom) {
+    [_roomServerClient leaveRoomWithRoomId:_roomId
+                                  clientId:_clientId
+                         completionHandler:nil];
+  }
+  if (_channel) {
+    if (_channel.state == kARDSignalingChannelStateRegistered) {
+      // Tell the other client we're hanging up.
+      ARDByeMessage *byeMessage = [[ARDByeMessage alloc] init];
+      [_channel sendMessage:byeMessage];
+    }
+    // Disconnect from collider.
+    _channel = nil;
+  }
+  _clientId = nil;
+  _roomId = nil;
+  _isInitiator = NO;
+  _hasReceivedSdp = NO;
+  _messageQueue = [NSMutableArray array];
+#if defined(WEBRTC_IOS)
+  [_factory stopAecDump];
+  [_peerConnection stopRtcEventLog];
+#endif
+  _peerConnection = nil;
+  self.state = kARDAppClientStateDisconnected;
+#if defined(WEBRTC_IOS)
+  RTCStopInternalCapture();
+#endif
+}
+
+#pragma mark - ARDSignalingChannelDelegate
+
+- (void)channel:(id<ARDSignalingChannel>)channel
+    didReceiveMessage:(ARDSignalingMessage *)message {
+  switch (message.type) {
+    case kARDSignalingMessageTypeOffer:
+    case kARDSignalingMessageTypeAnswer:
+      // Offers and answers must be processed before any other message, so we
+      // place them at the front of the queue.
+      _hasReceivedSdp = YES;
+      [_messageQueue insertObject:message atIndex:0];
+      break;
+    case kARDSignalingMessageTypeCandidate:
+    case kARDSignalingMessageTypeCandidateRemoval:
+      [_messageQueue addObject:message];
+      break;
+    case kARDSignalingMessageTypeBye:
+      // Disconnects can be processed immediately.
+      [self processSignalingMessage:message];
+      return;
+  }
+  [self drainMessageQueueIfReady];
+}
+
+- (void)channel:(id<ARDSignalingChannel>)channel
+    didChangeState:(ARDSignalingChannelState)state {
+  switch (state) {
+    case kARDSignalingChannelStateOpen:
+      break;
+    case kARDSignalingChannelStateRegistered:
+      break;
+    case kARDSignalingChannelStateClosed:
+    case kARDSignalingChannelStateError:
+      // TODO(tkchin): reconnection scenarios. Right now we just disconnect
+      // completely if the websocket connection fails.
+      [self disconnect];
+      break;
+  }
+}
+
+#pragma mark - RTCPeerConnectionDelegate
+// Callbacks for this delegate occur on non-main thread and need to be
+// dispatched back to main queue as needed.
+
+- (void)peerConnection:(RTCPeerConnection *)peerConnection
+    didChangeSignalingState:(RTCSignalingState)stateChanged {
+  RTCLog(@"Signaling state changed: %ld", (long)stateChanged);
+}
+
+- (void)peerConnection:(RTCPeerConnection *)peerConnection
+          didAddStream:(RTCMediaStream *)stream {
+  dispatch_async(dispatch_get_main_queue(), ^{
+    RTCLog(@"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
+       didRemoveStream:(RTCMediaStream *)stream {
+  RTCLog(@"Stream was removed.");
+}
+
+- (void)peerConnectionShouldNegotiate:(RTCPeerConnection *)peerConnection {
+  RTCLog(@"WARNING: Renegotiation needed but unimplemented.");
+}
+
+- (void)peerConnection:(RTCPeerConnection *)peerConnection
+    didChangeIceConnectionState:(RTCIceConnectionState)newState {
+  RTCLog(@"ICE state changed: %ld", (long)newState);
+  dispatch_async(dispatch_get_main_queue(), ^{
+    [_delegate appClient:self didChangeConnectionState:newState];
+  });
+}
+
+- (void)peerConnection:(RTCPeerConnection *)peerConnection
+    didChangeIceGatheringState:(RTCIceGatheringState)newState {
+  RTCLog(@"ICE gathering state changed: %ld", (long)newState);
+}
+
+- (void)peerConnection:(RTCPeerConnection *)peerConnection
+    didGenerateIceCandidate:(RTCIceCandidate *)candidate {
+  dispatch_async(dispatch_get_main_queue(), ^{
+    ARDICECandidateMessage *message =
+        [[ARDICECandidateMessage alloc] initWithCandidate:candidate];
+    [self sendSignalingMessage:message];
+  });
+}
+
+- (void)peerConnection:(RTCPeerConnection *)peerConnection
+    didRemoveIceCandidates:(NSArray<RTCIceCandidate *> *)candidates {
+  dispatch_async(dispatch_get_main_queue(), ^{
+    ARDICECandidateRemovalMessage *message =
+        [[ARDICECandidateRemovalMessage alloc]
+            initWithRemovedCandidates:candidates];
+    [self sendSignalingMessage:message];
+  });
+}
+
+- (void)peerConnection:(RTCPeerConnection *)peerConnection
+    didOpenDataChannel:(RTCDataChannel *)dataChannel {
+}
+
+#pragma mark - RTCSessionDescriptionDelegate
+// Callbacks for this delegate occur on non-main thread and need to be
+// dispatched back to main queue as needed.
+
+- (void)peerConnection:(RTCPeerConnection *)peerConnection
+    didCreateSessionDescription:(RTCSessionDescription *)sdp
+                          error:(NSError *)error {
+  dispatch_async(dispatch_get_main_queue(), ^{
+    if (error) {
+      RTCLogError(@"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;
+    }
+    // Prefer H264 if available.
+    RTCSessionDescription *sdpPreferringH264 =
+        [ARDSDPUtils descriptionForDescription:sdp
+                           preferredVideoCodec:@"H264"];
+    __weak ARDAppClient *weakSelf = self;
+    [_peerConnection setLocalDescription:sdpPreferringH264
+                       completionHandler:^(NSError *error) {
+      ARDAppClient *strongSelf = weakSelf;
+      [strongSelf peerConnection:strongSelf.peerConnection
+          didSetSessionDescriptionWithError:error];
+    }];
+    ARDSessionDescriptionMessage *message =
+        [[ARDSessionDescriptionMessage alloc]
+            initWithDescription:sdpPreferringH264];
+    [self sendSignalingMessage:message];
+  });
+}
+
+- (void)peerConnection:(RTCPeerConnection *)peerConnection
+    didSetSessionDescriptionWithError:(NSError *)error {
+  dispatch_async(dispatch_get_main_queue(), ^{
+    if (error) {
+      RTCLogError(@"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];
+      __weak ARDAppClient *weakSelf = self;
+      [_peerConnection answerForConstraints:constraints
+                          completionHandler:^(RTCSessionDescription *sdp,
+                                              NSError *error) {
+        ARDAppClient *strongSelf = weakSelf;
+        [strongSelf peerConnection:strongSelf.peerConnection
+            didCreateSessionDescription:sdp
+                                  error:error];
+      }];
+    }
+  });
+}
+
+#pragma mark - Private
+
+#if defined(WEBRTC_IOS)
+
+- (NSString *)documentsFilePathForFileName:(NSString *)fileName {
+  NSParameterAssert(fileName.length);
+  NSArray *paths = NSSearchPathForDirectoriesInDomains(
+      NSDocumentDirectory, NSUserDomainMask, YES);
+  NSString *documentsDirPath = paths.firstObject;
+  NSString *filePath =
+      [documentsDirPath stringByAppendingPathComponent:fileName];
+  return filePath;
+}
+
+#endif
+
+- (BOOL)hasJoinedRoomServerRoom {
+  return _clientId.length;
+}
+
+// Begins the peer connection connection process if we have both joined a room
+// on the room server and tried to obtain a TURN server. Otherwise does nothing.
+// A peer connection object will be created with a stream that contains local
+// audio and video capture. If this client is the caller, an offer is created as
+// well, otherwise the client will wait for an offer to arrive.
+- (void)startSignalingIfReady {
+  if (!_isTurnComplete || !self.hasJoinedRoomServerRoom) {
+    return;
+  }
+  self.state = kARDAppClientStateConnected;
+
+  // Create peer connection.
+  RTCMediaConstraints *constraints = [self defaultPeerConnectionConstraints];
+  RTCConfiguration *config = [[RTCConfiguration alloc] init];
+  config.iceServers = _iceServers;
+  _peerConnection = [_factory peerConnectionWithConfiguration:config
+                                                  constraints:constraints
+                                                     delegate:self];
+  // Create AV senders.
+  [self createAudioSender];
+  [self createVideoSender];
+  if (_isInitiator) {
+    // Send offer.
+    __weak ARDAppClient *weakSelf = self;
+    [_peerConnection offerForConstraints:[self defaultOfferConstraints]
+                       completionHandler:^(RTCSessionDescription *sdp,
+                                           NSError *error) {
+      ARDAppClient *strongSelf = weakSelf;
+      [strongSelf peerConnection:strongSelf.peerConnection
+          didCreateSessionDescription:sdp
+                                error:error];
+    }];
+  } else {
+    // Check if we've received an offer.
+    [self drainMessageQueueIfReady];
+  }
+#if defined(WEBRTC_IOS)
+  // Start event log.
+  if (kARDAppClientEnableRtcEventLog) {
+    NSString *filePath = [self documentsFilePathForFileName:@"webrtc-rtceventlog"];
+    if (![_peerConnection startRtcEventLogWithFilePath:filePath
+                                 maxSizeInBytes:kARDAppClientRtcEventLogMaxSizeInBytes]) {
+      RTCLogError(@"Failed to start event logging.");
+    }
+  }
+
+  // Start aecdump diagnostic recording.
+  if (_shouldMakeAecDump) {
+    NSString *filePath = [self documentsFilePathForFileName:@"webrtc-audio.aecdump"];
+    if (![_factory startAecDumpWithFilePath:filePath
+                             maxSizeInBytes:kARDAppClientAecDumpMaxSizeInBytes]) {
+      RTCLogError(@"Failed to start aec dump.");
+    }
+  }
+#endif
+}
+
+// Processes the messages that we've received from the room server and the
+// signaling channel. The offer or answer message must be processed before other
+// signaling messages, however they can arrive out of order. Hence, this method
+// only processes pending messages if there is a peer connection object and
+// if we have received either an offer or answer.
+- (void)drainMessageQueueIfReady {
+  if (!_peerConnection || !_hasReceivedSdp) {
+    return;
+  }
+  for (ARDSignalingMessage *message in _messageQueue) {
+    [self processSignalingMessage:message];
+  }
+  [_messageQueue removeAllObjects];
+}
+
+// Processes the given signaling message based on its type.
+- (void)processSignalingMessage:(ARDSignalingMessage *)message {
+  NSParameterAssert(_peerConnection ||
+      message.type == kARDSignalingMessageTypeBye);
+  switch (message.type) {
+    case kARDSignalingMessageTypeOffer:
+    case kARDSignalingMessageTypeAnswer: {
+      ARDSessionDescriptionMessage *sdpMessage =
+          (ARDSessionDescriptionMessage *)message;
+      RTCSessionDescription *description = sdpMessage.sessionDescription;
+      // Prefer H264 if available.
+      RTCSessionDescription *sdpPreferringH264 =
+          [ARDSDPUtils descriptionForDescription:description
+                             preferredVideoCodec:@"H264"];
+      __weak ARDAppClient *weakSelf = self;
+      [_peerConnection setRemoteDescription:sdpPreferringH264
+                          completionHandler:^(NSError *error) {
+        ARDAppClient *strongSelf = weakSelf;
+        [strongSelf peerConnection:strongSelf.peerConnection
+            didSetSessionDescriptionWithError:error];
+      }];
+      break;
+    }
+    case kARDSignalingMessageTypeCandidate: {
+      ARDICECandidateMessage *candidateMessage =
+          (ARDICECandidateMessage *)message;
+      [_peerConnection addIceCandidate:candidateMessage.candidate];
+      break;
+    }
+    case kARDSignalingMessageTypeCandidateRemoval: {
+      ARDICECandidateRemovalMessage *candidateMessage =
+          (ARDICECandidateRemovalMessage *)message;
+      [_peerConnection removeIceCandidates:candidateMessage.candidates];
+      break;
+    }
+    case kARDSignalingMessageTypeBye:
+      // Other client disconnected.
+      // TODO(tkchin): support waiting in room for next client. For now just
+      // disconnect.
+      [self disconnect];
+      break;
+  }
+}
+
+// Sends a signaling message to the other client. The caller will send messages
+// through the room server, whereas the callee will send messages over the
+// signaling channel.
+- (void)sendSignalingMessage:(ARDSignalingMessage *)message {
+  if (_isInitiator) {
+    __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 {
+    [_channel sendMessage:message];
+  }
+}
+
+- (RTCRtpSender *)createVideoSender {
+  RTCRtpSender *sender =
+      [_peerConnection senderWithKind:kRTCMediaStreamTrackKindVideo
+                             streamId:kARDMediaStreamId];
+  RTCVideoTrack *track = [self createLocalVideoTrack];
+  if (track) {
+    sender.track = track;
+    [_delegate appClient:self didReceiveLocalVideoTrack:track];
+  }
+  return sender;
+}
+
+- (RTCRtpSender *)createAudioSender {
+  RTCMediaConstraints *constraints = [self defaultMediaAudioConstraints];
+  RTCAudioSource *source = [_factory audioSourceWithConstraints:constraints];
+  RTCAudioTrack *track = [_factory audioTrackWithSource:source
+                                                trackId:kARDAudioTrackId];
+  RTCRtpSender *sender =
+      [_peerConnection senderWithKind:kRTCMediaStreamTrackKindAudio
+                             streamId:kARDMediaStreamId];
+  sender.track = track;
+  return sender;
+}
+
+- (RTCVideoTrack *)createLocalVideoTrack {
+  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.
+#if !TARGET_IPHONE_SIMULATOR
+  if (!_isAudioOnly) {
+    RTCMediaConstraints *mediaConstraints =
+        [self defaultMediaStreamConstraints];
+    RTCAVFoundationVideoSource *source =
+        [_factory avFoundationVideoSourceWithConstraints:mediaConstraints];
+    localVideoTrack =
+        [_factory videoTrackWithSource:source
+                               trackId:kARDVideoTrackId];
+  }
+#endif
+  return localVideoTrack;
+}
+
+#pragma mark - Collider methods
+
+- (void)registerWithColliderIfReady {
+  if (!self.hasJoinedRoomServerRoom) {
+    return;
+  }
+  // Open WebSocket connection.
+  if (!_channel) {
+    _channel =
+        [[ARDWebSocketChannel alloc] initWithURL:_websocketURL
+                                         restURL:_websocketRestURL
+                                        delegate:self];
+    if (_isLoopback) {
+      _loopbackChannel =
+          [[ARDLoopbackWebSocketChannel alloc] initWithURL:_websocketURL
+                                                   restURL:_websocketRestURL];
+    }
+  }
+  [_channel registerForRoomId:_roomId clientId:_clientId];
+  if (_isLoopback) {
+    [_loopbackChannel registerForRoomId:_roomId clientId:@"LOOPBACK_CLIENT_ID"];
+  }
+}
+
+#pragma mark - Defaults
+
+ - (RTCMediaConstraints *)defaultMediaAudioConstraints {
+   NSString *valueLevelControl = _shouldUseLevelControl ?
+       kRTCMediaConstraintsValueTrue : kRTCMediaConstraintsValueFalse;
+   NSDictionary *mandatoryConstraints = @{ kRTCMediaConstraintsLevelControl : valueLevelControl };
+   RTCMediaConstraints* constraints =
+       [[RTCMediaConstraints alloc]  initWithMandatoryConstraints:mandatoryConstraints
+                                              optionalConstraints:nil];
+   return constraints;
+}
+
+- (RTCMediaConstraints *)defaultMediaStreamConstraints {
+  RTCMediaConstraints* constraints =
+      [[RTCMediaConstraints alloc]
+          initWithMandatoryConstraints:nil
+                   optionalConstraints:nil];
+  return constraints;
+}
+
+- (RTCMediaConstraints *)defaultAnswerConstraints {
+  return [self defaultOfferConstraints];
+}
+
+- (RTCMediaConstraints *)defaultOfferConstraints {
+  NSDictionary *mandatoryConstraints = @{
+    @"OfferToReceiveAudio" : @"true",
+    @"OfferToReceiveVideo" : @"true"
+  };
+  RTCMediaConstraints* constraints =
+      [[RTCMediaConstraints alloc]
+          initWithMandatoryConstraints:mandatoryConstraints
+                   optionalConstraints:nil];
+  return constraints;
+}
+
+- (RTCMediaConstraints *)defaultPeerConnectionConstraints {
+  if (_defaultPeerConnectionConstraints) {
+    return _defaultPeerConnectionConstraints;
+  }
+  NSString *value = _isLoopback ? @"false" : @"true";
+  NSDictionary *optionalConstraints = @{ @"DtlsSrtpKeyAgreement" : value };
+  RTCMediaConstraints* constraints =
+      [[RTCMediaConstraints alloc]
+          initWithMandatoryConstraints:nil
+                   optionalConstraints:optionalConstraints];
+  return constraints;
+}
+
+- (RTCIceServer *)defaultSTUNServer {
+  return [[RTCIceServer alloc] initWithURLStrings:@[kARDDefaultSTUNServerUrl]
+                                         username:@""
+                                       credential:@""];
+}
+
+#pragma mark - Errors
+
++ (NSError *)errorForJoinResultType:(ARDJoinResultType)resultType {
+  NSError *error = nil;
+  switch (resultType) {
+    case kARDJoinResultTypeSuccess:
+      break;
+    case kARDJoinResultTypeUnknown: {
+      error = [[NSError alloc] initWithDomain:kARDAppClientErrorDomain
+                                         code:kARDAppClientErrorUnknown
+                                     userInfo:@{
+        NSLocalizedDescriptionKey: @"Unknown error.",
+      }];
+      break;
+    }
+    case kARDJoinResultTypeFull: {
+      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/webrtc/examples/objc/AppRTCMobile/ARDAppEngineClient.h b/webrtc/examples/objc/AppRTCMobile/ARDAppEngineClient.h
new file mode 100644
index 0000000..7514f36
--- /dev/null
+++ b/webrtc/examples/objc/AppRTCMobile/ARDAppEngineClient.h
@@ -0,0 +1,14 @@
+/*
+ *  Copyright 2014 The WebRTC Project Authors. All rights reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#import "ARDRoomServerClient.h"
+
+@interface ARDAppEngineClient : NSObject <ARDRoomServerClient>
+@end
diff --git a/webrtc/examples/objc/AppRTCMobile/ARDAppEngineClient.m b/webrtc/examples/objc/AppRTCMobile/ARDAppEngineClient.m
new file mode 100644
index 0000000..d707b92
--- /dev/null
+++ b/webrtc/examples/objc/AppRTCMobile/ARDAppEngineClient.m
@@ -0,0 +1,175 @@
+/*
+ *  Copyright 2014 The WebRTC Project Authors. All rights reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#import "ARDAppEngineClient.h"
+
+#import "WebRTC/RTCLogging.h"
+
+#import "ARDJoinResponse.h"
+#import "ARDMessageResponse.h"
+#import "ARDSignalingMessage.h"
+#import "ARDUtilities.h"
+
+// TODO(tkchin): move these to a configuration object.
+static NSString * const kARDRoomServerHostUrl =
+    @"https://appr.tc";
+static NSString * const kARDRoomServerJoinFormat =
+    @"https://appr.tc/join/%@";
+static NSString * const kARDRoomServerJoinFormatLoopback =
+    @"https://appr.tc/join/%@?debug=loopback";
+static NSString * const kARDRoomServerMessageFormat =
+    @"https://appr.tc/message/%@/%@";
+static NSString * const kARDRoomServerLeaveFormat =
+    @"https://appr.tc/leave/%@/%@";
+
+static NSString * const kARDAppEngineClientErrorDomain = @"ARDAppEngineClient";
+static NSInteger const kARDAppEngineClientErrorBadResponse = -1;
+
+@implementation ARDAppEngineClient
+
+#pragma mark - ARDRoomServerClient
+
+- (void)joinRoomWithRoomId:(NSString *)roomId
+                isLoopback:(BOOL)isLoopback
+         completionHandler:(void (^)(ARDJoinResponse *response,
+                                     NSError *error))completionHandler {
+  NSParameterAssert(roomId.length);
+
+  NSString *urlString = nil;
+  if (isLoopback) {
+    urlString =
+        [NSString stringWithFormat:kARDRoomServerJoinFormatLoopback, roomId];
+  } else {
+    urlString =
+        [NSString stringWithFormat:kARDRoomServerJoinFormat, roomId];
+  }
+
+  NSURL *roomURL = [NSURL URLWithString:urlString];
+  RTCLog(@"Joining room:%@ on room server.", roomId);
+  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;
+    }
+    ARDJoinResponse *joinResponse =
+        [ARDJoinResponse responseFromJSONData:data];
+    if (!joinResponse) {
+      if (completionHandler) {
+        NSError *error = [[self class] badResponseError];
+        completionHandler(nil, error);
+      }
+      return;
+    }
+    if (completionHandler) {
+      completionHandler(joinResponse, 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];
+  RTCLog(@"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)leaveRoomWithRoomId:(NSString *)roomId
+                   clientId:(NSString *)clientId
+          completionHandler:(void (^)(NSError *error))completionHandler {
+  NSParameterAssert(roomId.length);
+  NSParameterAssert(clientId.length);
+
+  NSString *urlString =
+      [NSString stringWithFormat:kARDRoomServerLeaveFormat, roomId, clientId];
+  NSURL *url = [NSURL URLWithString:urlString];
+  NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
+  request.HTTPMethod = @"POST";
+  NSURLResponse *response = nil;
+  NSError *error = nil;
+  // We want a synchronous request so that we know that we've left the room on
+  // room server before we do any further work.
+  RTCLog(@"C->RS: BYE");
+  [NSURLConnection sendSynchronousRequest:request
+                        returningResponse:&response
+                                    error:&error];
+  if (error) {
+    RTCLogError(@"Error leaving room %@ on room server: %@",
+          roomId, error.localizedDescription);
+    if (completionHandler) {
+      completionHandler(error);
+    }
+    return;
+  }
+  RTCLog(@"Left room:%@ on room server.", roomId);
+  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/webrtc/examples/objc/AppRTCMobile/ARDBitrateTracker.h b/webrtc/examples/objc/AppRTCMobile/ARDBitrateTracker.h
new file mode 100644
index 0000000..81ac4b4
--- /dev/null
+++ b/webrtc/examples/objc/AppRTCMobile/ARDBitrateTracker.h
@@ -0,0 +1,30 @@
+/*
+ *  Copyright 2015 The WebRTC Project Authors. All rights reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#import <Foundation/Foundation.h>
+
+/** Class used to estimate bitrate based on byte count. It is expected that
+ *  byte count is monotonocially increasing. This class tracks the times that
+ *  byte count is updated, and measures the bitrate based on the byte difference
+ *  over the interval between updates.
+ */
+@interface ARDBitrateTracker : NSObject
+
+/** The bitrate in bits per second. */
+@property(nonatomic, readonly) double bitrate;
+/** The bitrate as a formatted string in bps, Kbps or Mbps. */
+@property(nonatomic, readonly) NSString *bitrateString;
+
+/** Converts the bitrate to a readable format in bps, Kbps or Mbps. */
++ (NSString *)bitrateStringForBitrate:(double)bitrate;
+/** Updates the tracked bitrate with the new byte count. */
+- (void)updateBitrateWithCurrentByteCount:(NSInteger)byteCount;
+
+@end
diff --git a/webrtc/examples/objc/AppRTCMobile/ARDBitrateTracker.m b/webrtc/examples/objc/AppRTCMobile/ARDBitrateTracker.m
new file mode 100644
index 0000000..8158229
--- /dev/null
+++ b/webrtc/examples/objc/AppRTCMobile/ARDBitrateTracker.m
@@ -0,0 +1,45 @@
+/*
+ *  Copyright 2015 The WebRTC Project Authors. All rights reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#import "ARDBitrateTracker.h"
+
+#import <QuartzCore/QuartzCore.h>
+
+@implementation ARDBitrateTracker {
+  CFTimeInterval _prevTime;
+  NSInteger _prevByteCount;
+}
+
+@synthesize bitrate = _bitrate;
+
++ (NSString *)bitrateStringForBitrate:(double)bitrate {
+  if (bitrate > 1e6) {
+    return [NSString stringWithFormat:@"%.2fMbps", bitrate * 1e-6];
+  } else if (bitrate > 1e3) {
+    return [NSString stringWithFormat:@"%.0fKbps", bitrate * 1e-3];
+  } else {
+    return [NSString stringWithFormat:@"%.0fbps", bitrate];
+  }
+}
+
+- (NSString *)bitrateString {
+  return [[self class] bitrateStringForBitrate:_bitrate];
+}
+
+- (void)updateBitrateWithCurrentByteCount:(NSInteger)byteCount {
+  CFTimeInterval currentTime = CACurrentMediaTime();
+  if (_prevTime && (byteCount > _prevByteCount)) {
+    _bitrate = (byteCount - _prevByteCount) * 8 / (currentTime - _prevTime);
+  }
+  _prevByteCount = byteCount;
+  _prevTime = currentTime;
+}
+
+@end
diff --git a/webrtc/examples/objc/AppRTCMobile/ARDCEODTURNClient.h b/webrtc/examples/objc/AppRTCMobile/ARDCEODTURNClient.h
new file mode 100644
index 0000000..9b136aa
--- /dev/null
+++ b/webrtc/examples/objc/AppRTCMobile/ARDCEODTURNClient.h
@@ -0,0 +1,18 @@
+/*
+ *  Copyright 2014 The WebRTC Project Authors. All rights reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#import "ARDTURNClient.h"
+
+// Requests TURN server urls from compute engine on demand.
+@interface ARDCEODTURNClient : NSObject <ARDTURNClient>
+
+- (instancetype)initWithURL:(NSURL *)url;
+
+@end
diff --git a/webrtc/examples/objc/AppRTCMobile/ARDCEODTURNClient.m b/webrtc/examples/objc/AppRTCMobile/ARDCEODTURNClient.m
new file mode 100644
index 0000000..5be3335
--- /dev/null
+++ b/webrtc/examples/objc/AppRTCMobile/ARDCEODTURNClient.m
@@ -0,0 +1,66 @@
+/*
+ *  Copyright 2014 The WebRTC Project Authors. All rights reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#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(nil, error);
+      return;
+    }
+    NSDictionary *dict = [NSDictionary dictionaryWithJSONData:data];
+    turnServers = @[ [RTCIceServer serverFromCEODJSONDictionary: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/webrtc/examples/objc/AppRTCMobile/ARDJoinResponse+Internal.h b/webrtc/examples/objc/AppRTCMobile/ARDJoinResponse+Internal.h
new file mode 100644
index 0000000..b320299
--- /dev/null
+++ b/webrtc/examples/objc/AppRTCMobile/ARDJoinResponse+Internal.h
@@ -0,0 +1,23 @@
+/*
+ *  Copyright 2014 The WebRTC Project Authors. All rights reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#import "ARDJoinResponse.h"
+
+@interface ARDJoinResponse ()
+
+@property(nonatomic, assign) ARDJoinResultType 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/webrtc/examples/objc/AppRTCMobile/ARDJoinResponse.h b/webrtc/examples/objc/AppRTCMobile/ARDJoinResponse.h
new file mode 100644
index 0000000..2911202
--- /dev/null
+++ b/webrtc/examples/objc/AppRTCMobile/ARDJoinResponse.h
@@ -0,0 +1,32 @@
+/*
+ *  Copyright 2014 The WebRTC Project Authors. All rights reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#import <Foundation/Foundation.h>
+
+typedef NS_ENUM(NSInteger, ARDJoinResultType) {
+  kARDJoinResultTypeUnknown,
+  kARDJoinResultTypeSuccess,
+  kARDJoinResultTypeFull
+};
+
+// Result of joining a room on the room server.
+@interface ARDJoinResponse : NSObject
+
+@property(nonatomic, readonly) ARDJoinResultType 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;
+
++ (ARDJoinResponse *)responseFromJSONData:(NSData *)data;
+
+@end
diff --git a/webrtc/examples/objc/AppRTCMobile/ARDJoinResponse.m b/webrtc/examples/objc/AppRTCMobile/ARDJoinResponse.m
new file mode 100644
index 0000000..87d58e0
--- /dev/null
+++ b/webrtc/examples/objc/AppRTCMobile/ARDJoinResponse.m
@@ -0,0 +1,82 @@
+/*
+ *  Copyright 2014 The WebRTC Project Authors. All rights reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#import "ARDJoinResponse+Internal.h"
+
+#import "ARDSignalingMessage.h"
+#import "ARDUtilities.h"
+#import "RTCIceServer+JSON.h"
+
+static NSString const *kARDJoinResultKey = @"result";
+static NSString const *kARDJoinResultParamsKey = @"params";
+static NSString const *kARDJoinInitiatorKey = @"is_initiator";
+static NSString const *kARDJoinRoomIdKey = @"room_id";
+static NSString const *kARDJoinClientIdKey = @"client_id";
+static NSString const *kARDJoinMessagesKey = @"messages";
+static NSString const *kARDJoinWebSocketURLKey = @"wss_url";
+static NSString const *kARDJoinWebSocketRestURLKey = @"wss_post_url";
+
+@implementation ARDJoinResponse
+
+@synthesize result = _result;
+@synthesize isInitiator = _isInitiator;
+@synthesize roomId = _roomId;
+@synthesize clientId = _clientId;
+@synthesize messages = _messages;
+@synthesize webSocketURL = _webSocketURL;
+@synthesize webSocketRestURL = _webSocketRestURL;
+
++ (ARDJoinResponse *)responseFromJSONData:(NSData *)data {
+  NSDictionary *responseJSON = [NSDictionary dictionaryWithJSONData:data];
+  if (!responseJSON) {
+    return nil;
+  }
+  ARDJoinResponse *response = [[ARDJoinResponse alloc] init];
+  NSString *resultString = responseJSON[kARDJoinResultKey];
+  response.result = [[self class] resultTypeFromString:resultString];
+  NSDictionary *params = responseJSON[kARDJoinResultParamsKey];
+
+  response.isInitiator = [params[kARDJoinInitiatorKey] boolValue];
+  response.roomId = params[kARDJoinRoomIdKey];
+  response.clientId = params[kARDJoinClientIdKey];
+
+  // Parse messages.
+  NSArray *messages = params[kARDJoinMessagesKey];
+  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[kARDJoinWebSocketURLKey];
+  response.webSocketURL = [NSURL URLWithString:webSocketURLString];
+  NSString *webSocketRestURLString = params[kARDJoinWebSocketRestURLKey];
+  response.webSocketRestURL = [NSURL URLWithString:webSocketRestURLString];
+
+  return response;
+}
+
+#pragma mark - Private
+
++ (ARDJoinResultType)resultTypeFromString:(NSString *)resultString {
+  ARDJoinResultType result = kARDJoinResultTypeUnknown;
+  if ([resultString isEqualToString:@"SUCCESS"]) {
+    result = kARDJoinResultTypeSuccess;
+  } else if ([resultString isEqualToString:@"FULL"]) {
+    result = kARDJoinResultTypeFull;
+  }
+  return result;
+}
+
+@end
diff --git a/webrtc/examples/objc/AppRTCMobile/ARDMessageResponse+Internal.h b/webrtc/examples/objc/AppRTCMobile/ARDMessageResponse+Internal.h
new file mode 100644
index 0000000..66ee761
--- /dev/null
+++ b/webrtc/examples/objc/AppRTCMobile/ARDMessageResponse+Internal.h
@@ -0,0 +1,17 @@
+/*
+ *  Copyright 2014 The WebRTC Project Authors. All rights reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#import "ARDMessageResponse.h"
+
+@interface ARDMessageResponse ()
+
+@property(nonatomic, assign) ARDMessageResultType result;
+
+@end
diff --git a/webrtc/examples/objc/AppRTCMobile/ARDMessageResponse.h b/webrtc/examples/objc/AppRTCMobile/ARDMessageResponse.h
new file mode 100644
index 0000000..65468cd
--- /dev/null
+++ b/webrtc/examples/objc/AppRTCMobile/ARDMessageResponse.h
@@ -0,0 +1,26 @@
+/*
+ *  Copyright 2014 The WebRTC Project Authors. All rights reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#import <Foundation/Foundation.h>
+
+typedef NS_ENUM(NSInteger, ARDMessageResultType) {
+  kARDMessageResultTypeUnknown,
+  kARDMessageResultTypeSuccess,
+  kARDMessageResultTypeInvalidRoom,
+  kARDMessageResultTypeInvalidClient
+};
+
+@interface ARDMessageResponse : NSObject
+
+@property(nonatomic, readonly) ARDMessageResultType result;
+
++ (ARDMessageResponse *)responseFromJSONData:(NSData *)data;
+
+@end
diff --git a/webrtc/examples/objc/AppRTCMobile/ARDMessageResponse.m b/webrtc/examples/objc/AppRTCMobile/ARDMessageResponse.m
new file mode 100644
index 0000000..0f5383f
--- /dev/null
+++ b/webrtc/examples/objc/AppRTCMobile/ARDMessageResponse.m
@@ -0,0 +1,46 @@
+/*
+ *  Copyright 2014 The WebRTC Project Authors. All rights reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#import "ARDMessageResponse+Internal.h"
+
+#import "ARDUtilities.h"
+
+static NSString const *kARDMessageResultKey = @"result";
+
+@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/webrtc/examples/objc/AppRTCMobile/ARDRoomServerClient.h b/webrtc/examples/objc/AppRTCMobile/ARDRoomServerClient.h
new file mode 100644
index 0000000..70694a8
--- /dev/null
+++ b/webrtc/examples/objc/AppRTCMobile/ARDRoomServerClient.h
@@ -0,0 +1,34 @@
+/*
+ *  Copyright 2014 The WebRTC Project Authors. All rights reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#import <Foundation/Foundation.h>
+
+@class ARDJoinResponse;
+@class ARDMessageResponse;
+@class ARDSignalingMessage;
+
+@protocol ARDRoomServerClient <NSObject>
+
+- (void)joinRoomWithRoomId:(NSString *)roomId
+                isLoopback:(BOOL)isLoopback
+         completionHandler:(void (^)(ARDJoinResponse *response,
+                                     NSError *error))completionHandler;
+
+- (void)sendMessage:(ARDSignalingMessage *)message
+            forRoomId:(NSString *)roomId
+             clientId:(NSString *)clientId
+    completionHandler:(void (^)(ARDMessageResponse *response,
+                                NSError *error))completionHandler;
+
+- (void)leaveRoomWithRoomId:(NSString *)roomId
+                   clientId:(NSString *)clientId
+          completionHandler:(void (^)(NSError *error))completionHandler;
+
+@end
diff --git a/webrtc/examples/objc/AppRTCMobile/ARDSDPUtils.h b/webrtc/examples/objc/AppRTCMobile/ARDSDPUtils.h
new file mode 100644
index 0000000..18795af
--- /dev/null
+++ b/webrtc/examples/objc/AppRTCMobile/ARDSDPUtils.h
@@ -0,0 +1,24 @@
+/*
+ *  Copyright 2015 The WebRTC Project Authors. All rights reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#import <Foundation/Foundation.h>
+
+@class RTCSessionDescription;
+
+@interface ARDSDPUtils : NSObject
+
+// Updates the original SDP description to instead prefer the specified video
+// codec. We do this by placing the specified codec at the beginning of the
+// codec list if it exists in the sdp.
++ (RTCSessionDescription *)
+    descriptionForDescription:(RTCSessionDescription *)description
+          preferredVideoCodec:(NSString *)codec;
+
+@end
diff --git a/webrtc/examples/objc/AppRTCMobile/ARDSDPUtils.m b/webrtc/examples/objc/AppRTCMobile/ARDSDPUtils.m
new file mode 100644
index 0000000..3a8a578
--- /dev/null
+++ b/webrtc/examples/objc/AppRTCMobile/ARDSDPUtils.m
@@ -0,0 +1,92 @@
+/*
+ *  Copyright 2015 The WebRTC Project Authors. All rights reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#import "ARDSDPUtils.h"
+
+#import "WebRTC/RTCLogging.h"
+#import "WebRTC/RTCSessionDescription.h"
+
+@implementation ARDSDPUtils
+
++ (RTCSessionDescription *)
+    descriptionForDescription:(RTCSessionDescription *)description
+          preferredVideoCodec:(NSString *)codec {
+  NSString *sdpString = description.sdp;
+  NSString *lineSeparator = @"\n";
+  NSString *mLineSeparator = @" ";
+  // Copied from PeerConnectionClient.java.
+  // TODO(tkchin): Move this to a shared C++ file.
+  NSMutableArray *lines =
+      [NSMutableArray arrayWithArray:
+          [sdpString componentsSeparatedByString:lineSeparator]];
+  NSInteger mLineIndex = -1;
+  NSString *codecRtpMap = nil;
+  // a=rtpmap:<payload type> <encoding name>/<clock rate>
+  // [/<encoding parameters>]
+  NSString *pattern =
+      [NSString stringWithFormat:@"^a=rtpmap:(\\d+) %@(/\\d+)+[\r]?$", codec];
+  NSRegularExpression *regex =
+      [NSRegularExpression regularExpressionWithPattern:pattern
+                                                options:0
+                                                  error:nil];
+  for (NSInteger i = 0; (i < lines.count) && (mLineIndex == -1 || !codecRtpMap);
+       ++i) {
+    NSString *line = lines[i];
+    if ([line hasPrefix:@"m=video"]) {
+      mLineIndex = i;
+      continue;
+    }
+    NSTextCheckingResult *codecMatches =
+        [regex firstMatchInString:line
+                          options:0
+                            range:NSMakeRange(0, line.length)];
+    if (codecMatches) {
+      codecRtpMap =
+          [line substringWithRange:[codecMatches rangeAtIndex:1]];
+      continue;
+    }
+  }
+  if (mLineIndex == -1) {
+    RTCLog(@"No m=video line, so can't prefer %@", codec);
+    return description;
+  }
+  if (!codecRtpMap) {
+    RTCLog(@"No rtpmap for %@", codec);
+    return description;
+  }
+  NSArray *origMLineParts =
+      [lines[mLineIndex] componentsSeparatedByString:mLineSeparator];
+  if (origMLineParts.count > 3) {
+    NSMutableArray *newMLineParts =
+        [NSMutableArray arrayWithCapacity:origMLineParts.count];
+    NSInteger origPartIndex = 0;
+    // Format is: m=<media> <port> <proto> <fmt> ...
+    [newMLineParts addObject:origMLineParts[origPartIndex++]];
+    [newMLineParts addObject:origMLineParts[origPartIndex++]];
+    [newMLineParts addObject:origMLineParts[origPartIndex++]];
+    [newMLineParts addObject:codecRtpMap];
+    for (; origPartIndex < origMLineParts.count; ++origPartIndex) {
+      if (![codecRtpMap isEqualToString:origMLineParts[origPartIndex]]) {
+        [newMLineParts addObject:origMLineParts[origPartIndex]];
+      }
+    }
+    NSString *newMLine =
+        [newMLineParts componentsJoinedByString:mLineSeparator];
+    [lines replaceObjectAtIndex:mLineIndex
+                     withObject:newMLine];
+  } else {
+    RTCLogWarning(@"Wrong SDP media description format: %@", lines[mLineIndex]);
+  }
+  NSString *mangledSdpString = [lines componentsJoinedByString:lineSeparator];
+  return [[RTCSessionDescription alloc] initWithType:description.type
+                                                 sdp:mangledSdpString];
+}
+
+@end
diff --git a/webrtc/examples/objc/AppRTCMobile/ARDSignalingChannel.h b/webrtc/examples/objc/AppRTCMobile/ARDSignalingChannel.h
new file mode 100644
index 0000000..70ba2ff
--- /dev/null
+++ b/webrtc/examples/objc/AppRTCMobile/ARDSignalingChannel.h
@@ -0,0 +1,52 @@
+/*
+ *  Copyright 2014 The WebRTC Project Authors. All rights reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#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/webrtc/examples/objc/AppRTCMobile/ARDSignalingMessage.h b/webrtc/examples/objc/AppRTCMobile/ARDSignalingMessage.h
new file mode 100644
index 0000000..e605172
--- /dev/null
+++ b/webrtc/examples/objc/AppRTCMobile/ARDSignalingMessage.h
@@ -0,0 +1,59 @@
+/*
+ *  Copyright 2014 The WebRTC Project Authors. All rights reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "WebRTC/RTCIceCandidate.h"
+#import "WebRTC/RTCSessionDescription.h"
+
+typedef enum {
+  kARDSignalingMessageTypeCandidate,
+  kARDSignalingMessageTypeCandidateRemoval,
+  kARDSignalingMessageTypeOffer,
+  kARDSignalingMessageTypeAnswer,
+  kARDSignalingMessageTypeBye,
+} ARDSignalingMessageType;
+
+@interface ARDSignalingMessage : NSObject
+
+@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 ARDICECandidateRemovalMessage : ARDSignalingMessage
+
+@property(nonatomic, readonly) NSArray<RTCIceCandidate *> *candidates;
+
+- (instancetype)initWithRemovedCandidates:
+    (NSArray<RTCIceCandidate *> *)candidates;
+
+@end
+
+@interface ARDSessionDescriptionMessage : ARDSignalingMessage
+
+@property(nonatomic, readonly) RTCSessionDescription *sessionDescription;
+
+- (instancetype)initWithDescription:(RTCSessionDescription *)description;
+
+@end
+
+@interface ARDByeMessage : ARDSignalingMessage
+@end
diff --git a/webrtc/examples/objc/AppRTCMobile/ARDSignalingMessage.m b/webrtc/examples/objc/AppRTCMobile/ARDSignalingMessage.m
new file mode 100644
index 0000000..3fab185
--- /dev/null
+++ b/webrtc/examples/objc/AppRTCMobile/ARDSignalingMessage.m
@@ -0,0 +1,161 @@
+/*
+ *  Copyright 2014 The WebRTC Project Authors. All rights reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#import "ARDSignalingMessage.h"
+
+#import "WebRTC/RTCLogging.h"
+
+#import "ARDUtilities.h"
+#import "RTCIceCandidate+JSON.h"
+#import "RTCSessionDescription+JSON.h"
+
+static NSString * const kARDSignalingMessageTypeKey = @"type";
+static NSString * const kARDTypeValueRemoveCandidates = @"remove-candidates";
+
+@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) {
+    RTCLogError(@"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:kARDTypeValueRemoveCandidates]) {
+    RTCLogInfo(@"Received remove-candidates message");
+    NSArray<RTCIceCandidate *> *candidates =
+        [RTCIceCandidate candidatesFromJSONDictionary:values];
+    message = [[ARDICECandidateRemovalMessage alloc]
+                  initWithRemovedCandidates:candidates];
+  } 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 {
+    RTCLogError(@"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 ARDICECandidateRemovalMessage
+
+@synthesize candidates = _candidates;
+
+- (instancetype)initWithRemovedCandidates:(
+    NSArray<RTCIceCandidate *> *)candidates {
+  NSParameterAssert(candidates.count);
+  if (self = [super initWithType:kARDSignalingMessageTypeCandidateRemoval]) {
+    _candidates = candidates;
+  }
+  return self;
+}
+
+- (NSData *)JSONData {
+  return
+      [RTCIceCandidate JSONDataForIceCandidates:_candidates
+                                       withType:kARDTypeValueRemoveCandidates];
+}
+
+@end
+
+@implementation ARDSessionDescriptionMessage
+
+@synthesize sessionDescription = _sessionDescription;
+
+- (instancetype)initWithDescription:(RTCSessionDescription *)description {
+  ARDSignalingMessageType messageType = kARDSignalingMessageTypeOffer;
+  RTCSdpType sdpType = description.type;
+  switch (sdpType) {
+    case RTCSdpTypeOffer:
+      messageType = kARDSignalingMessageTypeOffer;
+      break;
+    case RTCSdpTypeAnswer:
+      messageType = kARDSignalingMessageTypeAnswer;
+      break;
+    case RTCSdpTypePrAnswer:
+      NSAssert(NO, @"Unexpected type: %@",
+          [RTCSessionDescription stringForType:sdpType]);
+      break;
+  }
+  if (self = [super initWithType:messageType]) {
+    _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/webrtc/examples/objc/AppRTCMobile/ARDStatsBuilder.h b/webrtc/examples/objc/AppRTCMobile/ARDStatsBuilder.h
new file mode 100644
index 0000000..a876b96
--- /dev/null
+++ b/webrtc/examples/objc/AppRTCMobile/ARDStatsBuilder.h
@@ -0,0 +1,29 @@
+/*
+ *  Copyright 2015 The WebRTC Project Authors. All rights reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#import <Foundation/Foundation.h>
+
+@class RTCLegacyStatsReport;
+
+/** Class used to accumulate stats information into a single displayable string.
+ */
+@interface ARDStatsBuilder : NSObject
+
+/** String that represents the accumulated stats reports passed into this
+ *  class.
+ */
+@property(nonatomic, readonly) NSString *statsString;
+
+/** Parses the information in the stats report into an appropriate internal
+ *  format used to generate the stats string.
+ */
+- (void)parseStatsReport:(RTCLegacyStatsReport *)statsReport;
+
+@end
diff --git a/webrtc/examples/objc/AppRTCMobile/ARDStatsBuilder.m b/webrtc/examples/objc/AppRTCMobile/ARDStatsBuilder.m
new file mode 100644
index 0000000..0643b42
--- /dev/null
+++ b/webrtc/examples/objc/AppRTCMobile/ARDStatsBuilder.m
@@ -0,0 +1,300 @@
+/*
+ *  Copyright 2015 The WebRTC Project Authors. All rights reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#import "ARDStatsBuilder.h"
+
+#import "WebRTC/RTCLegacyStatsReport.h"
+
+#import "ARDBitrateTracker.h"
+#import "ARDUtilities.h"
+
+@implementation ARDStatsBuilder {
+  // Connection stats.
+  NSString *_connRecvBitrate;
+  NSString *_connRtt;
+  NSString *_connSendBitrate;
+  NSString *_localCandType;
+  NSString *_remoteCandType;
+  NSString *_transportType;
+
+  // BWE stats.
+  NSString *_actualEncBitrate;
+  NSString *_availableRecvBw;
+  NSString *_availableSendBw;
+  NSString *_targetEncBitrate;
+
+  // Video send stats.
+  NSString *_videoEncodeMs;
+  NSString *_videoInputFps;
+  NSString *_videoInputHeight;
+  NSString *_videoInputWidth;
+  NSString *_videoSendCodec;
+  NSString *_videoSendBitrate;
+  NSString *_videoSendFps;
+  NSString *_videoSendHeight;
+  NSString *_videoSendWidth;
+
+  // Video receive stats.
+  NSString *_videoDecodeMs;
+  NSString *_videoDecodedFps;
+  NSString *_videoOutputFps;
+  NSString *_videoRecvBitrate;
+  NSString *_videoRecvFps;
+  NSString *_videoRecvHeight;
+  NSString *_videoRecvWidth;
+
+  // Audio send stats.
+  NSString *_audioSendBitrate;
+  NSString *_audioSendCodec;
+
+  // Audio receive stats.
+  NSString *_audioCurrentDelay;
+  NSString *_audioExpandRate;
+  NSString *_audioRecvBitrate;
+  NSString *_audioRecvCodec;
+
+  // Bitrate trackers.
+  ARDBitrateTracker *_audioRecvBitrateTracker;
+  ARDBitrateTracker *_audioSendBitrateTracker;
+  ARDBitrateTracker *_connRecvBitrateTracker;
+  ARDBitrateTracker *_connSendBitrateTracker;
+  ARDBitrateTracker *_videoRecvBitrateTracker;
+  ARDBitrateTracker *_videoSendBitrateTracker;
+}
+
+- (instancetype)init {
+  if (self = [super init]) {
+    _audioSendBitrateTracker = [[ARDBitrateTracker alloc] init];
+    _audioRecvBitrateTracker = [[ARDBitrateTracker alloc] init];
+    _connSendBitrateTracker = [[ARDBitrateTracker alloc] init];
+    _connRecvBitrateTracker = [[ARDBitrateTracker alloc] init];
+    _videoSendBitrateTracker = [[ARDBitrateTracker alloc] init];
+    _videoRecvBitrateTracker = [[ARDBitrateTracker alloc] init];
+  }
+  return self;
+}
+
+- (NSString *)statsString {
+  NSMutableString *result = [NSMutableString string];
+  NSString *systemStatsFormat = @"(cpu)%ld%%\n";
+  [result appendString:[NSString stringWithFormat:systemStatsFormat,
+      (long)ARDGetCpuUsagePercentage()]];
+
+  // Connection stats.
+  NSString *connStatsFormat = @"CN %@ms | %@->%@/%@ | (s)%@ | (r)%@\n";
+  [result appendString:[NSString stringWithFormat:connStatsFormat,
+      _connRtt,
+      _localCandType, _remoteCandType, _transportType,
+      _connSendBitrate, _connRecvBitrate]];
+
+  // Video send stats.
+  NSString *videoSendFormat = @"VS (input) %@x%@@%@fps | (sent) %@x%@@%@fps\n"
+                               "VS (enc) %@/%@ | (sent) %@/%@ | %@ms | %@\n";
+  [result appendString:[NSString stringWithFormat:videoSendFormat,
+      _videoInputWidth, _videoInputHeight, _videoInputFps,
+      _videoSendWidth, _videoSendHeight, _videoSendFps,
+      _actualEncBitrate, _targetEncBitrate,
+      _videoSendBitrate, _availableSendBw,
+      _videoEncodeMs,
+      _videoSendCodec]];
+
+  // Video receive stats.
+  NSString *videoReceiveFormat =
+      @"VR (recv) %@x%@@%@fps | (decoded)%@ | (output)%@fps | %@/%@ | %@ms\n";
+  [result appendString:[NSString stringWithFormat:videoReceiveFormat,
+      _videoRecvWidth, _videoRecvHeight, _videoRecvFps,
+      _videoDecodedFps,
+      _videoOutputFps,
+      _videoRecvBitrate, _availableRecvBw,
+      _videoDecodeMs]];
+
+  // Audio send stats.
+  NSString *audioSendFormat = @"AS %@ | %@\n";
+  [result appendString:[NSString stringWithFormat:audioSendFormat,
+      _audioSendBitrate, _audioSendCodec]];
+
+  // Audio receive stats.
+  NSString *audioReceiveFormat = @"AR %@ | %@ | %@ms | (expandrate)%@";
+  [result appendString:[NSString stringWithFormat:audioReceiveFormat,
+      _audioRecvBitrate, _audioRecvCodec, _audioCurrentDelay,
+      _audioExpandRate]];
+
+  return result;
+}
+
+- (void)parseStatsReport:(RTCLegacyStatsReport *)statsReport {
+  NSString *reportType = statsReport.type;
+  if ([reportType isEqualToString:@"ssrc"] &&
+      [statsReport.reportId rangeOfString:@"ssrc"].location != NSNotFound) {
+    if ([statsReport.reportId rangeOfString:@"send"].location != NSNotFound) {
+      [self parseSendSsrcStatsReport:statsReport];
+    }
+    if ([statsReport.reportId rangeOfString:@"recv"].location != NSNotFound) {
+      [self parseRecvSsrcStatsReport:statsReport];
+    }
+  } else if ([reportType isEqualToString:@"VideoBwe"]) {
+    [self parseBweStatsReport:statsReport];
+  } else if ([reportType isEqualToString:@"googCandidatePair"]) {
+    [self parseConnectionStatsReport:statsReport];
+  }
+}
+
+#pragma mark - Private
+
+- (void)parseBweStatsReport:(RTCLegacyStatsReport *)statsReport {
+  [statsReport.values enumerateKeysAndObjectsUsingBlock:^(
+      NSString *key, NSString *value, BOOL *stop) {
+    if ([key isEqualToString:@"googAvailableSendBandwidth"]) {
+      _availableSendBw =
+          [ARDBitrateTracker bitrateStringForBitrate:value.doubleValue];
+    } else if ([key isEqualToString:@"googAvailableReceiveBandwidth"]) {
+      _availableRecvBw =
+          [ARDBitrateTracker bitrateStringForBitrate:value.doubleValue];
+    } else if ([key isEqualToString:@"googActualEncBitrate"]) {
+      _actualEncBitrate =
+          [ARDBitrateTracker bitrateStringForBitrate:value.doubleValue];
+    } else if ([key isEqualToString:@"googTargetEncBitrate"]) {
+      _targetEncBitrate =
+          [ARDBitrateTracker bitrateStringForBitrate:value.doubleValue];
+    }
+  }];
+}
+
+- (void)parseConnectionStatsReport:(RTCLegacyStatsReport *)statsReport {
+  NSString *activeConnection = statsReport.values[@"googActiveConnection"];
+  if (![activeConnection isEqualToString:@"true"]) {
+    return;
+  }
+  [statsReport.values enumerateKeysAndObjectsUsingBlock:^(
+      NSString *key, NSString *value, BOOL *stop) {
+    if ([key isEqualToString:@"googRtt"]) {
+      _connRtt = value;
+    } else if ([key isEqualToString:@"googLocalCandidateType"]) {
+      _localCandType = value;
+    } else if ([key isEqualToString:@"googRemoteCandidateType"]) {
+      _remoteCandType = value;
+    } else if ([key isEqualToString:@"googTransportType"]) {
+      _transportType = value;
+    } else if ([key isEqualToString:@"bytesReceived"]) {
+      NSInteger byteCount = value.integerValue;
+      [_connRecvBitrateTracker updateBitrateWithCurrentByteCount:byteCount];
+      _connRecvBitrate = _connRecvBitrateTracker.bitrateString;
+    } else if ([key isEqualToString:@"bytesSent"]) {
+      NSInteger byteCount = value.integerValue;
+      [_connSendBitrateTracker updateBitrateWithCurrentByteCount:byteCount];
+      _connSendBitrate = _connSendBitrateTracker.bitrateString;
+    }
+  }];
+}
+
+- (void)parseSendSsrcStatsReport:(RTCLegacyStatsReport *)statsReport {
+  NSDictionary *values = statsReport.values;
+  if ([values objectForKey:@"googFrameRateSent"]) {
+    // Video track.
+    [self parseVideoSendStatsReport:statsReport];
+  } else if ([values objectForKey:@"audioInputLevel"]) {
+    // Audio track.
+    [self parseAudioSendStatsReport:statsReport];
+  }
+}
+
+- (void)parseAudioSendStatsReport:(RTCLegacyStatsReport *)statsReport {
+  [statsReport.values enumerateKeysAndObjectsUsingBlock:^(
+      NSString *key, NSString *value, BOOL *stop) {
+    if ([key isEqualToString:@"googCodecName"]) {
+      _audioSendCodec = value;
+    } else if ([key isEqualToString:@"bytesSent"]) {
+      NSInteger byteCount = value.integerValue;
+      [_audioSendBitrateTracker updateBitrateWithCurrentByteCount:byteCount];
+      _audioSendBitrate = _audioSendBitrateTracker.bitrateString;
+    }
+  }];
+}
+
+- (void)parseVideoSendStatsReport:(RTCLegacyStatsReport *)statsReport {
+  [statsReport.values enumerateKeysAndObjectsUsingBlock:^(
+      NSString *key, NSString *value, BOOL *stop) {
+    if ([key isEqualToString:@"googCodecName"]) {
+      _videoSendCodec = value;
+    } else if ([key isEqualToString:@"googFrameHeightInput"]) {
+      _videoInputHeight = value;
+    } else if ([key isEqualToString:@"googFrameWidthInput"]) {
+      _videoInputWidth = value;
+    } else if ([key isEqualToString:@"googFrameRateInput"]) {
+      _videoInputFps = value;
+    } else if ([key isEqualToString:@"googFrameHeightSent"]) {
+      _videoSendHeight = value;
+    } else if ([key isEqualToString:@"googFrameWidthSent"]) {
+      _videoSendWidth = value;
+    } else if ([key isEqualToString:@"googFrameRateSent"]) {
+      _videoSendFps = value;
+    } else if ([key isEqualToString:@"googAvgEncodeMs"]) {
+      _videoEncodeMs = value;
+    } else if ([key isEqualToString:@"bytesSent"]) {
+      NSInteger byteCount = value.integerValue;
+      [_videoSendBitrateTracker updateBitrateWithCurrentByteCount:byteCount];
+      _videoSendBitrate = _videoSendBitrateTracker.bitrateString;
+    }
+  }];
+}
+
+- (void)parseRecvSsrcStatsReport:(RTCLegacyStatsReport *)statsReport {
+  NSDictionary *values = statsReport.values;
+  if ([values objectForKey:@"googFrameWidthReceived"]) {
+    // Video track.
+    [self parseVideoRecvStatsReport:statsReport];
+  } else if ([values objectForKey:@"audioOutputLevel"]) {
+    // Audio track.
+    [self parseAudioRecvStatsReport:statsReport];
+  }
+}
+
+- (void)parseAudioRecvStatsReport:(RTCLegacyStatsReport *)statsReport {
+  [statsReport.values enumerateKeysAndObjectsUsingBlock:^(
+      NSString *key, NSString *value, BOOL *stop) {
+    if ([key isEqualToString:@"googCodecName"]) {
+      _audioRecvCodec = value;
+    } else if ([key isEqualToString:@"bytesReceived"]) {
+      NSInteger byteCount = value.integerValue;
+      [_audioRecvBitrateTracker updateBitrateWithCurrentByteCount:byteCount];
+      _audioRecvBitrate = _audioRecvBitrateTracker.bitrateString;
+    } else if ([key isEqualToString:@"googSpeechExpandRate"]) {
+      _audioExpandRate = value;
+    } else if ([key isEqualToString:@"googCurrentDelayMs"]) {
+      _audioCurrentDelay = value;
+    }
+  }];
+}
+
+- (void)parseVideoRecvStatsReport:(RTCLegacyStatsReport *)statsReport {
+  [statsReport.values enumerateKeysAndObjectsUsingBlock:^(
+      NSString *key, NSString *value, BOOL *stop) {
+    if ([key isEqualToString:@"googFrameHeightReceived"]) {
+      _videoRecvHeight = value;
+    } else if ([key isEqualToString:@"googFrameWidthReceived"]) {
+      _videoRecvWidth = value;
+    } else if ([key isEqualToString:@"googFrameRateReceived"]) {
+      _videoRecvFps = value;
+    } else if ([key isEqualToString:@"googFrameRateDecoded"]) {
+      _videoDecodedFps = value;
+    } else if ([key isEqualToString:@"googFrameRateOutput"]) {
+      _videoOutputFps = value;
+    } else if ([key isEqualToString:@"googDecodeMs"]) {
+      _videoDecodeMs = value;
+    } else if ([key isEqualToString:@"bytesReceived"]) {
+      NSInteger byteCount = value.integerValue;
+      [_videoRecvBitrateTracker updateBitrateWithCurrentByteCount:byteCount];
+      _videoRecvBitrate = _videoRecvBitrateTracker.bitrateString;
+    }
+  }];
+}
+
+@end
+
diff --git a/webrtc/examples/objc/AppRTCMobile/ARDTURNClient.h b/webrtc/examples/objc/AppRTCMobile/ARDTURNClient.h
new file mode 100644
index 0000000..75ccffc
--- /dev/null
+++ b/webrtc/examples/objc/AppRTCMobile/ARDTURNClient.h
@@ -0,0 +1,22 @@
+/*
+ *  Copyright 2014 The WebRTC Project Authors. All rights reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#import <Foundation/Foundation.h>
+
+@class RTCIceServer;
+
+@protocol ARDTURNClient <NSObject>
+
+// Returns TURN server urls if successful.
+- (void)requestServersWithCompletionHandler:
+    (void (^)(NSArray *turnServers,
+              NSError *error))completionHandler;
+
+@end
diff --git a/webrtc/examples/objc/AppRTCMobile/ARDWebSocketChannel.h b/webrtc/examples/objc/AppRTCMobile/ARDWebSocketChannel.h
new file mode 100644
index 0000000..ffb0b72
--- /dev/null
+++ b/webrtc/examples/objc/AppRTCMobile/ARDWebSocketChannel.h
@@ -0,0 +1,41 @@
+/*
+ *  Copyright 2014 The WebRTC Project Authors. All rights reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "ARDSignalingChannel.h"
+
+// Wraps a WebSocket connection to the AppRTC WebSocket server.
+@interface ARDWebSocketChannel : NSObject <ARDSignalingChannel>
+
+- (instancetype)initWithURL:(NSURL *)url
+                    restURL:(NSURL *)restURL
+                   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 message over the WebSocket connection if registered, otherwise POSTs to
+// the web socket server instead.
+- (void)sendMessage:(ARDSignalingMessage *)message;
+
+@end
+
+// Loopback mode is used to cause the client to connect to itself for testing.
+// A second web socket connection is established simulating the other client.
+// Any messages received are sent back to the WebSocket server after modifying
+// them as appropriate.
+@interface ARDLoopbackWebSocketChannel : ARDWebSocketChannel
+
+- (instancetype)initWithURL:(NSURL *)url restURL:(NSURL *)restURL;
+
+@end
diff --git a/webrtc/examples/objc/AppRTCMobile/ARDWebSocketChannel.m b/webrtc/examples/objc/AppRTCMobile/ARDWebSocketChannel.m
new file mode 100644
index 0000000..6f60380
--- /dev/null
+++ b/webrtc/examples/objc/AppRTCMobile/ARDWebSocketChannel.m
@@ -0,0 +1,251 @@
+/*
+ *  Copyright 2014 The WebRTC Project Authors. All rights reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#import "ARDWebSocketChannel.h"
+
+#import "WebRTC/RTCLogging.h"
+#import "SRWebSocket.h"
+
+#import "ARDSignalingMessage.h"
+#import "ARDUtilities.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<ARDSignalingChannelDelegate>)delegate {
+  if (self = [super init]) {
+    _url = url;
+    _restURL = restURL;
+    _delegate = delegate;
+    _socket = [[SRWebSocket alloc] initWithURL:url];
+    _socket.delegate = self;
+    RTCLog(@"Opening WebSocket.");
+    [_socket open];
+  }
+  return self;
+}
+
+- (void)dealloc {
+  [self disconnect];
+}
+
+- (void)setState:(ARDSignalingChannelState)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 == kARDSignalingChannelStateOpen) {
+    [self registerWithCollider];
+  }
+}
+
+- (void)sendMessage:(ARDSignalingMessage *)message {
+  NSParameterAssert(_clientId.length);
+  NSParameterAssert(_roomId.length);
+  NSData *data = [message JSONData];
+  if (_state == kARDSignalingChannelStateRegistered) {
+    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];
+    RTCLog(@"C->WSS: %@", messageString);
+    [_socket send:messageString];
+  } else {
+    NSString *dataString =
+        [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
+    RTCLog(@"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 == kARDSignalingChannelStateClosed ||
+      _state == kARDSignalingChannelStateError) {
+    return;
+  }
+  [_socket close];
+  RTCLog(@"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 {
+  RTCLog(@"WebSocket connection opened.");
+  self.state = kARDSignalingChannelStateOpen;
+  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]]) {
+    RTCLogError(@"Unexpected message: %@", jsonObject);
+    return;
+  }
+  NSDictionary *wssMessage = jsonObject;
+  NSString *errorString = wssMessage[kARDWSSMessageErrorKey];
+  if (errorString.length) {
+    RTCLogError(@"WSS error: %@", errorString);
+    return;
+  }
+  NSString *payload = wssMessage[kARDWSSMessagePayloadKey];
+  ARDSignalingMessage *signalingMessage =
+      [ARDSignalingMessage messageFromJSONString:payload];
+  RTCLog(@"WSS->C: %@", payload);
+  [_delegate channel:self didReceiveMessage:signalingMessage];
+}
+
+- (void)webSocket:(SRWebSocket *)webSocket didFailWithError:(NSError *)error {
+  RTCLogError(@"WebSocket error: %@", error);
+  self.state = kARDSignalingChannelStateError;
+}
+
+- (void)webSocket:(SRWebSocket *)webSocket
+    didCloseWithCode:(NSInteger)code
+              reason:(NSString *)reason
+            wasClean:(BOOL)wasClean {
+  RTCLog(@"WebSocket closed with code: %ld reason:%@ wasClean:%d",
+      (long)code, reason, wasClean);
+  NSParameterAssert(_state != kARDSignalingChannelStateError);
+  self.state = kARDSignalingChannelStateClosed;
+}
+
+#pragma mark - Private
+
+- (void)registerWithCollider {
+  if (_state == kARDSignalingChannelStateRegistered) {
+    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];
+  RTCLog(@"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 = kARDSignalingChannelStateRegistered;
+}
+
+@end
+
+@interface ARDLoopbackWebSocketChannel () <ARDSignalingChannelDelegate>
+@end
+
+@implementation ARDLoopbackWebSocketChannel
+
+- (instancetype)initWithURL:(NSURL *)url restURL:(NSURL *)restURL {
+  return [super initWithURL:url restURL:restURL delegate:self];
+}
+
+#pragma mark - ARDSignalingChannelDelegate
+
+- (void)channel:(id<ARDSignalingChannel>)channel
+    didReceiveMessage:(ARDSignalingMessage *)message {
+  switch (message.type) {
+    case kARDSignalingMessageTypeOffer: {
+      // Change message to answer, send back to server.
+      ARDSessionDescriptionMessage *sdpMessage =
+          (ARDSessionDescriptionMessage *)message;
+      RTCSessionDescription *description = sdpMessage.sessionDescription;
+      NSString *dsc = description.sdp;
+      dsc = [dsc stringByReplacingOccurrencesOfString:@"offer"
+                                           withString:@"answer"];
+      RTCSessionDescription *answerDescription =
+          [[RTCSessionDescription alloc] initWithType:RTCSdpTypeAnswer sdp:dsc];
+      ARDSignalingMessage *answer =
+          [[ARDSessionDescriptionMessage alloc]
+               initWithDescription:answerDescription];
+      [self sendMessage:answer];
+      break;
+    }
+    case kARDSignalingMessageTypeAnswer:
+      // Should not receive answer in loopback scenario.
+      break;
+    case kARDSignalingMessageTypeCandidate:
+    case kARDSignalingMessageTypeCandidateRemoval:
+      // Send back to server.
+      [self sendMessage:message];
+      break;
+    case kARDSignalingMessageTypeBye:
+      // Nothing to do.
+      return;
+  }
+}
+
+- (void)channel:(id<ARDSignalingChannel>)channel
+    didChangeState:(ARDSignalingChannelState)state {
+}
+
+@end
+
diff --git a/webrtc/examples/objc/AppRTCMobile/RTCIceCandidate+JSON.h b/webrtc/examples/objc/AppRTCMobile/RTCIceCandidate+JSON.h
new file mode 100644
index 0000000..d2e5e33
--- /dev/null
+++ b/webrtc/examples/objc/AppRTCMobile/RTCIceCandidate+JSON.h
@@ -0,0 +1,22 @@
+/*
+ *  Copyright 2014 The WebRTC Project Authors. All rights reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#import "WebRTC/RTCIceCandidate.h"
+
+@interface RTCIceCandidate (JSON)
+
++ (RTCIceCandidate *)candidateFromJSONDictionary:(NSDictionary *)dictionary;
++ (NSArray<RTCIceCandidate *> *)candidatesFromJSONDictionary:
+    (NSDictionary *)dictionary;
++ (NSData *)JSONDataForIceCandidates:(NSArray<RTCIceCandidate *> *)candidates
+                            withType:(NSString *)typeValue;
+- (NSData *)JSONData;
+
+@end
diff --git a/webrtc/examples/objc/AppRTCMobile/RTCIceCandidate+JSON.m b/webrtc/examples/objc/AppRTCMobile/RTCIceCandidate+JSON.m
new file mode 100644
index 0000000..b1be7fb
--- /dev/null
+++ b/webrtc/examples/objc/AppRTCMobile/RTCIceCandidate+JSON.m
@@ -0,0 +1,100 @@
+/*
+ *  Copyright 2014 The WebRTC Project Authors. All rights reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#import "RTCIceCandidate+JSON.h"
+
+#import "WebRTC/RTCLogging.h"
+
+static NSString const *kRTCICECandidateTypeKey = @"type";
+static NSString const *kRTCICECandidateTypeValue = @"candidate";
+static NSString const *kRTCICECandidateMidKey = @"id";
+static NSString const *kRTCICECandidateMLineIndexKey = @"label";
+static NSString const *kRTCICECandidateSdpKey = @"candidate";
+static NSString const *kRTCICECandidatesTypeKey = @"candidates";
+
+
+@implementation RTCIceCandidate (JSON)
+
++ (RTCIceCandidate *)candidateFromJSONDictionary:(NSDictionary *)dictionary {
+  NSString *mid = dictionary[kRTCICECandidateMidKey];
+  NSString *sdp = dictionary[kRTCICECandidateSdpKey];
+  NSNumber *num = dictionary[kRTCICECandidateMLineIndexKey];
+  NSInteger mLineIndex = [num integerValue];
+  return [[RTCIceCandidate alloc] initWithSdp:sdp
+                                sdpMLineIndex:mLineIndex
+                                       sdpMid:mid];
+}
+
++ (NSData *)JSONDataForIceCandidates:(NSArray<RTCIceCandidate *> *)candidates
+                            withType:(NSString *)typeValue {
+  NSMutableArray *jsonCandidates =
+      [NSMutableArray arrayWithCapacity:candidates.count];
+  for (RTCIceCandidate *candidate in candidates) {
+    NSDictionary *jsonCandidate = [candidate JSONDictionary];
+    [jsonCandidates addObject:jsonCandidate];
+  }
+  NSDictionary *json = @{
+    kRTCICECandidateTypeKey : typeValue,
+    kRTCICECandidatesTypeKey : jsonCandidates
+  };
+  NSError *error = nil;
+  NSData *data =
+      [NSJSONSerialization dataWithJSONObject:json
+                                      options:NSJSONWritingPrettyPrinted
+                                        error:&error];
+  if (error) {
+    RTCLogError(@"Error serializing JSON: %@", error);
+    return nil;
+  }
+  return data;
+}
+
++ (NSArray<RTCIceCandidate *> *)candidatesFromJSONDictionary:
+    (NSDictionary *)dictionary {
+  NSArray *jsonCandidates = dictionary[kRTCICECandidatesTypeKey];
+  NSMutableArray<RTCIceCandidate *> *candidates =
+      [NSMutableArray arrayWithCapacity:jsonCandidates.count];
+  for (NSDictionary *jsonCandidate in jsonCandidates) {
+    RTCIceCandidate *candidate =
+        [RTCIceCandidate candidateFromJSONDictionary:jsonCandidate];
+    [candidates addObject:candidate];
+  }
+  return candidates;
+}
+
+- (NSData *)JSONData {
+  NSDictionary *json = @{
+    kRTCICECandidateTypeKey : kRTCICECandidateTypeValue,
+    kRTCICECandidateMLineIndexKey : @(self.sdpMLineIndex),
+    kRTCICECandidateMidKey : self.sdpMid,
+    kRTCICECandidateSdpKey : self.sdp
+  };
+  NSError *error = nil;
+  NSData *data =
+      [NSJSONSerialization dataWithJSONObject:json
+                                      options:NSJSONWritingPrettyPrinted
+                                        error:&error];
+  if (error) {
+    RTCLogError(@"Error serializing JSON: %@", error);
+    return nil;
+  }
+  return data;
+}
+
+- (NSDictionary *)JSONDictionary{
+  NSDictionary *json = @{
+    kRTCICECandidateMLineIndexKey : @(self.sdpMLineIndex),
+    kRTCICECandidateMidKey : self.sdpMid,
+    kRTCICECandidateSdpKey : self.sdp
+  };
+  return json;
+}
+
+@end
diff --git a/webrtc/examples/objc/AppRTCMobile/RTCIceServer+JSON.h b/webrtc/examples/objc/AppRTCMobile/RTCIceServer+JSON.h
new file mode 100644
index 0000000..69fb432
--- /dev/null
+++ b/webrtc/examples/objc/AppRTCMobile/RTCIceServer+JSON.h
@@ -0,0 +1,19 @@
+/*
+ *  Copyright 2014 The WebRTC Project Authors. All rights reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#import "WebRTC/RTCIceServer.h"
+
+@interface RTCIceServer (JSON)
+
++ (RTCIceServer *)serverFromJSONDictionary:(NSDictionary *)dictionary;
+// CEOD provides different JSON, and this parses that.
++ (RTCIceServer *)serverFromCEODJSONDictionary:(NSDictionary *)dictionary;
+
+@end
diff --git a/webrtc/examples/objc/AppRTCMobile/RTCIceServer+JSON.m b/webrtc/examples/objc/AppRTCMobile/RTCIceServer+JSON.m
new file mode 100644
index 0000000..0bc31bb
--- /dev/null
+++ b/webrtc/examples/objc/AppRTCMobile/RTCIceServer+JSON.m
@@ -0,0 +1,43 @@
+/*
+ *  Copyright 2014 The WebRTC Project Authors. All rights reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#import "RTCIceServer+JSON.h"
+
+static NSString const *kRTCICEServerUsernameKey = @"username";
+static NSString const *kRTCICEServerPasswordKey = @"password";
+static NSString const *kRTCICEServerUrisKey = @"uris";
+static NSString const *kRTCICEServerUrlKey = @"urls";
+static NSString const *kRTCICEServerCredentialKey = @"credential";
+
+@implementation RTCIceServer (JSON)
+
++ (RTCIceServer *)serverFromJSONDictionary:(NSDictionary *)dictionary {
+  NSString *url = dictionary[kRTCICEServerUrlKey];
+  NSString *username = dictionary[kRTCICEServerUsernameKey];
+  NSString *credential = dictionary[kRTCICEServerCredentialKey];
+  username = username ? username : @"";
+  credential = credential ? credential : @"";
+  return [[RTCIceServer alloc] initWithURLStrings:@[url]
+                                         username:username
+                                       credential:credential];
+}
+
++ (RTCIceServer *)serverFromCEODJSONDictionary:(NSDictionary *)dictionary {
+  NSString *username = dictionary[kRTCICEServerUsernameKey];
+  NSString *password = dictionary[kRTCICEServerPasswordKey];
+  NSArray *uris = dictionary[kRTCICEServerUrisKey];
+  RTCIceServer *server =
+      [[RTCIceServer alloc] initWithURLStrings:uris
+                                      username:username
+                                    credential:password];
+  return server;
+}
+
+@end
diff --git a/webrtc/examples/objc/AppRTCMobile/RTCMediaConstraints+JSON.h b/webrtc/examples/objc/AppRTCMobile/RTCMediaConstraints+JSON.h
new file mode 100644
index 0000000..74f89a9
--- /dev/null
+++ b/webrtc/examples/objc/AppRTCMobile/RTCMediaConstraints+JSON.h
@@ -0,0 +1,19 @@
+/*
+ *  Copyright 2014 The WebRTC Project Authors. All rights reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#import "WebRTC/RTCMediaConstraints.h"
+
+@interface RTCMediaConstraints (JSON)
+
++ (RTCMediaConstraints *)constraintsFromJSONDictionary:
+    (NSDictionary *)dictionary;
+
+@end
+
diff --git a/webrtc/examples/objc/AppRTCMobile/RTCMediaConstraints+JSON.m b/webrtc/examples/objc/AppRTCMobile/RTCMediaConstraints+JSON.m
new file mode 100644
index 0000000..c903735
--- /dev/null
+++ b/webrtc/examples/objc/AppRTCMobile/RTCMediaConstraints+JSON.m
@@ -0,0 +1,34 @@
+/*
+ *  Copyright 2014 The WebRTC Project Authors. All rights reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#import "RTCMediaConstraints+JSON.h"
+
+static NSString const *kRTCMediaConstraintsMandatoryKey = @"mandatory";
+
+@implementation RTCMediaConstraints (JSON)
+
++ (RTCMediaConstraints *)constraintsFromJSONDictionary:
+    (NSDictionary *)dictionary {
+  NSDictionary *mandatory = dictionary[kRTCMediaConstraintsMandatoryKey];
+  NSMutableDictionary *mandatoryContraints =
+      [NSMutableDictionary dictionaryWithCapacity:[mandatory count]];
+  [mandatory enumerateKeysAndObjectsUsingBlock:^(
+      id key, id obj, BOOL *stop) {
+    mandatoryContraints[key] = obj;
+  }];
+  // TODO(tkchin): figure out json formats for optional constraints.
+  RTCMediaConstraints *constraints =
+      [[RTCMediaConstraints alloc]
+          initWithMandatoryConstraints:mandatoryContraints
+                   optionalConstraints:nil];
+  return constraints;
+}
+
+@end
diff --git a/webrtc/examples/objc/AppRTCMobile/RTCSessionDescription+JSON.h b/webrtc/examples/objc/AppRTCMobile/RTCSessionDescription+JSON.h
new file mode 100644
index 0000000..cccff9a
--- /dev/null
+++ b/webrtc/examples/objc/AppRTCMobile/RTCSessionDescription+JSON.h
@@ -0,0 +1,19 @@
+/*
+ *  Copyright 2014 The WebRTC Project Authors. All rights reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#import "WebRTC/RTCSessionDescription.h"
+
+@interface RTCSessionDescription (JSON)
+
++ (RTCSessionDescription *)descriptionFromJSONDictionary:
+    (NSDictionary *)dictionary;
+- (NSData *)JSONData;
+
+@end
diff --git a/webrtc/examples/objc/AppRTCMobile/RTCSessionDescription+JSON.m b/webrtc/examples/objc/AppRTCMobile/RTCSessionDescription+JSON.m
new file mode 100644
index 0000000..a6059f7
--- /dev/null
+++ b/webrtc/examples/objc/AppRTCMobile/RTCSessionDescription+JSON.m
@@ -0,0 +1,35 @@
+/*
+ *  Copyright 2014 The WebRTC Project Authors. All rights reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#import "RTCSessionDescription+JSON.h"
+
+static NSString const *kRTCSessionDescriptionTypeKey = @"type";
+static NSString const *kRTCSessionDescriptionSdpKey = @"sdp";
+
+@implementation RTCSessionDescription (JSON)
+
++ (RTCSessionDescription *)descriptionFromJSONDictionary:
+    (NSDictionary *)dictionary {
+  NSString *typeString = dictionary[kRTCSessionDescriptionTypeKey];
+  RTCSdpType type = [[self class] typeForString:typeString];
+  NSString *sdp = dictionary[kRTCSessionDescriptionSdpKey];
+  return [[RTCSessionDescription alloc] initWithType:type sdp:sdp];
+}
+
+- (NSData *)JSONData {
+  NSString *type = [[self class] stringForType:self.type];
+  NSDictionary *json = @{
+    kRTCSessionDescriptionTypeKey : type,
+    kRTCSessionDescriptionSdpKey : self.sdp
+  };
+  return [NSJSONSerialization dataWithJSONObject:json options:0 error:nil];
+}
+
+@end
diff --git a/webrtc/examples/objc/AppRTCMobile/common/ARDUtilities.h b/webrtc/examples/objc/AppRTCMobile/common/ARDUtilities.h
new file mode 100644
index 0000000..8a5c126
--- /dev/null
+++ b/webrtc/examples/objc/AppRTCMobile/common/ARDUtilities.h
@@ -0,0 +1,38 @@
+/*
+ *  Copyright 2014 The WebRTC Project Authors. All rights reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#import <Foundation/Foundation.h>
+
+@interface NSDictionary (ARDUtilites)
+
+// Creates a dictionary with the keys and values in the JSON object.
++ (NSDictionary *)dictionaryWithJSONString:(NSString *)jsonString;
++ (NSDictionary *)dictionaryWithJSONData:(NSData *)jsonData;
+
+@end
+
+@interface NSURLConnection (ARDUtilities)
+
+// Issues an asynchronous request that calls back on main queue.
++ (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
+
+NSInteger ARDGetCpuUsagePercentage();
+
diff --git a/webrtc/examples/objc/AppRTCMobile/common/ARDUtilities.m b/webrtc/examples/objc/AppRTCMobile/common/ARDUtilities.m
new file mode 100644
index 0000000..c9d029f
--- /dev/null
+++ b/webrtc/examples/objc/AppRTCMobile/common/ARDUtilities.m
@@ -0,0 +1,128 @@
+/*
+ *  Copyright 2014 The WebRTC Project Authors. All rights reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#import "ARDUtilities.h"
+
+#import <mach/mach.h>
+
+#import "WebRTC/RTCLogging.h"
+
+@implementation NSDictionary (ARDUtilites)
+
++ (NSDictionary *)dictionaryWithJSONString:(NSString *)jsonString {
+  NSParameterAssert(jsonString.length > 0);
+  NSData *data = [jsonString dataUsingEncoding:NSUTF8StringEncoding];
+  NSError *error = nil;
+  NSDictionary *dict =
+      [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
+  if (error) {
+    RTCLogError(@"Error parsing JSON: %@", error.localizedDescription);
+  }
+  return dict;
+}
+
++ (NSDictionary *)dictionaryWithJSONData:(NSData *)jsonData {
+  NSError *error = nil;
+  NSDictionary *dict =
+      [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error];
+  if (error) {
+    RTCLogError(@"Error parsing JSON: %@", error.localizedDescription);
+  }
+  return dict;
+}
+
+@end
+
+@implementation NSURLConnection (ARDUtilities)
+
++ (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) {
+    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) {
+      RTCLogError(@"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;
+      RTCLogError(@"Received bad response: %@", serverResponse);
+      if (completionHandler) {
+        completionHandler(NO, data);
+      }
+      return;
+    }
+    if (completionHandler) {
+      completionHandler(YES, data);
+    }
+  }];
+}
+
+@end
+
+NSInteger ARDGetCpuUsagePercentage() {
+  // Create an array of thread ports for the current task.
+  const task_t task = mach_task_self();
+  thread_act_array_t thread_array;
+  mach_msg_type_number_t thread_count;
+  if (task_threads(task, &thread_array, &thread_count) != KERN_SUCCESS) {
+    return -1;
+  }
+
+  // Sum cpu usage from all threads.
+  float cpu_usage_percentage = 0;
+  thread_basic_info_data_t thread_info_data = {};
+  mach_msg_type_number_t thread_info_count;
+  for (size_t i = 0; i < thread_count; ++i) {
+    thread_info_count = THREAD_BASIC_INFO_COUNT;
+    kern_return_t ret = thread_info(thread_array[i],
+                                    THREAD_BASIC_INFO,
+                                    (thread_info_t)&thread_info_data,
+                                    &thread_info_count);
+    if (ret == KERN_SUCCESS) {
+      cpu_usage_percentage +=
+          100.f * (float)thread_info_data.cpu_usage / TH_USAGE_SCALE;
+    }
+  }
+
+  // Dealloc the created array.
+  vm_deallocate(task, (vm_address_t)thread_array,
+                sizeof(thread_act_t) * thread_count);
+  return lroundf(cpu_usage_percentage);
+}
diff --git a/webrtc/examples/objc/AppRTCMobile/ios/ARDAppDelegate.h b/webrtc/examples/objc/AppRTCMobile/ios/ARDAppDelegate.h
new file mode 100644
index 0000000..7eafff8
--- /dev/null
+++ b/webrtc/examples/objc/AppRTCMobile/ios/ARDAppDelegate.h
@@ -0,0 +1,17 @@
+/*
+ *  Copyright 2013 The WebRTC Project Authors. All rights reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#import <UIKit/UIKit.h>
+
+// The main application class of the AppRTCMobile iOS app demonstrating
+// interoperability between the Objective C implementation of PeerConnection
+// and the appr.tc demo webapp.
+@interface ARDAppDelegate : NSObject <UIApplicationDelegate>
+@end
diff --git a/webrtc/examples/objc/AppRTCMobile/ios/ARDAppDelegate.m b/webrtc/examples/objc/AppRTCMobile/ios/ARDAppDelegate.m
new file mode 100644
index 0000000..8f19262
--- /dev/null
+++ b/webrtc/examples/objc/AppRTCMobile/ios/ARDAppDelegate.m
@@ -0,0 +1,51 @@
+/*
+ *  Copyright 2013 The WebRTC Project Authors. All rights reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#import "ARDAppDelegate.h"
+
+#import "WebRTC/RTCFieldTrials.h"
+#import "WebRTC/RTCLogging.h"
+#import "WebRTC/RTCSSLAdapter.h"
+#import "WebRTC/RTCTracing.h"
+
+#import "ARDMainViewController.h"
+
+@implementation ARDAppDelegate {
+  UIWindow *_window;
+}
+
+#pragma mark - UIApplicationDelegate methods
+
+- (BOOL)application:(UIApplication *)application
+    didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
+  RTCInitFieldTrials(RTCFieldTrialOptionsSendSideBwe);
+  RTCInitializeSSL();
+  RTCSetupInternalTracer();
+  _window =  [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
+  [_window makeKeyAndVisible];
+  ARDMainViewController *viewController = [[ARDMainViewController alloc] init];
+  _window.rootViewController = viewController;
+
+#if defined(NDEBUG)
+  // In debug builds the default level is LS_INFO and in non-debug builds it is
+  // disabled. Continue to log to console in non-debug builds, but only
+  // warnings and errors.
+  RTCSetMinDebugLogLevel(RTCLoggingSeverityWarning);
+#endif
+
+  return YES;
+}
+
+- (void)applicationWillTerminate:(UIApplication *)application {
+  RTCShutdownInternalTracer();
+  RTCCleanupSSL();
+}
+
+@end
diff --git a/webrtc/examples/objc/AppRTCMobile/ios/ARDMainView.h b/webrtc/examples/objc/AppRTCMobile/ios/ARDMainView.h
new file mode 100644
index 0000000..1f2497a
--- /dev/null
+++ b/webrtc/examples/objc/AppRTCMobile/ios/ARDMainView.h
@@ -0,0 +1,37 @@
+/*
+ *  Copyright 2015 The WebRTC Project Authors. All rights reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#import <UIKit/UIKit.h>
+
+@class ARDMainView;
+
+@protocol ARDMainViewDelegate <NSObject>
+
+- (void)mainView:(ARDMainView *)mainView
+             didInputRoom:(NSString *)room
+               isLoopback:(BOOL)isLoopback
+              isAudioOnly:(BOOL)isAudioOnly
+        shouldMakeAecDump:(BOOL)shouldMakeAecDump
+    shouldUseLevelControl:(BOOL)shouldUseLevelControl
+           useManualAudio:(BOOL)useManualAudio;
+
+- (void)mainViewDidToggleAudioLoop:(ARDMainView *)mainView;
+
+@end
+
+// The main view of AppRTCMobile. It contains an input field for entering a room
+// name on apprtc to connect to.
+@interface ARDMainView : UIView
+
+@property(nonatomic, weak) id<ARDMainViewDelegate> delegate;
+// Updates the audio loop button as needed.
+@property(nonatomic, assign) BOOL isAudioLoopPlaying;
+
+@end
diff --git a/webrtc/examples/objc/AppRTCMobile/ios/ARDMainView.m b/webrtc/examples/objc/AppRTCMobile/ios/ARDMainView.m
new file mode 100644
index 0000000..9ebee5c
--- /dev/null
+++ b/webrtc/examples/objc/AppRTCMobile/ios/ARDMainView.m
@@ -0,0 +1,392 @@
+/*
+ *  Copyright 2015 The WebRTC Project Authors. All rights reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#import "ARDMainView.h"
+
+#import "UIImage+ARDUtilities.h"
+
+// TODO(tkchin): retrieve status bar height dynamically.
+static CGFloat const kStatusBarHeight = 20;
+
+static CGFloat const kRoomTextButtonSize = 40;
+static CGFloat const kRoomTextFieldHeight = 40;
+static CGFloat const kRoomTextFieldMargin = 8;
+static CGFloat const kCallControlMargin = 8;
+
+// Helper view that contains a text field and a clear button.
+@interface ARDRoomTextField : UIView <UITextFieldDelegate>
+@property(nonatomic, readonly) NSString *roomText;
+@end
+
+@implementation ARDRoomTextField {
+  UITextField *_roomText;
+  UIButton *_clearButton;
+}
+
+- (instancetype)initWithFrame:(CGRect)frame {
+  if (self = [super initWithFrame:frame]) {
+    _roomText = [[UITextField alloc] initWithFrame:CGRectZero];
+    _roomText.borderStyle = UITextBorderStyleNone;
+    _roomText.font = [UIFont fontWithName:@"Roboto" size:12];
+    _roomText.placeholder = @"Room name";
+    _roomText.autocorrectionType = UITextAutocorrectionTypeNo;
+    _roomText.autocapitalizationType = UITextAutocapitalizationTypeNone;
+    _roomText.delegate = self;
+    [_roomText addTarget:self
+                  action:@selector(textFieldDidChange:)
+        forControlEvents:UIControlEventEditingChanged];
+    [self addSubview:_roomText];
+
+    _clearButton = [UIButton buttonWithType:UIButtonTypeCustom];
+    UIImage *image = [UIImage imageForName:@"ic_clear_black_24dp.png"
+                                     color:[UIColor colorWithWhite:0 alpha:.4]];
+
+    [_clearButton setImage:image forState:UIControlStateNormal];
+    [_clearButton addTarget:self
+                      action:@selector(onClear:)
+            forControlEvents:UIControlEventTouchUpInside];
+    _clearButton.hidden = YES;
+    [self addSubview:_clearButton];
+
+    // Give rounded corners and a light gray border.
+    self.layer.borderWidth = 1;
+    self.layer.borderColor = [[UIColor lightGrayColor] CGColor];
+    self.layer.cornerRadius = 2;
+  }
+  return self;
+}
+
+- (void)layoutSubviews {
+  CGRect bounds = self.bounds;
+  _clearButton.frame = CGRectMake(CGRectGetMaxX(bounds) - kRoomTextButtonSize,
+                                  CGRectGetMinY(bounds),
+                                  kRoomTextButtonSize,
+                                  kRoomTextButtonSize);
+  _roomText.frame = CGRectMake(
+      CGRectGetMinX(bounds) + kRoomTextFieldMargin,
+      CGRectGetMinY(bounds),
+      CGRectGetMinX(_clearButton.frame) - CGRectGetMinX(bounds) -
+          kRoomTextFieldMargin,
+      kRoomTextFieldHeight);
+}
+
+- (CGSize)sizeThatFits:(CGSize)size {
+  size.height = kRoomTextFieldHeight;
+  return size;
+}
+
+- (NSString *)roomText {
+  return _roomText.text;
+}
+
+#pragma mark - UITextFieldDelegate
+
+- (BOOL)textFieldShouldReturn:(UITextField *)textField {
+  // There is no other control that can take focus, so manually resign focus
+  // when return (Join) is pressed to trigger |textFieldDidEndEditing|.
+  [textField resignFirstResponder];
+  return YES;
+}
+
+#pragma mark - Private
+
+- (void)textFieldDidChange:(id)sender {
+  [self updateClearButton];
+}
+
+- (void)onClear:(id)sender {
+  _roomText.text = @"";
+  [self updateClearButton];
+  [_roomText resignFirstResponder];
+}
+
+- (void)updateClearButton {
+  _clearButton.hidden = _roomText.text.length == 0;
+}
+
+@end
+
+@implementation ARDMainView {
+  UILabel *_appLabel;
+  ARDRoomTextField *_roomText;
+  UILabel *_callOptionsLabel;
+  UISwitch *_audioOnlySwitch;
+  UILabel *_audioOnlyLabel;
+  UISwitch *_aecdumpSwitch;
+  UILabel *_aecdumpLabel;
+  UISwitch *_levelControlSwitch;
+  UILabel *_levelControlLabel;
+  UISwitch *_loopbackSwitch;
+  UILabel *_loopbackLabel;
+  UISwitch *_useManualAudioSwitch;
+  UILabel *_useManualAudioLabel;
+  UIButton *_startCallButton;
+  UIButton *_audioLoopButton;
+}
+
+@synthesize delegate = _delegate;
+@synthesize isAudioLoopPlaying = _isAudioLoopPlaying;
+
+- (instancetype)initWithFrame:(CGRect)frame {
+  if (self = [super initWithFrame:frame]) {
+    _appLabel = [[UILabel alloc] initWithFrame:CGRectZero];
+    _appLabel.text = @"AppRTCMobile";
+    _appLabel.font = [UIFont fontWithName:@"Roboto" size:34];
+    _appLabel.textColor = [UIColor colorWithWhite:0 alpha:.2];
+    [_appLabel sizeToFit];
+    [self addSubview:_appLabel];
+
+    _roomText = [[ARDRoomTextField alloc] initWithFrame:CGRectZero];
+    [self addSubview:_roomText];
+
+    UIFont *controlFont = [UIFont fontWithName:@"Roboto" size:20];
+    UIColor *controlFontColor = [UIColor colorWithWhite:0 alpha:.6];
+
+    _callOptionsLabel = [[UILabel alloc] initWithFrame:CGRectZero];
+    _callOptionsLabel.text = @"Call Options";
+    _callOptionsLabel.font = controlFont;
+    _callOptionsLabel.textColor = controlFontColor;
+    [_callOptionsLabel sizeToFit];
+    [self addSubview:_callOptionsLabel];
+
+    _audioOnlySwitch = [[UISwitch alloc] initWithFrame:CGRectZero];
+    [_audioOnlySwitch sizeToFit];
+    [self addSubview:_audioOnlySwitch];
+
+    _audioOnlyLabel = [[UILabel alloc] initWithFrame:CGRectZero];
+    _audioOnlyLabel.text = @"Audio only";
+    _audioOnlyLabel.font = controlFont;
+    _audioOnlyLabel.textColor = controlFontColor;
+    [_audioOnlyLabel sizeToFit];
+    [self addSubview:_audioOnlyLabel];
+
+    _loopbackSwitch = [[UISwitch alloc] initWithFrame:CGRectZero];
+    [_loopbackSwitch sizeToFit];
+    [self addSubview:_loopbackSwitch];
+
+    _loopbackLabel = [[UILabel alloc] initWithFrame:CGRectZero];
+    _loopbackLabel.text = @"Loopback mode";
+    _loopbackLabel.font = controlFont;
+    _loopbackLabel.textColor = controlFontColor;
+    [_loopbackLabel sizeToFit];
+    [self addSubview:_loopbackLabel];
+
+    _aecdumpSwitch = [[UISwitch alloc] initWithFrame:CGRectZero];
+    [_aecdumpSwitch sizeToFit];
+    [self addSubview:_aecdumpSwitch];
+
+    _aecdumpLabel = [[UILabel alloc] initWithFrame:CGRectZero];
+    _aecdumpLabel.text = @"Create AecDump";
+    _aecdumpLabel.font = controlFont;
+    _aecdumpLabel.textColor = controlFontColor;
+    [_aecdumpLabel sizeToFit];
+    [self addSubview:_aecdumpLabel];
+
+    _levelControlSwitch = [[UISwitch alloc] initWithFrame:CGRectZero];
+    [_levelControlSwitch sizeToFit];
+    [self addSubview:_levelControlSwitch];
+
+    _levelControlLabel = [[UILabel alloc] initWithFrame:CGRectZero];
+    _levelControlLabel.text = @"Use level controller";
+    _levelControlLabel.font = controlFont;
+    _levelControlLabel.textColor = controlFontColor;
+    [_levelControlLabel sizeToFit];
+    [self addSubview:_levelControlLabel];
+
+    _useManualAudioSwitch = [[UISwitch alloc] initWithFrame:CGRectZero];
+    [_useManualAudioSwitch sizeToFit];
+    _useManualAudioSwitch.on = YES;
+    [self addSubview:_useManualAudioSwitch];
+
+    _useManualAudioLabel = [[UILabel alloc] initWithFrame:CGRectZero];
+    _useManualAudioLabel.text = @"Use manual audio config";
+    _useManualAudioLabel.font = controlFont;
+    _useManualAudioLabel.textColor = controlFontColor;
+    [_useManualAudioLabel sizeToFit];
+    [self addSubview:_useManualAudioLabel];
+
+    _startCallButton = [UIButton buttonWithType:UIButtonTypeSystem];
+    _startCallButton.backgroundColor = [UIColor blueColor];
+    _startCallButton.layer.cornerRadius = 10;
+    _startCallButton.clipsToBounds = YES;
+    _startCallButton.contentEdgeInsets = UIEdgeInsetsMake(5, 10, 5, 10);
+    [_startCallButton setTitle:@"Start call"
+                      forState:UIControlStateNormal];
+    _startCallButton.titleLabel.font = controlFont;
+    [_startCallButton setTitleColor:[UIColor whiteColor]
+                           forState:UIControlStateNormal];
+    [_startCallButton setTitleColor:[UIColor lightGrayColor]
+                           forState:UIControlStateSelected];
+    [_startCallButton sizeToFit];
+    [_startCallButton addTarget:self
+                         action:@selector(onStartCall:)
+               forControlEvents:UIControlEventTouchUpInside];
+    [self addSubview:_startCallButton];
+
+    // Used to test what happens to sounds when calls are in progress.
+    _audioLoopButton = [UIButton buttonWithType:UIButtonTypeSystem];
+    _audioLoopButton.layer.cornerRadius = 10;
+    _audioLoopButton.clipsToBounds = YES;
+    _audioLoopButton.contentEdgeInsets = UIEdgeInsetsMake(5, 10, 5, 10);
+    _audioLoopButton.titleLabel.font = controlFont;
+    [_audioLoopButton setTitleColor:[UIColor whiteColor]
+                           forState:UIControlStateNormal];
+    [_audioLoopButton setTitleColor:[UIColor lightGrayColor]
+                           forState:UIControlStateSelected];
+    [self updateAudioLoopButton];
+    [_audioLoopButton addTarget:self
+                         action:@selector(onToggleAudioLoop:)
+               forControlEvents:UIControlEventTouchUpInside];
+    [self addSubview:_audioLoopButton];
+
+    self.backgroundColor = [UIColor whiteColor];
+  }
+  return self;
+}
+
+- (void)setIsAudioLoopPlaying:(BOOL)isAudioLoopPlaying {
+  if (_isAudioLoopPlaying == isAudioLoopPlaying) {
+    return;
+  }
+  _isAudioLoopPlaying = isAudioLoopPlaying;
+  [self updateAudioLoopButton];
+}
+
+- (void)layoutSubviews {
+  CGRect bounds = self.bounds;
+  CGFloat roomTextWidth = bounds.size.width - 2 * kRoomTextFieldMargin;
+  CGFloat roomTextHeight = [_roomText sizeThatFits:bounds.size].height;
+  _roomText.frame = CGRectMake(kRoomTextFieldMargin,
+                               kStatusBarHeight + kRoomTextFieldMargin,
+                               roomTextWidth,
+                               roomTextHeight);
+  _appLabel.center = CGPointMake(CGRectGetMidX(bounds), CGRectGetMidY(bounds));
+
+  CGFloat callOptionsLabelTop =
+      CGRectGetMaxY(_roomText.frame) + kCallControlMargin * 4;
+  _callOptionsLabel.frame = CGRectMake(kCallControlMargin,
+                                       callOptionsLabelTop,
+                                       _callOptionsLabel.frame.size.width,
+                                       _callOptionsLabel.frame.size.height);
+
+  CGFloat audioOnlyTop =
+      CGRectGetMaxY(_callOptionsLabel.frame) + kCallControlMargin * 2;
+  CGRect audioOnlyRect = CGRectMake(kCallControlMargin * 3,
+                                    audioOnlyTop,
+                                    _audioOnlySwitch.frame.size.width,
+                                    _audioOnlySwitch.frame.size.height);
+  _audioOnlySwitch.frame = audioOnlyRect;
+  CGFloat audioOnlyLabelCenterX = CGRectGetMaxX(audioOnlyRect) +
+      kCallControlMargin + _audioOnlyLabel.frame.size.width / 2;
+  _audioOnlyLabel.center = CGPointMake(audioOnlyLabelCenterX,
+                                       CGRectGetMidY(audioOnlyRect));
+
+  CGFloat loopbackModeTop =
+      CGRectGetMaxY(_audioOnlySwitch.frame) + kCallControlMargin;
+  CGRect loopbackModeRect = CGRectMake(kCallControlMargin * 3,
+                                       loopbackModeTop,
+                                       _loopbackSwitch.frame.size.width,
+                                       _loopbackSwitch.frame.size.height);
+  _loopbackSwitch.frame = loopbackModeRect;
+  CGFloat loopbackModeLabelCenterX = CGRectGetMaxX(loopbackModeRect) +
+      kCallControlMargin + _loopbackLabel.frame.size.width / 2;
+  _loopbackLabel.center = CGPointMake(loopbackModeLabelCenterX,
+                                      CGRectGetMidY(loopbackModeRect));
+
+  CGFloat aecdumpModeTop =
+      CGRectGetMaxY(_loopbackSwitch.frame) + kCallControlMargin;
+  CGRect aecdumpModeRect = CGRectMake(kCallControlMargin * 3,
+                                      aecdumpModeTop,
+                                      _aecdumpSwitch.frame.size.width,
+                                      _aecdumpSwitch.frame.size.height);
+  _aecdumpSwitch.frame = aecdumpModeRect;
+  CGFloat aecdumpModeLabelCenterX = CGRectGetMaxX(aecdumpModeRect) +
+      kCallControlMargin + _aecdumpLabel.frame.size.width / 2;
+  _aecdumpLabel.center = CGPointMake(aecdumpModeLabelCenterX,
+                                     CGRectGetMidY(aecdumpModeRect));
+
+  CGFloat levelControlModeTop =
+       CGRectGetMaxY(_aecdumpSwitch.frame) + kCallControlMargin;
+  CGRect levelControlModeRect = CGRectMake(kCallControlMargin * 3,
+                                           levelControlModeTop,
+                                           _levelControlSwitch.frame.size.width,
+                                           _levelControlSwitch.frame.size.height);
+  _levelControlSwitch.frame = levelControlModeRect;
+  CGFloat levelControlModeLabelCenterX = CGRectGetMaxX(levelControlModeRect) +
+      kCallControlMargin + _levelControlLabel.frame.size.width / 2;
+  _levelControlLabel.center = CGPointMake(levelControlModeLabelCenterX,
+                                         CGRectGetMidY(levelControlModeRect));
+
+  CGFloat useManualAudioTop =
+      CGRectGetMaxY(_levelControlSwitch.frame) + kCallControlMargin;
+  CGRect useManualAudioRect =
+      CGRectMake(kCallControlMargin * 3,
+                 useManualAudioTop,
+                 _useManualAudioSwitch.frame.size.width,
+                 _useManualAudioSwitch.frame.size.height);
+  _useManualAudioSwitch.frame = useManualAudioRect;
+  CGFloat useManualAudioLabelCenterX = CGRectGetMaxX(useManualAudioRect) +
+      kCallControlMargin + _useManualAudioLabel.frame.size.width / 2;
+  _useManualAudioLabel.center =
+      CGPointMake(useManualAudioLabelCenterX,
+                  CGRectGetMidY(useManualAudioRect));
+
+  CGFloat audioLoopTop =
+     CGRectGetMaxY(useManualAudioRect) + kCallControlMargin * 3;
+  _audioLoopButton.frame = CGRectMake(kCallControlMargin,
+                                      audioLoopTop,
+                                      _audioLoopButton.frame.size.width,
+                                      _audioLoopButton.frame.size.height);
+
+  CGFloat startCallTop =
+      CGRectGetMaxY(_audioLoopButton.frame) + kCallControlMargin * 3;
+  _startCallButton.frame = CGRectMake(kCallControlMargin,
+                                      startCallTop,
+                                      _startCallButton.frame.size.width,
+                                      _startCallButton.frame.size.height);
+}
+
+#pragma mark - Private
+
+- (void)updateAudioLoopButton {
+  if (_isAudioLoopPlaying) {
+    _audioLoopButton.backgroundColor = [UIColor redColor];
+    [_audioLoopButton setTitle:@"Stop sound"
+                      forState:UIControlStateNormal];
+    [_audioLoopButton sizeToFit];
+  } else {
+    _audioLoopButton.backgroundColor = [UIColor greenColor];
+    [_audioLoopButton setTitle:@"Play sound"
+                      forState:UIControlStateNormal];
+    [_audioLoopButton sizeToFit];
+  }
+}
+
+- (void)onToggleAudioLoop:(id)sender {
+  [_delegate mainViewDidToggleAudioLoop:self];
+}
+
+- (void)onStartCall:(id)sender {
+  NSString *room = _roomText.roomText;
+  // If this is a loopback call, allow a generated room name.
+  if (!room.length && _loopbackSwitch.isOn) {
+    room = [[NSUUID UUID] UUIDString];
+  }
+  room = [room stringByReplacingOccurrencesOfString:@"-" withString:@""];
+  [_delegate mainView:self
+                didInputRoom:room
+                  isLoopback:_loopbackSwitch.isOn
+                 isAudioOnly:_audioOnlySwitch.isOn
+           shouldMakeAecDump:_aecdumpSwitch.isOn
+       shouldUseLevelControl:_levelControlSwitch.isOn
+              useManualAudio:_useManualAudioSwitch.isOn];
+}
+
+@end
diff --git a/webrtc/examples/objc/AppRTCMobile/ios/ARDMainViewController.h b/webrtc/examples/objc/AppRTCMobile/ios/ARDMainViewController.h
new file mode 100644
index 0000000..e5c92dd
--- /dev/null
+++ b/webrtc/examples/objc/AppRTCMobile/ios/ARDMainViewController.h
@@ -0,0 +1,14 @@
+/*
+ *  Copyright 2015 The WebRTC Project Authors. All rights reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#import <UIKit/UIKit.h>
+
+@interface ARDMainViewController : UIViewController
+@end
diff --git a/webrtc/examples/objc/AppRTCMobile/ios/ARDMainViewController.m b/webrtc/examples/objc/AppRTCMobile/ios/ARDMainViewController.m
new file mode 100644
index 0000000..e392168
--- /dev/null
+++ b/webrtc/examples/objc/AppRTCMobile/ios/ARDMainViewController.m
@@ -0,0 +1,211 @@
+/*
+ *  Copyright 2015 The WebRTC Project Authors. All rights reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#import "ARDMainViewController.h"
+
+#import <AVFoundation/AVFoundation.h>
+
+#import "WebRTC/RTCDispatcher.h"
+#import "WebRTC/RTCLogging.h"
+#import "webrtc/modules/audio_device/ios/objc/RTCAudioSession.h"
+#import "webrtc/modules/audio_device/ios/objc/RTCAudioSessionConfiguration.h"
+
+#import "ARDAppClient.h"
+#import "ARDMainView.h"
+#import "ARDVideoCallViewController.h"
+
+@interface ARDMainViewController () <
+    ARDMainViewDelegate,
+    ARDVideoCallViewControllerDelegate,
+    RTCAudioSessionDelegate>
+@end
+
+@implementation ARDMainViewController {
+  ARDMainView *_mainView;
+  AVAudioPlayer *_audioPlayer;
+  BOOL _useManualAudio;
+}
+
+- (void)loadView {
+  _mainView = [[ARDMainView alloc] initWithFrame:CGRectZero];
+  _mainView.delegate = self;
+  self.view = _mainView;
+
+  RTCAudioSessionConfiguration *webRTCConfig =
+      [RTCAudioSessionConfiguration webRTCConfiguration];
+  webRTCConfig.categoryOptions = webRTCConfig.categoryOptions |
+      AVAudioSessionCategoryOptionDefaultToSpeaker;
+  [RTCAudioSessionConfiguration setWebRTCConfiguration:webRTCConfig];
+
+  RTCAudioSession *session = [RTCAudioSession sharedInstance];
+  [session addDelegate:self];
+
+  [self configureAudioSession];
+  [self setupAudioPlayer];
+}
+
+#pragma mark - ARDMainViewDelegate
+
+- (void)mainView:(ARDMainView *)mainView
+             didInputRoom:(NSString *)room
+               isLoopback:(BOOL)isLoopback
+              isAudioOnly:(BOOL)isAudioOnly
+        shouldMakeAecDump:(BOOL)shouldMakeAecDump
+    shouldUseLevelControl:(BOOL)shouldUseLevelControl
+           useManualAudio:(BOOL)useManualAudio {
+  if (!room.length) {
+    [self showAlertWithMessage:@"Missing room name."];
+    return;
+  }
+  // Trim whitespaces.
+  NSCharacterSet *whitespaceSet = [NSCharacterSet whitespaceCharacterSet];
+  NSString *trimmedRoom = [room stringByTrimmingCharactersInSet:whitespaceSet];
+
+  // Check that room name is valid.
+  NSError *error = nil;
+  NSRegularExpressionOptions options = NSRegularExpressionCaseInsensitive;
+  NSRegularExpression *regex =
+      [NSRegularExpression regularExpressionWithPattern:@"\\w+"
+                                                options:options
+                                                  error:&error];
+  if (error) {
+    [self showAlertWithMessage:error.localizedDescription];
+    return;
+  }
+  NSRange matchRange =
+      [regex rangeOfFirstMatchInString:trimmedRoom
+                               options:0
+                                 range:NSMakeRange(0, trimmedRoom.length)];
+  if (matchRange.location == NSNotFound ||
+      matchRange.length != trimmedRoom.length) {
+    [self showAlertWithMessage:@"Invalid room name."];
+    return;
+  }
+
+  RTCAudioSession *session = [RTCAudioSession sharedInstance];
+  session.useManualAudio = useManualAudio;
+  session.isAudioEnabled = NO;
+
+  // Kick off the video call.
+  ARDVideoCallViewController *videoCallViewController =
+      [[ARDVideoCallViewController alloc] initForRoom:trimmedRoom
+                                           isLoopback:isLoopback
+                                          isAudioOnly:isAudioOnly
+                                    shouldMakeAecDump:shouldMakeAecDump
+                                shouldUseLevelControl:shouldUseLevelControl
+                                             delegate:self];
+  videoCallViewController.modalTransitionStyle =
+      UIModalTransitionStyleCrossDissolve;
+  [self presentViewController:videoCallViewController
+                     animated:YES
+                   completion:nil];
+}
+
+- (void)mainViewDidToggleAudioLoop:(ARDMainView *)mainView {
+  if (mainView.isAudioLoopPlaying) {
+    [_audioPlayer stop];
+  } else {
+    [_audioPlayer play];
+  }
+  mainView.isAudioLoopPlaying = _audioPlayer.playing;
+}
+
+#pragma mark - ARDVideoCallViewControllerDelegate
+
+- (void)viewControllerDidFinish:(ARDVideoCallViewController *)viewController {
+  if (![viewController isBeingDismissed]) {
+    RTCLog(@"Dismissing VC");
+    [self dismissViewControllerAnimated:YES completion:^{
+      [self restartAudioPlayerIfNeeded];
+    }];
+  }
+  RTCAudioSession *session = [RTCAudioSession sharedInstance];
+  session.isAudioEnabled = NO;
+}
+
+#pragma mark - RTCAudioSessionDelegate
+
+- (void)audioSessionDidStartPlayOrRecord:(RTCAudioSession *)session {
+  // Stop playback on main queue and then configure WebRTC.
+  [RTCDispatcher dispatchAsyncOnType:RTCDispatcherTypeMain
+                               block:^{
+    if (_mainView.isAudioLoopPlaying) {
+      RTCLog(@"Stopping audio loop due to WebRTC start.");
+      [_audioPlayer stop];
+    }
+    RTCLog(@"Setting isAudioEnabled to YES.");
+    session.isAudioEnabled = YES;
+  }];
+}
+
+- (void)audioSessionDidStopPlayOrRecord:(RTCAudioSession *)session {
+  // WebRTC is done with the audio session. Restart playback.
+  [RTCDispatcher dispatchAsyncOnType:RTCDispatcherTypeMain
+                               block:^{
+    RTCLog(@"audioSessionDidStopPlayOrRecord");
+    [self restartAudioPlayerIfNeeded];
+  }];
+}
+
+#pragma mark - Private
+
+- (void)configureAudioSession {
+  RTCAudioSessionConfiguration *configuration =
+      [[RTCAudioSessionConfiguration alloc] init];
+  configuration.category = AVAudioSessionCategoryAmbient;
+  configuration.categoryOptions = AVAudioSessionCategoryOptionDuckOthers;
+  configuration.mode = AVAudioSessionModeDefault;
+
+  RTCAudioSession *session = [RTCAudioSession sharedInstance];
+  [session lockForConfiguration];
+  BOOL hasSucceeded = NO;
+  NSError *error = nil;
+  if (session.isActive) {
+    hasSucceeded = [session setConfiguration:configuration error:&error];
+  } else {
+    hasSucceeded = [session setConfiguration:configuration
+                                      active:YES
+                                       error:&error];
+  }
+  if (!hasSucceeded) {
+    RTCLogError(@"Error setting configuration: %@", error.localizedDescription);
+  }
+  [session unlockForConfiguration];
+}
+
+- (void)setupAudioPlayer {
+  NSString *audioFilePath =
+      [[NSBundle mainBundle] pathForResource:@"mozart" ofType:@"mp3"];
+  NSURL *audioFileURL = [NSURL URLWithString:audioFilePath];
+  _audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:audioFileURL
+                                                        error:nil];
+  _audioPlayer.numberOfLoops = -1;
+  _audioPlayer.volume = 1.0;
+  [_audioPlayer prepareToPlay];
+}
+
+- (void)restartAudioPlayerIfNeeded {
+  if (_mainView.isAudioLoopPlaying && !self.presentedViewController) {
+    RTCLog(@"Starting audio loop due to WebRTC end.");
+    [self configureAudioSession];
+    [_audioPlayer play];
+  }
+}
+
+- (void)showAlertWithMessage:(NSString*)message {
+  UIAlertView* alertView = [[UIAlertView alloc] initWithTitle:nil
+                                                      message:message
+                                                     delegate:nil
+                                            cancelButtonTitle:@"OK"
+                                            otherButtonTitles:nil];
+  [alertView show];
+}
+
+@end
diff --git a/webrtc/examples/objc/AppRTCMobile/ios/ARDStatsView.h b/webrtc/examples/objc/AppRTCMobile/ios/ARDStatsView.h
new file mode 100644
index 0000000..9c86364
--- /dev/null
+++ b/webrtc/examples/objc/AppRTCMobile/ios/ARDStatsView.h
@@ -0,0 +1,17 @@
+/*
+ *  Copyright 2015 The WebRTC Project Authors. All rights reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#import <UIKit/UIKit.h>
+
+@interface ARDStatsView : UIView
+
+- (void)setStats:(NSArray *)stats;
+
+@end
diff --git a/webrtc/examples/objc/AppRTCMobile/ios/ARDStatsView.m b/webrtc/examples/objc/AppRTCMobile/ios/ARDStatsView.m
new file mode 100644
index 0000000..39067e0
--- /dev/null
+++ b/webrtc/examples/objc/AppRTCMobile/ios/ARDStatsView.m
@@ -0,0 +1,52 @@
+/*
+ *  Copyright 2015 The WebRTC Project Authors. All rights reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#import "ARDStatsView.h"
+
+#import "WebRTC/RTCLegacyStatsReport.h"
+
+#import "ARDStatsBuilder.h"
+
+@implementation ARDStatsView {
+  UILabel *_statsLabel;
+  ARDStatsBuilder *_statsBuilder;
+}
+
+- (instancetype)initWithFrame:(CGRect)frame {
+  if (self = [super initWithFrame:frame]) {
+    _statsLabel = [[UILabel alloc] initWithFrame:CGRectZero];
+    _statsLabel.numberOfLines = 0;
+    _statsLabel.font = [UIFont fontWithName:@"Roboto" size:12];
+    _statsLabel.adjustsFontSizeToFitWidth = YES;
+    _statsLabel.minimumScaleFactor = 0.6;
+    _statsLabel.textColor = [UIColor greenColor];
+    [self addSubview:_statsLabel];
+    self.backgroundColor = [UIColor colorWithWhite:0 alpha:.6];
+    _statsBuilder = [[ARDStatsBuilder alloc] init];
+  }
+  return self;
+}
+
+- (void)setStats:(NSArray *)stats {
+  for (RTCLegacyStatsReport *report in stats) {
+    [_statsBuilder parseStatsReport:report];
+  }
+  _statsLabel.text = _statsBuilder.statsString;
+}
+
+- (void)layoutSubviews {
+  _statsLabel.frame = self.bounds;
+}
+
+- (CGSize)sizeThatFits:(CGSize)size {
+  return [_statsLabel sizeThatFits:size];
+}
+
+@end
diff --git a/webrtc/examples/objc/AppRTCMobile/ios/ARDVideoCallView.h b/webrtc/examples/objc/AppRTCMobile/ios/ARDVideoCallView.h
new file mode 100644
index 0000000..dec1bfc
--- /dev/null
+++ b/webrtc/examples/objc/AppRTCMobile/ios/ARDVideoCallView.h
@@ -0,0 +1,45 @@
+/*
+ *  Copyright 2015 The WebRTC Project Authors. All rights reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#import <UIKit/UIKit.h>
+
+#import "WebRTC/RTCCameraPreviewView.h"
+#import "WebRTC/RTCEAGLVideoView.h"
+
+#import "ARDStatsView.h"
+
+@class ARDVideoCallView;
+@protocol ARDVideoCallViewDelegate <NSObject>
+
+// Called when the camera switch button is pressed.
+- (void)videoCallViewDidSwitchCamera:(ARDVideoCallView *)view;
+
+// Called when the route change button is pressed.
+- (void)videoCallViewDidChangeRoute:(ARDVideoCallView *)view;
+
+// Called when the hangup button is pressed.
+- (void)videoCallViewDidHangup:(ARDVideoCallView *)view;
+
+// Called when stats are enabled by triple tapping.
+- (void)videoCallViewDidEnableStats:(ARDVideoCallView *)view;
+
+@end
+
+// Video call view that shows local and remote video, provides a label to
+// display status, and also a hangup button.
+@interface ARDVideoCallView : UIView
+
+@property(nonatomic, readonly) UILabel *statusLabel;
+@property(nonatomic, readonly) RTCCameraPreviewView *localVideoView;
+@property(nonatomic, readonly) RTCEAGLVideoView *remoteVideoView;
+@property(nonatomic, readonly) ARDStatsView *statsView;
+@property(nonatomic, weak) id<ARDVideoCallViewDelegate> delegate;
+
+@end
diff --git a/webrtc/examples/objc/AppRTCMobile/ios/ARDVideoCallView.m b/webrtc/examples/objc/AppRTCMobile/ios/ARDVideoCallView.m
new file mode 100644
index 0000000..6e5fc59
--- /dev/null
+++ b/webrtc/examples/objc/AppRTCMobile/ios/ARDVideoCallView.m
@@ -0,0 +1,193 @@
+/*
+ *  Copyright 2015 The WebRTC Project Authors. All rights reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#import "ARDVideoCallView.h"
+
+#import <AVFoundation/AVFoundation.h>
+#import "UIImage+ARDUtilities.h"
+
+static CGFloat const kButtonPadding = 16;
+static CGFloat const kButtonSize = 48;
+static CGFloat const kLocalVideoViewSize = 120;
+static CGFloat const kLocalVideoViewPadding = 8;
+static CGFloat const kStatusBarHeight = 20;
+
+@interface ARDVideoCallView () <RTCEAGLVideoViewDelegate>
+@end
+
+@implementation ARDVideoCallView {
+  UIButton *_routeChangeButton;
+  UIButton *_cameraSwitchButton;
+  UIButton *_hangupButton;
+  CGSize _remoteVideoSize;
+  BOOL _useRearCamera;
+}
+
+@synthesize statusLabel = _statusLabel;
+@synthesize localVideoView = _localVideoView;
+@synthesize remoteVideoView = _remoteVideoView;
+@synthesize statsView = _statsView;
+@synthesize delegate = _delegate;
+
+- (instancetype)initWithFrame:(CGRect)frame {
+  if (self = [super initWithFrame:frame]) {
+    _remoteVideoView = [[RTCEAGLVideoView alloc] initWithFrame:CGRectZero];
+    _remoteVideoView.delegate = self;
+    [self addSubview:_remoteVideoView];
+
+    _localVideoView = [[RTCCameraPreviewView alloc] initWithFrame:CGRectZero];
+    [self addSubview:_localVideoView];
+
+    _statsView = [[ARDStatsView alloc] initWithFrame:CGRectZero];
+    _statsView.hidden = YES;
+    [self addSubview:_statsView];
+
+    _routeChangeButton = [UIButton buttonWithType:UIButtonTypeCustom];
+    _routeChangeButton.backgroundColor = [UIColor whiteColor];
+    _routeChangeButton.layer.cornerRadius = kButtonSize / 2;
+    _routeChangeButton.layer.masksToBounds = YES;
+    UIImage *image = [UIImage imageNamed:@"ic_surround_sound_black_24dp.png"];
+    [_routeChangeButton setImage:image forState:UIControlStateNormal];
+    [_routeChangeButton addTarget:self
+                           action:@selector(onRouteChange:)
+                 forControlEvents:UIControlEventTouchUpInside];
+    [self addSubview:_routeChangeButton];
+
+    // TODO(tkchin): don't display this if we can't actually do camera switch.
+    _cameraSwitchButton = [UIButton buttonWithType:UIButtonTypeCustom];
+    _cameraSwitchButton.backgroundColor = [UIColor whiteColor];
+    _cameraSwitchButton.layer.cornerRadius = kButtonSize / 2;
+    _cameraSwitchButton.layer.masksToBounds = YES;
+    image = [UIImage imageNamed:@"ic_switch_video_black_24dp.png"];
+    [_cameraSwitchButton setImage:image forState:UIControlStateNormal];
+    [_cameraSwitchButton addTarget:self
+                      action:@selector(onCameraSwitch:)
+            forControlEvents:UIControlEventTouchUpInside];
+    [self addSubview:_cameraSwitchButton];
+
+    _hangupButton = [UIButton buttonWithType:UIButtonTypeCustom];
+    _hangupButton.backgroundColor = [UIColor redColor];
+    _hangupButton.layer.cornerRadius = kButtonSize / 2;
+    _hangupButton.layer.masksToBounds = YES;
+    image = [UIImage imageForName:@"ic_call_end_black_24dp.png"
+                            color:[UIColor whiteColor]];
+    [_hangupButton setImage:image forState:UIControlStateNormal];
+    [_hangupButton addTarget:self
+                      action:@selector(onHangup:)
+            forControlEvents:UIControlEventTouchUpInside];
+    [self addSubview:_hangupButton];
+
+    _statusLabel = [[UILabel alloc] initWithFrame:CGRectZero];
+    _statusLabel.font = [UIFont fontWithName:@"Roboto" size:16];
+    _statusLabel.textColor = [UIColor whiteColor];
+    [self addSubview:_statusLabel];
+
+    UITapGestureRecognizer *tapRecognizer =
+        [[UITapGestureRecognizer alloc]
+            initWithTarget:self
+                    action:@selector(didTripleTap:)];
+    tapRecognizer.numberOfTapsRequired = 3;
+    [self addGestureRecognizer:tapRecognizer];
+  }
+  return self;
+}
+
+- (void)layoutSubviews {
+  CGRect bounds = self.bounds;
+  if (_remoteVideoSize.width > 0 && _remoteVideoSize.height > 0) {
+    // Aspect fill remote video into bounds.
+    CGRect remoteVideoFrame =
+        AVMakeRectWithAspectRatioInsideRect(_remoteVideoSize, bounds);
+    CGFloat scale = 1;
+    if (remoteVideoFrame.size.width > remoteVideoFrame.size.height) {
+      // Scale by height.
+      scale = bounds.size.height / remoteVideoFrame.size.height;
+    } else {
+      // Scale by width.
+      scale = bounds.size.width / remoteVideoFrame.size.width;
+    }
+    remoteVideoFrame.size.height *= scale;
+    remoteVideoFrame.size.width *= scale;
+    _remoteVideoView.frame = remoteVideoFrame;
+    _remoteVideoView.center =
+        CGPointMake(CGRectGetMidX(bounds), CGRectGetMidY(bounds));
+  } else {
+    _remoteVideoView.frame = bounds;
+  }
+
+  // Aspect fit local video view into a square box.
+  CGRect localVideoFrame =
+      CGRectMake(0, 0, kLocalVideoViewSize, kLocalVideoViewSize);
+  // Place the view in the bottom right.
+  localVideoFrame.origin.x = CGRectGetMaxX(bounds)
+      - localVideoFrame.size.width - kLocalVideoViewPadding;
+  localVideoFrame.origin.y = CGRectGetMaxY(bounds)
+      - localVideoFrame.size.height - kLocalVideoViewPadding;
+  _localVideoView.frame = localVideoFrame;
+
+  // Place stats at the top.
+  CGSize statsSize = [_statsView sizeThatFits:bounds.size];
+  _statsView.frame = CGRectMake(CGRectGetMinX(bounds),
+                                CGRectGetMinY(bounds) + kStatusBarHeight,
+                                statsSize.width, statsSize.height);
+
+  // Place hangup button in the bottom left.
+  _hangupButton.frame =
+      CGRectMake(CGRectGetMinX(bounds) + kButtonPadding,
+                 CGRectGetMaxY(bounds) - kButtonPadding -
+                     kButtonSize,
+                 kButtonSize,
+                 kButtonSize);
+
+  // Place button to the right of hangup button.
+  CGRect cameraSwitchFrame = _hangupButton.frame;
+  cameraSwitchFrame.origin.x =
+      CGRectGetMaxX(cameraSwitchFrame) + kButtonPadding;
+  _cameraSwitchButton.frame = cameraSwitchFrame;
+
+  // Place route button to the right of camera button.
+  CGRect routeChangeFrame = _cameraSwitchButton.frame;
+  routeChangeFrame.origin.x =
+      CGRectGetMaxX(routeChangeFrame) + kButtonPadding;
+  _routeChangeButton.frame = routeChangeFrame;
+
+  [_statusLabel sizeToFit];
+  _statusLabel.center =
+      CGPointMake(CGRectGetMidX(bounds), CGRectGetMidY(bounds));
+}
+
+#pragma mark - RTCEAGLVideoViewDelegate
+
+- (void)videoView:(RTCEAGLVideoView*)videoView didChangeVideoSize:(CGSize)size {
+ if (videoView == _remoteVideoView) {
+    _remoteVideoSize = size;
+  }
+  [self setNeedsLayout];
+}
+
+#pragma mark - Private
+
+- (void)onCameraSwitch:(id)sender {
+  [_delegate videoCallViewDidSwitchCamera:self];
+}
+
+- (void)onRouteChange:(id)sender {
+  [_delegate videoCallViewDidChangeRoute:self];
+}
+
+- (void)onHangup:(id)sender {
+  [_delegate videoCallViewDidHangup:self];
+}
+
+- (void)didTripleTap:(UITapGestureRecognizer *)recognizer {
+  [_delegate videoCallViewDidEnableStats:self];
+}
+
+@end
diff --git a/webrtc/examples/objc/AppRTCMobile/ios/ARDVideoCallViewController.h b/webrtc/examples/objc/AppRTCMobile/ios/ARDVideoCallViewController.h
new file mode 100644
index 0000000..3ca2dc2
--- /dev/null
+++ b/webrtc/examples/objc/AppRTCMobile/ios/ARDVideoCallViewController.h
@@ -0,0 +1,31 @@
+/*
+ *  Copyright 2015 The WebRTC Project Authors. All rights reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#import <UIKit/UIKit.h>
+
+@class ARDVideoCallViewController;
+@protocol ARDVideoCallViewControllerDelegate <NSObject>
+
+- (void)viewControllerDidFinish:(ARDVideoCallViewController *)viewController;
+
+@end
+
+@interface ARDVideoCallViewController : UIViewController
+
+@property(nonatomic, weak) id<ARDVideoCallViewControllerDelegate> delegate;
+
+- (instancetype)initForRoom:(NSString *)room
+                 isLoopback:(BOOL)isLoopback
+                isAudioOnly:(BOOL)isAudioOnly
+          shouldMakeAecDump:(BOOL)shouldMakeAecDump
+      shouldUseLevelControl:(BOOL)shouldUseLevelControl
+                   delegate:(id<ARDVideoCallViewControllerDelegate>)delegate;
+
+@end
diff --git a/webrtc/examples/objc/AppRTCMobile/ios/ARDVideoCallViewController.m b/webrtc/examples/objc/AppRTCMobile/ios/ARDVideoCallViewController.m
new file mode 100644
index 0000000..aa9ea21
--- /dev/null
+++ b/webrtc/examples/objc/AppRTCMobile/ios/ARDVideoCallViewController.m
@@ -0,0 +1,222 @@
+/*
+ *  Copyright 2015 The WebRTC Project Authors. All rights reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#import "ARDVideoCallViewController.h"
+
+#import "webrtc/modules/audio_device/ios/objc/RTCAudioSession.h"
+
+#import "WebRTC/RTCAVFoundationVideoSource.h"
+#import "WebRTC/RTCDispatcher.h"
+#import "WebRTC/RTCLogging.h"
+
+#import "ARDAppClient.h"
+#import "ARDVideoCallView.h"
+
+@interface ARDVideoCallViewController () <ARDAppClientDelegate,
+    ARDVideoCallViewDelegate>
+@property(nonatomic, strong) RTCVideoTrack *localVideoTrack;
+@property(nonatomic, strong) RTCVideoTrack *remoteVideoTrack;
+@property(nonatomic, readonly) ARDVideoCallView *videoCallView;
+@end
+
+@implementation ARDVideoCallViewController {
+  ARDAppClient *_client;
+  RTCVideoTrack *_remoteVideoTrack;
+  RTCVideoTrack *_localVideoTrack;
+  AVAudioSessionPortOverride _portOverride;
+}
+
+@synthesize videoCallView = _videoCallView;
+@synthesize delegate = _delegate;
+
+- (instancetype)initForRoom:(NSString *)room
+                 isLoopback:(BOOL)isLoopback
+                isAudioOnly:(BOOL)isAudioOnly
+          shouldMakeAecDump:(BOOL)shouldMakeAecDump
+      shouldUseLevelControl:(BOOL)shouldUseLevelControl
+                   delegate:(id<ARDVideoCallViewControllerDelegate>)delegate {
+  if (self = [super init]) {
+    _delegate = delegate;
+    _client = [[ARDAppClient alloc] initWithDelegate:self];
+    [_client connectToRoomWithId:room
+                      isLoopback:isLoopback
+                     isAudioOnly:isAudioOnly
+               shouldMakeAecDump:shouldMakeAecDump
+           shouldUseLevelControl:shouldUseLevelControl];
+  }
+  return self;
+}
+
+- (void)loadView {
+  _videoCallView = [[ARDVideoCallView alloc] initWithFrame:CGRectZero];
+  _videoCallView.delegate = self;
+  _videoCallView.statusLabel.text =
+      [self statusTextForState:RTCIceConnectionStateNew];
+  self.view = _videoCallView;
+}
+
+#pragma mark - ARDAppClientDelegate
+
+- (void)appClient:(ARDAppClient *)client
+    didChangeState:(ARDAppClientState)state {
+  switch (state) {
+    case kARDAppClientStateConnected:
+      RTCLog(@"Client connected.");
+      break;
+    case kARDAppClientStateConnecting:
+      RTCLog(@"Client connecting.");
+      break;
+    case kARDAppClientStateDisconnected:
+      RTCLog(@"Client disconnected.");
+      [self hangup];
+      break;
+  }
+}
+
+- (void)appClient:(ARDAppClient *)client
+    didChangeConnectionState:(RTCIceConnectionState)state {
+  RTCLog(@"ICE state changed: %ld", (long)state);
+  __weak ARDVideoCallViewController *weakSelf = self;
+  dispatch_async(dispatch_get_main_queue(), ^{
+    ARDVideoCallViewController *strongSelf = weakSelf;
+    strongSelf.videoCallView.statusLabel.text =
+        [strongSelf statusTextForState:state];
+  });
+}
+
+- (void)appClient:(ARDAppClient *)client
+    didReceiveLocalVideoTrack:(RTCVideoTrack *)localVideoTrack {
+  self.localVideoTrack = localVideoTrack;
+}
+
+- (void)appClient:(ARDAppClient *)client
+    didReceiveRemoteVideoTrack:(RTCVideoTrack *)remoteVideoTrack {
+  self.remoteVideoTrack = remoteVideoTrack;
+  _videoCallView.statusLabel.hidden = YES;
+}
+
+- (void)appClient:(ARDAppClient *)client
+      didGetStats:(NSArray *)stats {
+  _videoCallView.statsView.stats = stats;
+  [_videoCallView setNeedsLayout];
+}
+
+- (void)appClient:(ARDAppClient *)client
+         didError:(NSError *)error {
+  NSString *message =
+      [NSString stringWithFormat:@"%@", error.localizedDescription];
+  [self showAlertWithMessage:message];
+  [self hangup];
+}
+
+#pragma mark - ARDVideoCallViewDelegate
+
+- (void)videoCallViewDidHangup:(ARDVideoCallView *)view {
+  [self hangup];
+}
+
+- (void)videoCallViewDidSwitchCamera:(ARDVideoCallView *)view {
+  // TODO(tkchin): Rate limit this so you can't tap continously on it.
+  // Probably through an animation.
+  [self switchCamera];
+}
+
+- (void)videoCallViewDidChangeRoute:(ARDVideoCallView *)view {
+  AVAudioSessionPortOverride override = AVAudioSessionPortOverrideNone;
+  if (_portOverride == AVAudioSessionPortOverrideNone) {
+    override = AVAudioSessionPortOverrideSpeaker;
+  }
+  [RTCDispatcher dispatchAsyncOnType:RTCDispatcherTypeAudioSession
+                               block:^{
+    RTCAudioSession *session = [RTCAudioSession sharedInstance];
+    [session lockForConfiguration];
+    NSError *error = nil;
+    if ([session overrideOutputAudioPort:override error:&error]) {
+      _portOverride = override;
+    } else {
+      RTCLogError(@"Error overriding output port: %@",
+                  error.localizedDescription);
+    }
+    [session unlockForConfiguration];
+  }];
+}
+
+- (void)videoCallViewDidEnableStats:(ARDVideoCallView *)view {
+  _client.shouldGetStats = YES;
+  _videoCallView.statsView.hidden = NO;
+}
+
+#pragma mark - Private
+
+- (void)setLocalVideoTrack:(RTCVideoTrack *)localVideoTrack {
+  if (_localVideoTrack == localVideoTrack) {
+    return;
+  }
+  _localVideoTrack = nil;
+  _localVideoTrack = localVideoTrack;
+  RTCAVFoundationVideoSource *source = nil;
+  if ([localVideoTrack.source
+          isKindOfClass:[RTCAVFoundationVideoSource class]]) {
+    source = (RTCAVFoundationVideoSource*)localVideoTrack.source;
+  }
+  _videoCallView.localVideoView.captureSession = source.captureSession;
+}
+
+- (void)setRemoteVideoTrack:(RTCVideoTrack *)remoteVideoTrack {
+  if (_remoteVideoTrack == remoteVideoTrack) {
+    return;
+  }
+  [_remoteVideoTrack removeRenderer:_videoCallView.remoteVideoView];
+  _remoteVideoTrack = nil;
+  [_videoCallView.remoteVideoView renderFrame:nil];
+  _remoteVideoTrack = remoteVideoTrack;
+  [_remoteVideoTrack addRenderer:_videoCallView.remoteVideoView];
+}
+
+- (void)hangup {
+  self.remoteVideoTrack = nil;
+  self.localVideoTrack = nil;
+  [_client disconnect];
+  [_delegate viewControllerDidFinish:self];
+}
+
+- (void)switchCamera {
+  RTCVideoSource* source = self.localVideoTrack.source;
+  if ([source isKindOfClass:[RTCAVFoundationVideoSource class]]) {
+    RTCAVFoundationVideoSource* avSource = (RTCAVFoundationVideoSource*)source;
+    avSource.useBackCamera = !avSource.useBackCamera;
+  }
+}
+
+- (NSString *)statusTextForState:(RTCIceConnectionState)state {
+  switch (state) {
+    case RTCIceConnectionStateNew:
+    case RTCIceConnectionStateChecking:
+      return @"Connecting...";
+    case RTCIceConnectionStateConnected:
+    case RTCIceConnectionStateCompleted:
+    case RTCIceConnectionStateFailed:
+    case RTCIceConnectionStateDisconnected:
+    case RTCIceConnectionStateClosed:
+    case RTCIceConnectionStateCount:
+      return nil;
+  }
+}
+
+- (void)showAlertWithMessage:(NSString*)message {
+  UIAlertView* alertView = [[UIAlertView alloc] initWithTitle:nil
+                                                      message:message
+                                                     delegate:nil
+                                            cancelButtonTitle:@"OK"
+                                            otherButtonTitles:nil];
+  [alertView show];
+}
+
+@end
diff --git a/webrtc/examples/objc/AppRTCMobile/ios/AppRTCMobile-Prefix.pch b/webrtc/examples/objc/AppRTCMobile/ios/AppRTCMobile-Prefix.pch
new file mode 100644
index 0000000..ca0db90
--- /dev/null
+++ b/webrtc/examples/objc/AppRTCMobile/ios/AppRTCMobile-Prefix.pch
@@ -0,0 +1,23 @@
+/*
+ *  Copyright 2013 The WebRTC Project Authors. All rights reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+//
+// Prefix header for all source files of the 'AppRTCMobile' target in the
+// 'AppRTCMobile' project
+//
+
+#import <Availability.h>
+
+#if __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_6_0
+#warning "This project uses features only available in iOS SDK 6.0 and later."
+#endif
+
+#import <Foundation/Foundation.h>
+#import <UIKit/UIKit.h>
diff --git a/webrtc/examples/objc/AppRTCMobile/ios/Info.plist b/webrtc/examples/objc/AppRTCMobile/ios/Info.plist
new file mode 100644
index 0000000..e46d0e6
--- /dev/null
+++ b/webrtc/examples/objc/AppRTCMobile/ios/Info.plist
@@ -0,0 +1,98 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+  <key>BuildMachineOSBuild</key>
+  <string>12E55</string>
+  <key>CFBundleDevelopmentRegion</key>
+  <string>en</string>
+  <key>CFBundleDisplayName</key>
+  <string>AppRTCMobile</string>
+  <key>CFBundleExecutable</key>
+  <string>AppRTCMobile</string>
+  <key>CFBundleIcons</key>
+  <dict>
+    <key>CFBundlePrimaryIcon</key>
+    <dict>
+      <key>CFBundleIconFiles</key>
+      <array>
+        <string>Icon.png</string>
+      </array>
+    </dict>
+  </dict>
+  <key>CFBundleIdentifier</key>
+  <string>com.google.AppRTCMobile</string>
+  <key>CFBundleInfoDictionaryVersion</key>
+  <string>6.0</string>
+  <key>CFBundleName</key>
+  <string>AppRTCMobile</string>
+  <key>CFBundlePackageType</key>
+  <string>APPL</string>
+  <key>CFBundleShortVersionString</key>
+  <string>1.0</string>
+  <key>CFBundleSignature</key>
+  <string>????</string>
+  <key>CFBundleSupportedPlatforms</key>
+  <array>
+    <string>iPhoneOS</string>
+  </array>
+  <key>CFBundleVersion</key>
+  <string>1.0</string>
+  <key>UIStatusBarTintParameters</key>
+  <dict>
+    <key>UINavigationBar</key>
+    <dict>
+      <key>Style</key>
+      <string>UIBarStyleDefault</string>
+      <key>Translucent</key>
+      <false/>
+    </dict>
+  </dict>
+  <key>UISupportedInterfaceOrientations</key>
+  <array>
+    <string>UIInterfaceOrientationPortrait</string>
+  </array>
+  <key>UIAppFonts</key>
+  <array>
+    <string>Roboto-Regular.ttf</string>
+  </array>
+  <key>UIBackgroundModes</key>
+  <array>
+    <string>audio</string>
+    <string>voip</string>
+  </array>
+  <key>UILaunchImages</key>
+  <array>
+    <dict>
+      <key>UILaunchImageMinimumOSVersion</key>
+      <string>7.0</string>
+      <key>UILaunchImageName</key>
+      <string>iPhone5</string>
+      <key>UILaunchImageOrientation</key>
+      <string>Portrait</string>
+      <key>UILaunchImageSize</key>
+      <string>{320, 568}</string>
+    </dict>
+    <dict>
+      <key>UILaunchImageMinimumOSVersion</key>
+      <string>8.0</string>
+      <key>UILaunchImageName</key>
+      <string>iPhone6</string>
+      <key>UILaunchImageOrientation</key>
+      <string>Portrait</string>
+      <key>UILaunchImageSize</key>
+      <string>{375, 667}</string>
+    </dict>
+    <dict>
+      <key>UILaunchImageMinimumOSVersion</key>
+      <string>8.0</string>
+      <key>UILaunchImageName</key>
+      <string>iPhone6p</string>
+      <key>UILaunchImageOrientation</key>
+      <string>Portrait</string>
+      <key>UILaunchImageSize</key>
+      <string>{414, 736}</string>
+    </dict>
+  </array>
+</dict>
+</plist>
diff --git a/webrtc/examples/objc/AppRTCMobile/ios/UIImage+ARDUtilities.h b/webrtc/examples/objc/AppRTCMobile/ios/UIImage+ARDUtilities.h
new file mode 100644
index 0000000..d56ba02
--- /dev/null
+++ b/webrtc/examples/objc/AppRTCMobile/ios/UIImage+ARDUtilities.h
@@ -0,0 +1,18 @@
+/*
+ *  Copyright 2015 The WebRTC Project Authors. All rights reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#import <UIKit/UIKit.h>
+
+@interface UIImage (ARDUtilities)
+
+// Returns an color tinted version for the given image resource.
++ (UIImage *)imageForName:(NSString *)name color:(UIColor *)color;
+
+@end
diff --git a/webrtc/examples/objc/AppRTCMobile/ios/UIImage+ARDUtilities.m b/webrtc/examples/objc/AppRTCMobile/ios/UIImage+ARDUtilities.m
new file mode 100644
index 0000000..1bbe8c3
--- /dev/null
+++ b/webrtc/examples/objc/AppRTCMobile/ios/UIImage+ARDUtilities.m
@@ -0,0 +1,31 @@
+/*
+ *  Copyright 2015 The WebRTC Project Authors. All rights reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#import "UIImage+ARDUtilities.h"
+
+@implementation UIImage (ARDUtilities)
+
++ (UIImage *)imageForName:(NSString *)name color:(UIColor *)color {
+  UIImage *image = [UIImage imageNamed:name];
+  if (!image) {
+    return nil;
+  }
+  UIGraphicsBeginImageContextWithOptions(image.size, NO, 0.0f);
+  [color setFill];
+  CGRect bounds = CGRectMake(0, 0, image.size.width, image.size.height);
+  UIRectFill(bounds);
+  [image drawInRect:bounds blendMode:kCGBlendModeDestinationIn alpha:1.0f];
+  UIImage *coloredImage = UIGraphicsGetImageFromCurrentImageContext();
+  UIGraphicsEndImageContext();
+
+  return coloredImage;
+}
+
+@end
diff --git a/webrtc/examples/objc/AppRTCMobile/ios/main.m b/webrtc/examples/objc/AppRTCMobile/ios/main.m
new file mode 100644
index 0000000..00b83f7
--- /dev/null
+++ b/webrtc/examples/objc/AppRTCMobile/ios/main.m
@@ -0,0 +1,20 @@
+/*
+ *  Copyright 2013 The WebRTC Project Authors. All rights reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#import <UIKit/UIKit.h>
+
+#import "ARDAppDelegate.h"
+
+int main(int argc, char* argv[]) {
+  @autoreleasepool {
+    return UIApplicationMain(
+        argc, argv, nil, NSStringFromClass([ARDAppDelegate class]));
+  }
+}
diff --git a/webrtc/examples/objc/AppRTCMobile/ios/resources/Roboto-Regular.ttf b/webrtc/examples/objc/AppRTCMobile/ios/resources/Roboto-Regular.ttf
new file mode 100644
index 0000000..0e58508
--- /dev/null
+++ b/webrtc/examples/objc/AppRTCMobile/ios/resources/Roboto-Regular.ttf
Binary files differ
diff --git a/webrtc/examples/objc/AppRTCMobile/ios/resources/iPhone5@2x.png b/webrtc/examples/objc/AppRTCMobile/ios/resources/iPhone5@2x.png
new file mode 100644
index 0000000..9d005fd
--- /dev/null
+++ b/webrtc/examples/objc/AppRTCMobile/ios/resources/iPhone5@2x.png
Binary files differ
diff --git a/webrtc/examples/objc/AppRTCMobile/ios/resources/iPhone6@2x.png b/webrtc/examples/objc/AppRTCMobile/ios/resources/iPhone6@2x.png
new file mode 100644
index 0000000..fce3eb9
--- /dev/null
+++ b/webrtc/examples/objc/AppRTCMobile/ios/resources/iPhone6@2x.png
Binary files differ
diff --git a/webrtc/examples/objc/AppRTCMobile/ios/resources/iPhone6p@3x.png b/webrtc/examples/objc/AppRTCMobile/ios/resources/iPhone6p@3x.png
new file mode 100644
index 0000000..aee20c2
--- /dev/null
+++ b/webrtc/examples/objc/AppRTCMobile/ios/resources/iPhone6p@3x.png
Binary files differ
diff --git a/webrtc/examples/objc/AppRTCMobile/ios/resources/ic_call_end_black_24dp.png b/webrtc/examples/objc/AppRTCMobile/ios/resources/ic_call_end_black_24dp.png
new file mode 100644
index 0000000..531cb0f
--- /dev/null
+++ b/webrtc/examples/objc/AppRTCMobile/ios/resources/ic_call_end_black_24dp.png
Binary files differ
diff --git a/webrtc/examples/objc/AppRTCMobile/ios/resources/ic_call_end_black_24dp@2x.png b/webrtc/examples/objc/AppRTCMobile/ios/resources/ic_call_end_black_24dp@2x.png
new file mode 100644
index 0000000..03dd381
--- /dev/null
+++ b/webrtc/examples/objc/AppRTCMobile/ios/resources/ic_call_end_black_24dp@2x.png
Binary files differ
diff --git a/webrtc/examples/objc/AppRTCMobile/ios/resources/ic_clear_black_24dp.png b/webrtc/examples/objc/AppRTCMobile/ios/resources/ic_clear_black_24dp.png
new file mode 100644
index 0000000..4ebf8a2
--- /dev/null
+++ b/webrtc/examples/objc/AppRTCMobile/ios/resources/ic_clear_black_24dp.png
Binary files differ
diff --git a/webrtc/examples/objc/AppRTCMobile/ios/resources/ic_clear_black_24dp@2x.png b/webrtc/examples/objc/AppRTCMobile/ios/resources/ic_clear_black_24dp@2x.png
new file mode 100644
index 0000000..ed2b252
--- /dev/null
+++ b/webrtc/examples/objc/AppRTCMobile/ios/resources/ic_clear_black_24dp@2x.png
Binary files differ
diff --git a/webrtc/examples/objc/AppRTCMobile/ios/resources/ic_surround_sound_black_24dp.png b/webrtc/examples/objc/AppRTCMobile/ios/resources/ic_surround_sound_black_24dp.png
new file mode 100644
index 0000000..8f3343d
--- /dev/null
+++ b/webrtc/examples/objc/AppRTCMobile/ios/resources/ic_surround_sound_black_24dp.png
Binary files differ
diff --git a/webrtc/examples/objc/AppRTCMobile/ios/resources/ic_surround_sound_black_24dp@2x.png b/webrtc/examples/objc/AppRTCMobile/ios/resources/ic_surround_sound_black_24dp@2x.png
new file mode 100644
index 0000000..7648804
--- /dev/null
+++ b/webrtc/examples/objc/AppRTCMobile/ios/resources/ic_surround_sound_black_24dp@2x.png
Binary files differ
diff --git a/webrtc/examples/objc/AppRTCMobile/ios/resources/ic_switch_video_black_24dp.png b/webrtc/examples/objc/AppRTCMobile/ios/resources/ic_switch_video_black_24dp.png
new file mode 100644
index 0000000..85271c8
--- /dev/null
+++ b/webrtc/examples/objc/AppRTCMobile/ios/resources/ic_switch_video_black_24dp.png
Binary files differ
diff --git a/webrtc/examples/objc/AppRTCMobile/ios/resources/ic_switch_video_black_24dp@2x.png b/webrtc/examples/objc/AppRTCMobile/ios/resources/ic_switch_video_black_24dp@2x.png
new file mode 100644
index 0000000..62b13a6
--- /dev/null
+++ b/webrtc/examples/objc/AppRTCMobile/ios/resources/ic_switch_video_black_24dp@2x.png
Binary files differ
diff --git a/webrtc/examples/objc/AppRTCMobile/ios/resources/mozart.mp3 b/webrtc/examples/objc/AppRTCMobile/ios/resources/mozart.mp3
new file mode 100644
index 0000000..5981ba3
--- /dev/null
+++ b/webrtc/examples/objc/AppRTCMobile/ios/resources/mozart.mp3
Binary files differ
diff --git a/webrtc/examples/objc/AppRTCMobile/mac/APPRTCAppDelegate.h b/webrtc/examples/objc/AppRTCMobile/mac/APPRTCAppDelegate.h
new file mode 100644
index 0000000..95f3594
--- /dev/null
+++ b/webrtc/examples/objc/AppRTCMobile/mac/APPRTCAppDelegate.h
@@ -0,0 +1,14 @@
+/*
+ *  Copyright 2014 The WebRTC Project Authors. All rights reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#import <Cocoa/Cocoa.h>
+
+@interface APPRTCAppDelegate : NSObject<NSApplicationDelegate>
+@end
diff --git a/webrtc/examples/objc/AppRTCMobile/mac/APPRTCAppDelegate.m b/webrtc/examples/objc/AppRTCMobile/mac/APPRTCAppDelegate.m
new file mode 100644
index 0000000..20e6c27
--- /dev/null
+++ b/webrtc/examples/objc/AppRTCMobile/mac/APPRTCAppDelegate.m
@@ -0,0 +1,59 @@
+/*
+ *  Copyright 2014 The WebRTC Project Authors. All rights reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#if !defined(__has_feature) || !__has_feature(objc_arc)
+#error "This file requires ARC support."
+#endif
+
+#import "APPRTCAppDelegate.h"
+#import "APPRTCViewController.h"
+#import "WebRTC/RTCSSLAdapter.h"
+
+@interface APPRTCAppDelegate () <NSWindowDelegate>
+@end
+
+@implementation APPRTCAppDelegate {
+  APPRTCViewController* _viewController;
+  NSWindow* _window;
+}
+
+#pragma mark - NSApplicationDelegate
+
+- (void)applicationDidFinishLaunching:(NSNotification*)notification {
+  RTCInitializeSSL();
+  NSScreen* screen = [NSScreen mainScreen];
+  NSRect visibleRect = [screen visibleFrame];
+  NSRect windowRect = NSMakeRect(NSMidX(visibleRect),
+                                 NSMidY(visibleRect),
+                                 1320,
+                                 1140);
+  NSUInteger styleMask = NSTitledWindowMask | NSClosableWindowMask;
+  _window = [[NSWindow alloc] initWithContentRect:windowRect
+                                        styleMask:styleMask
+                                          backing:NSBackingStoreBuffered
+                                            defer:NO];
+  _window.delegate = self;
+  [_window makeKeyAndOrderFront:self];
+  [_window makeMainWindow];
+  _viewController = [[APPRTCViewController alloc] initWithNibName:nil
+                                                           bundle:nil];
+  [_window setContentView:[_viewController view]];
+}
+
+#pragma mark - NSWindow
+
+- (void)windowWillClose:(NSNotification*)notification {
+  [_viewController windowWillClose:notification];
+  RTCCleanupSSL();
+  [NSApp terminate:self];
+}
+
+@end
+
diff --git a/webrtc/examples/objc/AppRTCMobile/mac/APPRTCViewController.h b/webrtc/examples/objc/AppRTCMobile/mac/APPRTCViewController.h
new file mode 100644
index 0000000..306ecd9
--- /dev/null
+++ b/webrtc/examples/objc/AppRTCMobile/mac/APPRTCViewController.h
@@ -0,0 +1,17 @@
+/*
+ *  Copyright 2014 The WebRTC Project Authors. All rights reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#import <AppKit/AppKit.h>
+
+@interface APPRTCViewController : NSViewController
+
+- (void)windowWillClose:(NSNotification*)notification;
+
+@end
diff --git a/webrtc/examples/objc/AppRTCMobile/mac/APPRTCViewController.m b/webrtc/examples/objc/AppRTCMobile/mac/APPRTCViewController.m
new file mode 100644
index 0000000..710b8a5
--- /dev/null
+++ b/webrtc/examples/objc/AppRTCMobile/mac/APPRTCViewController.m
@@ -0,0 +1,328 @@
+/*
+ *  Copyright 2014 The WebRTC Project Authors. All rights reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#import "APPRTCViewController.h"
+
+#import <AVFoundation/AVFoundation.h>
+
+#import "WebRTC/RTCNSGLVideoView.h"
+#import "WebRTC/RTCVideoTrack.h"
+
+#import "ARDAppClient.h"
+
+static NSUInteger const kContentWidth = 1280;
+static NSUInteger const kContentHeight = 720;
+static NSUInteger const kRoomFieldWidth = 80;
+static NSUInteger const kLogViewHeight = 280;
+static NSUInteger const kPreviewWidth = 490;
+
+@class APPRTCMainView;
+@protocol APPRTCMainViewDelegate
+
+- (void)appRTCMainView:(APPRTCMainView*)mainView
+        didEnterRoomId:(NSString*)roomId;
+
+@end
+
+@interface APPRTCMainView : NSView
+
+@property(nonatomic, weak) id<APPRTCMainViewDelegate> delegate;
+@property(nonatomic, readonly) RTCNSGLVideoView* localVideoView;
+@property(nonatomic, readonly) RTCNSGLVideoView* remoteVideoView;
+
+- (void)displayLogMessage:(NSString*)message;
+
+@end
+
+@interface APPRTCMainView () <NSTextFieldDelegate, RTCNSGLVideoViewDelegate>
+@end
+@implementation APPRTCMainView  {
+  NSScrollView* _scrollView;
+  NSTextField* _roomLabel;
+  NSTextField* _roomField;
+  NSTextView* _logView;
+  CGSize _localVideoSize;
+  CGSize _remoteVideoSize;
+}
+
+@synthesize delegate = _delegate;
+@synthesize localVideoView = _localVideoView;
+@synthesize remoteVideoView = _remoteVideoView;
+
++ (BOOL)requiresConstraintBasedLayout {
+  return YES;
+}
+
+- (instancetype)initWithFrame:(NSRect)frame {
+  if (self = [super initWithFrame:frame]) {
+    [self setupViews];
+  }
+  return self;
+}
+
+- (void)updateConstraints {
+  NSParameterAssert(
+      _roomField != nil && _scrollView != nil && _remoteVideoView != nil);
+  [self removeConstraints:[self constraints]];
+  NSDictionary* viewsDictionary =
+      NSDictionaryOfVariableBindings(_roomLabel,
+                                     _roomField,
+                                     _scrollView,
+                                     _remoteVideoView,
+                                     _localVideoView);
+
+  NSSize remoteViewSize = [self remoteVideoViewSize];
+  NSDictionary* metrics = @{
+    @"kLogViewHeight" : @(kLogViewHeight),
+    @"kPreviewWidth" : @(kPreviewWidth),
+    @"kRoomFieldWidth" : @(kRoomFieldWidth),
+    @"remoteViewWidth" : @(remoteViewSize.width),
+    @"remoteViewHeight" : @(remoteViewSize.height),
+    @"localViewHeight" : @(remoteViewSize.height),
+    @"scrollViewWidth" : @(kContentWidth - kPreviewWidth),
+  };
+  // Declare this separately to avoid compiler warning about splitting string
+  // within an NSArray expression.
+  NSString* verticalConstraint =
+      @"V:|-[_roomLabel]-[_roomField]-[_scrollView(kLogViewHeight)]"
+       "-[_remoteVideoView(remoteViewHeight)]-|";
+  NSArray* constraintFormats = @[
+      verticalConstraint,
+      @"V:[_localVideoView]-[_remoteVideoView]",
+      @"V:[_localVideoView(kLogViewHeight)]",
+      @"|-[_roomLabel]",
+      @"|-[_roomField(kRoomFieldWidth)]",
+      @"|-[_scrollView(scrollViewWidth)]",
+      @"[_scrollView]-[_localVideoView]",
+      @"|-[_remoteVideoView(remoteViewWidth)]-|",
+      @"[_localVideoView(kPreviewWidth)]-|",
+  ];
+  for (NSString* constraintFormat in constraintFormats) {
+    NSArray* constraints =
+        [NSLayoutConstraint constraintsWithVisualFormat:constraintFormat
+                                                options:0
+                                                metrics:metrics
+                                                  views:viewsDictionary];
+    for (NSLayoutConstraint* constraint in constraints) {
+      [self addConstraint:constraint];
+    }
+  }
+  [super updateConstraints];
+}
+
+- (void)displayLogMessage:(NSString*)message {
+  _logView.string =
+      [NSString stringWithFormat:@"%@%@\n", _logView.string, message];
+  NSRange range = NSMakeRange([_logView.string length], 0);
+  [_logView scrollRangeToVisible:range];
+}
+
+#pragma mark - NSControl delegate
+
+- (void)controlTextDidEndEditing:(NSNotification*)notification {
+  NSDictionary* userInfo = [notification userInfo];
+  NSInteger textMovement = [userInfo[@"NSTextMovement"] intValue];
+  if (textMovement == NSReturnTextMovement) {
+    [self.delegate appRTCMainView:self didEnterRoomId:_roomField.stringValue];
+  }
+}
+
+#pragma mark - RTCNSGLVideoViewDelegate
+
+- (void)videoView:(RTCNSGLVideoView*)videoView
+    didChangeVideoSize:(NSSize)size {
+  if (videoView == _remoteVideoView) {
+    _remoteVideoSize = size;
+  } else if (videoView == _localVideoView) {
+    _localVideoSize = size;
+  } else {
+    return;
+  }
+  [self setNeedsUpdateConstraints:YES];
+}
+
+#pragma mark - Private
+
+- (void)setupViews {
+  NSParameterAssert([[self subviews] count] == 0);
+
+  _roomLabel = [[NSTextField alloc] initWithFrame:NSZeroRect];
+  [_roomLabel setTranslatesAutoresizingMaskIntoConstraints:NO];
+  [_roomLabel setBezeled:NO];
+  [_roomLabel setDrawsBackground:NO];
+  [_roomLabel setEditable:NO];
+  [_roomLabel setStringValue:@"Enter AppRTC room id:"];
+  [self addSubview:_roomLabel];
+
+  _roomField = [[NSTextField alloc] initWithFrame:NSZeroRect];
+  [_roomField setTranslatesAutoresizingMaskIntoConstraints:NO];
+  [self addSubview:_roomField];
+  [_roomField setEditable:YES];
+  [_roomField setDelegate:self];
+
+  _logView = [[NSTextView alloc] initWithFrame:NSZeroRect];
+  [_logView setMinSize:NSMakeSize(0, kLogViewHeight)];
+  [_logView setMaxSize:NSMakeSize(FLT_MAX, FLT_MAX)];
+  [_logView setVerticallyResizable:YES];
+  [_logView setAutoresizingMask:NSViewWidthSizable];
+  NSTextContainer* textContainer = [_logView textContainer];
+  NSSize containerSize = NSMakeSize(kContentWidth, FLT_MAX);
+  [textContainer setContainerSize:containerSize];
+  [textContainer setWidthTracksTextView:YES];
+  [_logView setEditable:NO];
+
+  _scrollView = [[NSScrollView alloc] initWithFrame:NSZeroRect];
+  [_scrollView setTranslatesAutoresizingMaskIntoConstraints:NO];
+  [_scrollView setHasVerticalScroller:YES];
+  [_scrollView setDocumentView:_logView];
+  [self addSubview:_scrollView];
+
+  NSOpenGLPixelFormatAttribute attributes[] = {
+    NSOpenGLPFADoubleBuffer,
+    NSOpenGLPFADepthSize, 24,
+    NSOpenGLPFAOpenGLProfile,
+    NSOpenGLProfileVersion3_2Core,
+    0
+  };
+  NSOpenGLPixelFormat* pixelFormat =
+      [[NSOpenGLPixelFormat alloc] initWithAttributes:attributes];
+  _remoteVideoView = [[RTCNSGLVideoView alloc] initWithFrame:NSZeroRect
+                                                 pixelFormat:pixelFormat];
+  [_remoteVideoView setTranslatesAutoresizingMaskIntoConstraints:NO];
+  _remoteVideoView.delegate = self;
+  [self addSubview:_remoteVideoView];
+
+  _localVideoView = [[RTCNSGLVideoView alloc] initWithFrame:NSZeroRect
+                                                 pixelFormat:pixelFormat];
+  [_localVideoView setTranslatesAutoresizingMaskIntoConstraints:NO];
+  _localVideoView.delegate = self;
+  [self addSubview:_localVideoView];
+}
+
+- (NSSize)remoteVideoViewSize {
+  NSInteger width = MAX(_remoteVideoSize.width, kContentWidth);
+  NSInteger height = (width/16) * 9;
+  return NSMakeSize(width, height);
+}
+
+@end
+
+@interface APPRTCViewController ()
+    <ARDAppClientDelegate, APPRTCMainViewDelegate>
+@property(nonatomic, readonly) APPRTCMainView* mainView;
+@end
+
+@implementation APPRTCViewController {
+  ARDAppClient* _client;
+  RTCVideoTrack* _localVideoTrack;
+  RTCVideoTrack* _remoteVideoTrack;
+}
+
+- (void)dealloc {
+  [self disconnect];
+}
+
+- (void)loadView {
+  APPRTCMainView* view = [[APPRTCMainView alloc] initWithFrame:NSZeroRect];
+  [view setTranslatesAutoresizingMaskIntoConstraints:NO];
+  view.delegate = self;
+  self.view = view;
+}
+
+- (void)windowWillClose:(NSNotification*)notification {
+  [self disconnect];
+}
+
+#pragma mark - ARDAppClientDelegate
+
+- (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
+    didChangeConnectionState:(RTCIceConnectionState)state {
+}
+
+- (void)appClient:(ARDAppClient *)client
+    didReceiveLocalVideoTrack:(RTCVideoTrack *)localVideoTrack {
+  _localVideoTrack = localVideoTrack;
+  [_localVideoTrack addRenderer:self.mainView.localVideoView];
+}
+
+- (void)appClient:(ARDAppClient *)client
+    didReceiveRemoteVideoTrack:(RTCVideoTrack *)remoteVideoTrack {
+  _remoteVideoTrack = remoteVideoTrack;
+  [_remoteVideoTrack addRenderer:self.mainView.remoteVideoView];
+}
+
+- (void)appClient:(ARDAppClient *)client
+         didError:(NSError *)error {
+  [self showAlertWithMessage:[NSString stringWithFormat:@"%@", error]];
+  [self disconnect];
+}
+
+- (void)appClient:(ARDAppClient *)client
+      didGetStats:(NSArray *)stats {
+}
+
+#pragma mark - APPRTCMainViewDelegate
+
+- (void)appRTCMainView:(APPRTCMainView*)mainView
+        didEnterRoomId:(NSString*)roomId {
+  [_client disconnect];
+  ARDAppClient *client = [[ARDAppClient alloc] initWithDelegate:self];
+  [client connectToRoomWithId:roomId
+                   isLoopback:NO
+                  isAudioOnly:NO
+            shouldMakeAecDump:NO
+        shouldUseLevelControl:NO];
+  _client = client;
+}
+
+#pragma mark - Private
+
+- (APPRTCMainView*)mainView {
+  return (APPRTCMainView*)self.view;
+}
+
+- (void)showAlertWithMessage:(NSString*)message {
+  NSAlert* alert = [[NSAlert alloc] init];
+  [alert setMessageText:message];
+  [alert runModal];
+}
+
+- (void)resetUI {
+  [_remoteVideoTrack removeRenderer:self.mainView.remoteVideoView];
+  [_localVideoTrack removeRenderer:self.mainView.localVideoView];
+  _remoteVideoTrack = nil;
+  _localVideoTrack = nil;
+  [self.mainView.remoteVideoView renderFrame:nil];
+  [self.mainView.localVideoView renderFrame:nil];
+}
+
+- (void)disconnect {
+  [self resetUI];
+  [_client disconnect];
+}
+
+@end
diff --git a/webrtc/examples/objc/AppRTCMobile/mac/Info.plist b/webrtc/examples/objc/AppRTCMobile/mac/Info.plist
new file mode 100644
index 0000000..4dcb240
--- /dev/null
+++ b/webrtc/examples/objc/AppRTCMobile/mac/Info.plist
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!DOCTYPE plist PUBLIC "-//Apple/DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+  <key>CFBundleDevelopmentRegion</key>
+  <string>en</string>
+  <key>CFBundleDisplayName</key>
+  <string>${PRODUCT_NAME}</string>
+  <key>CFBundleExecutable</key>
+  <string>${EXECUTABLE_NAME}</string>
+  <key>CFBundleIdentifier</key>
+  <string>com.Google.${PRODUCT_NAME:rfc1034identifier}</string>
+  <key>CFBundleInfoDictionaryVersion</key>
+  <string>6.0</string>
+  <key>CFBundleName</key>
+  <string>${PRODUCT_NAME}</string>
+  <key>CFBundlePackageType</key>
+  <string>APPL</string>
+  <key>CFBundleShortVersionString</key>
+  <string>1.0</string>
+  <key>CFBundleVersion</key>
+  <string>1.0</string>
+  <key>LSMinimumSystemVersion</key>
+  <string>${MACOSX_DEPLOYMENT_TARGET}</string>
+  <key>NSPrincipalClass</key>
+  <string>NSApplication</string>
+</dict>
+</plist>
\ No newline at end of file
diff --git a/webrtc/examples/objc/AppRTCMobile/mac/main.m b/webrtc/examples/objc/AppRTCMobile/mac/main.m
new file mode 100644
index 0000000..79b17f5
--- /dev/null
+++ b/webrtc/examples/objc/AppRTCMobile/mac/main.m
@@ -0,0 +1,22 @@
+/*
+ *  Copyright 2014 The WebRTC Project Authors. All rights reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#import <AppKit/AppKit.h>
+
+#import "APPRTCAppDelegate.h"
+
+int main(int argc, char* argv[]) {
+  @autoreleasepool {
+    [NSApplication sharedApplication];
+    APPRTCAppDelegate* delegate = [[APPRTCAppDelegate alloc] init];
+    [NSApp setDelegate:delegate];
+    [NSApp run];
+  }
+}
diff --git a/webrtc/examples/objc/AppRTCMobile/tests/ARDAppClientTest.mm b/webrtc/examples/objc/AppRTCMobile/tests/ARDAppClientTest.mm
new file mode 100644
index 0000000..c1fc08c
--- /dev/null
+++ b/webrtc/examples/objc/AppRTCMobile/tests/ARDAppClientTest.mm
@@ -0,0 +1,350 @@
+/*
+ *  Copyright 2014 The WebRTC Project Authors. All rights reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#import <Foundation/Foundation.h>
+#import <OCMock/OCMock.h>
+
+#include "webrtc/base/gunit.h"
+#include "webrtc/base/ssladapter.h"
+
+#import "WebRTC/RTCMediaConstraints.h"
+#import "WebRTC/RTCPeerConnectionFactory.h"
+#import "WebRTC/RTCSessionDescription.h"
+
+#import "ARDAppClient+Internal.h"
+#import "ARDJoinResponse+Internal.h"
+#import "ARDMessageResponse+Internal.h"
+#import "ARDSDPUtils.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 join response.
+  ARDJoinResponse *joinResponse = [[ARDJoinResponse alloc] init];
+  joinResponse.result = kARDJoinResultTypeSuccess;
+  joinResponse.roomId = roomId;
+  joinResponse.clientId = clientId;
+  joinResponse.isInitiator = isInitiator;
+  joinResponse.messages = messages;
+
+  // Successful message response.
+  ARDMessageResponse *messageResponse = [[ARDMessageResponse alloc] init];
+  messageResponse.result = kARDMessageResultTypeSuccess;
+
+  // Return join response from above on join.
+  [[[mockRoomServerClient stub] andDo:^(NSInvocation *invocation) {
+    __unsafe_unretained void (^completionHandler)(ARDJoinResponse *response,
+                                                  NSError *error);
+    [invocation getArgument:&completionHandler atIndex:3];
+    completionHandler(joinResponse, nil);
+  }] joinRoomWithRoomId:roomId isLoopback:NO completionHandler:[OCMArg any]];
+
+  // Return message response from above on join.
+  [[[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 leave.
+  [[[mockRoomServerClient stub] andDo:^(NSInvocation *invocation) {
+    __unsafe_unretained void (^completionHandler)(NSError *error);
+    [invocation getArgument:&completionHandler atIndex:4];
+    if (completionHandler) {
+      completionHandler(nil);
+    }
+  }] leaveRoomWithRoomId: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:RTCIceConnectionStateConnected];
+
+  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
+// RTCIceConnectionStateConnected 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] initWithMandatoryConstraints:nil
+                                            optionalConstraints:nil];
+  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] initWithMandatoryConstraints:nil
+                                            optionalConstraints:nil];
+  weakAnswerer = answerer;
+
+  // Kick off connection.
+  [caller connectToRoomWithId:roomId
+                   isLoopback:NO
+                  isAudioOnly:NO
+            shouldMakeAecDump:NO
+        shouldUseLevelControl:NO];
+  [answerer connectToRoomWithId:roomId
+                     isLoopback:NO
+                    isAudioOnly:NO
+              shouldMakeAecDump:NO
+          shouldUseLevelControl:NO];
+  [self waitForExpectationsWithTimeout:20 handler:^(NSError *error) {
+    if (error) {
+      NSLog(@"Expectations error: %@", error);
+    }
+  }];
+}
+
+@end
+
+@interface ARDSDPUtilsTest : ARDTestCase
+- (void)testPreferVideoCodec;
+@end
+
+@implementation ARDSDPUtilsTest
+
+- (void)testPreferVideoCodec {
+  NSString *sdp = @("m=video 9 RTP/SAVPF 100 116 117 96 120\n"
+                    "a=rtpmap:120 H264/90000\n");
+  NSString *expectedSdp = @("m=video 9 RTP/SAVPF 120 100 116 117 96\n"
+                            "a=rtpmap:120 H264/90000\n");
+  RTCSessionDescription* desc =
+      [[RTCSessionDescription alloc] initWithType:RTCSdpTypeOffer sdp:sdp];
+  RTCSessionDescription *h264Desc =
+      [ARDSDPUtils descriptionForDescription:desc
+                         preferredVideoCodec:@"H264"];
+  EXPECT_TRUE([h264Desc.description isEqualToString:expectedSdp]);
+}
+
+@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];
+  }
+}
+
+TEST_F(SignalingTest, SDPTest) {
+  @autoreleasepool {
+    ARDSDPUtilsTest *test = [[ARDSDPUtilsTest alloc] init];
+    [test testPreferVideoCodec];
+  }
+}
+
+
diff --git a/webrtc/examples/objc/AppRTCMobile/third_party/SocketRocket/LICENSE b/webrtc/examples/objc/AppRTCMobile/third_party/SocketRocket/LICENSE
new file mode 100644
index 0000000..c01a79c
--- /dev/null
+++ b/webrtc/examples/objc/AppRTCMobile/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/webrtc/examples/objc/AppRTCMobile/third_party/SocketRocket/SRWebSocket.h b/webrtc/examples/objc/AppRTCMobile/third_party/SocketRocket/SRWebSocket.h
new file mode 100644
index 0000000..5cce725
--- /dev/null
+++ b/webrtc/examples/objc/AppRTCMobile/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/webrtc/examples/objc/AppRTCMobile/third_party/SocketRocket/SRWebSocket.m b/webrtc/examples/objc/AppRTCMobile/third_party/SocketRocket/SRWebSocket.m
new file mode 100644
index 0000000..b8add7f
--- /dev/null
+++ b/webrtc/examples/objc/AppRTCMobile/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