/* * 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" #if defined(WEBRTC_IOS) #import "RTCAVFoundationVideoSource.h" #endif #import "RTCFileLogger.h" #import "RTCICEServer.h" #import "RTCLogging.h" #import "RTCMediaConstraints.h" #import "RTCMediaStream.h" #import "RTCPair.h" #import "RTCPeerConnectionInterface.h" #import "RTCVideoCapturer.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; // 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; - (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)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)rsClient signalingChannel:(id)channel turnClient:(id)turnClient delegate:(id)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 getStatsWithDelegate:strongSelf mediaStreamTrack:nil statsOutputLevel:RTCStatsOutputLevelDebug]; }]; } 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 { NSParameterAssert(roomId.length); NSParameterAssert(_state == kARDAppClientStateDisconnected); _isLoopback = isLoopback; _isAudioOnly = isAudioOnly; self.state = kARDAppClientStateConnecting; // 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]; _peerConnection = nil; self.state = kARDAppClientStateDisconnected; } #pragma mark - ARDSignalingChannelDelegate - (void)channel:(id)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: [_messageQueue addObject:message]; break; case kARDSignalingMessageTypeBye: // Disconnects can be processed immediately. [self processSignalingMessage:message]; return; } [self drainMessageQueueIfReady]; } - (void)channel:(id)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 signalingStateChanged:(RTCSignalingState)stateChanged { RTCLog(@"Signaling state changed: %d", stateChanged); } - (void)peerConnection:(RTCPeerConnection *)peerConnection addedStream:(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 removedStream:(RTCMediaStream *)stream { RTCLog(@"Stream was removed."); } - (void)peerConnectionOnRenegotiationNeeded: (RTCPeerConnection *)peerConnection { RTCLog(@"WARNING: Renegotiation needed but unimplemented."); } - (void)peerConnection:(RTCPeerConnection *)peerConnection iceConnectionChanged:(RTCICEConnectionState)newState { RTCLog(@"ICE state changed: %d", newState); dispatch_async(dispatch_get_main_queue(), ^{ [_delegate appClient:self didChangeConnectionState:newState]; }); } - (void)peerConnection:(RTCPeerConnection *)peerConnection iceGatheringChanged:(RTCICEGatheringState)newState { RTCLog(@"ICE gathering state changed: %d", newState); } - (void)peerConnection:(RTCPeerConnection *)peerConnection gotICECandidate:(RTCICECandidate *)candidate { dispatch_async(dispatch_get_main_queue(), ^{ ARDICECandidateMessage *message = [[ARDICECandidateMessage alloc] initWithCandidate:candidate]; [self sendSignalingMessage:message]; }); } - (void)peerConnection:(RTCPeerConnection *)peerConnection didOpenDataChannel:(RTCDataChannel *)dataChannel { } #pragma mark - RTCStatsDelegate - (void)peerConnection:(RTCPeerConnection *)peerConnection didGetStats:(NSArray *)stats { dispatch_async(dispatch_get_main_queue(), ^{ [_delegate appClient:self didGetStats:stats]; }); } #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"]; [_peerConnection setLocalDescriptionWithDelegate:self sessionDescription:sdpPreferringH264]; 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]; [_peerConnection createAnswerWithDelegate:self constraints:constraints]; } }); } #pragma mark - Private - (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 media stream and add it to the peer connection. RTCMediaStream *localStream = [self createLocalMediaStream]; [_peerConnection addStream:localStream]; if (_isInitiator) { // Send offer. [_peerConnection createOfferWithDelegate:self constraints:[self defaultOfferConstraints]]; } else { // Check if we've received an offer. [self drainMessageQueueIfReady]; } } // 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"]; [_peerConnection setRemoteDescriptionWithDelegate:self sessionDescription:sdpPreferringH264]; break; } case kARDSignalingMessageTypeCandidate: { ARDICECandidateMessage *candidateMessage = (ARDICECandidateMessage *)message; [_peerConnection addICECandidate:candidateMessage.candidate]; break; } case kARDSignalingMessageTypeBye: // Other client disconnected. // TODO(tkchin): support waiting in room for next client. For now just // disconnect. [self disconnect]; break; } } // 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]; } } - (RTCMediaStream *)createLocalMediaStream { RTCMediaStream* localStream = [_factory mediaStreamWithLabel:@"ARDAMS"]; RTCVideoTrack* localVideoTrack = [self createLocalVideoTrack]; if (localVideoTrack) { [localStream addVideoTrack:localVideoTrack]; [_delegate appClient:self didReceiveLocalVideoTrack:localVideoTrack]; } [localStream addAudioTrack:[_factory audioTrackWithID:@"ARDAMSa0"]]; return localStream; } - (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. // TODO(tkchin): local video capture for OSX. See // https://code.google.com/p/webrtc/issues/detail?id=3417. #if !TARGET_IPHONE_SIMULATOR && TARGET_OS_IPHONE if (!_isAudioOnly) { RTCMediaConstraints *mediaConstraints = [self defaultMediaStreamConstraints]; RTCAVFoundationVideoSource *source = [[RTCAVFoundationVideoSource alloc] initWithFactory:_factory constraints:mediaConstraints]; localVideoTrack = [[RTCVideoTrack alloc] initWithFactory:_factory source:source trackId:@"ARDAMSv0"]; } #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 *)defaultMediaStreamConstraints { RTCMediaConstraints* constraints = [[RTCMediaConstraints alloc] initWithMandatoryConstraints:nil optionalConstraints:nil]; return constraints; } - (RTCMediaConstraints *)defaultAnswerConstraints { return [self defaultOfferConstraints]; } - (RTCMediaConstraints *)defaultOfferConstraints { NSArray *mandatoryConstraints = @[ [[RTCPair alloc] initWithKey:@"OfferToReceiveAudio" value:@"true"], [[RTCPair alloc] initWithKey:@"OfferToReceiveVideo" value:@"true"] ]; RTCMediaConstraints* constraints = [[RTCMediaConstraints alloc] initWithMandatoryConstraints:mandatoryConstraints optionalConstraints:nil]; return constraints; } - (RTCMediaConstraints *)defaultPeerConnectionConstraints { if (_defaultPeerConnectionConstraints) { return _defaultPeerConnectionConstraints; } NSString *value = _isLoopback ? @"false" : @"true"; NSArray *optionalConstraints = @[ [[RTCPair alloc] initWithKey:@"DtlsSrtpKeyAgreement" value:value] ]; RTCMediaConstraints* constraints = [[RTCMediaConstraints alloc] initWithMandatoryConstraints:nil optionalConstraints:optionalConstraints]; return constraints; } - (RTCICEServer *)defaultSTUNServer { NSURL *defaultSTUNServerURL = [NSURL URLWithString:kARDDefaultSTUNServerUrl]; return [[RTCICEServer alloc] initWithURI:defaultSTUNServerURL username:@"" password:@""]; } #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