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