Index: webrtc/examples/objc/AppRTCDemo/ARDAppClient.m |
diff --git a/webrtc/examples/objc/AppRTCDemo/ARDAppClient.m b/webrtc/examples/objc/AppRTCDemo/ARDAppClient.m |
new file mode 100644 |
index 0000000000000000000000000000000000000000..1020621cc1bd2b91340963ac20609688aaacd8f6 |
--- /dev/null |
+++ b/webrtc/examples/objc/AppRTCDemo/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 |