/* * libjingle * Copyright 2014, Google Inc. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * 3. The name of the author may not be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #import "ARDAppClient.h" #import #import "ARDMessageResponse.h" #import "ARDRegisterResponse.h" #import "ARDSignalingMessage.h" #import "ARDUtilities.h" #import "ARDWebSocketChannel.h" #import "RTCICECandidate+JSON.h" #import "RTCICEServer+JSON.h" #import "RTCMediaConstraints.h" #import "RTCMediaStream.h" #import "RTCPair.h" #import "RTCPeerConnection.h" #import "RTCPeerConnectionDelegate.h" #import "RTCPeerConnectionFactory.h" #import "RTCSessionDescription+JSON.h" #import "RTCSessionDescriptionDelegate.h" #import "RTCVideoCapturer.h" #import "RTCVideoTrack.h" // TODO(tkchin): move these to a configuration object. static NSString *kARDRoomServerHostUrl = @"https://apprtc.appspot.com"; static NSString *kARDRoomServerRegisterFormat = @"https://apprtc.appspot.com/register/%@"; static NSString *kARDRoomServerMessageFormat = @"https://apprtc.appspot.com/message/%@/%@"; static NSString *kARDRoomServerByeFormat = @"https://apprtc.appspot.com/bye/%@/%@"; static NSString *kARDDefaultSTUNServerUrl = @"stun:stun.l.google.com:19302"; // TODO(tkchin): figure out a better username for CEOD statistics. static NSString *kARDTurnRequestUrl = @"https://computeengineondemand.appspot.com" @"/turn?username=iapprtc&key=4080218913"; static NSString *kARDAppClientErrorDomain = @"ARDAppClient"; static NSInteger kARDAppClientErrorUnknown = -1; static NSInteger kARDAppClientErrorRoomFull = -2; static NSInteger kARDAppClientErrorCreateSDP = -3; static NSInteger kARDAppClientErrorSetSDP = -4; static NSInteger kARDAppClientErrorNetwork = -5; static NSInteger kARDAppClientErrorInvalidClient = -6; static NSInteger kARDAppClientErrorInvalidRoom = -7; @interface ARDAppClient () @property(nonatomic, strong) ARDWebSocketChannel *channel; @property(nonatomic, strong) RTCPeerConnection *peerConnection; @property(nonatomic, strong) RTCPeerConnectionFactory *factory; @property(nonatomic, strong) NSMutableArray *messageQueue; @property(nonatomic, assign) BOOL isTurnComplete; @property(nonatomic, assign) BOOL hasReceivedSdp; @property(nonatomic, readonly) BOOL isRegisteredWithRoomServer; @property(nonatomic, strong) NSString *roomId; @property(nonatomic, strong) NSString *clientId; @property(nonatomic, assign) BOOL isInitiator; @property(nonatomic, strong) NSMutableArray *iceServers; @property(nonatomic, strong) NSURL *webSocketURL; @property(nonatomic, strong) NSURL *webSocketRestURL; @end @implementation ARDAppClient @synthesize delegate = _delegate; @synthesize state = _state; @synthesize channel = _channel; @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; - (instancetype)initWithDelegate:(id)delegate { if (self = [super init]) { _delegate = delegate; _factory = [[RTCPeerConnectionFactory alloc] init]; _messageQueue = [NSMutableArray array]; _iceServers = [NSMutableArray arrayWithObject:[self defaultSTUNServer]]; } return self; } - (void)dealloc { [self disconnect]; } - (void)setState:(ARDAppClientState)state { if (_state == state) { return; } _state = state; [_delegate appClient:self didChangeState:_state]; } - (void)connectToRoomWithId:(NSString *)roomId options:(NSDictionary *)options { NSParameterAssert(roomId.length); NSParameterAssert(_state == kARDAppClientStateDisconnected); self.state = kARDAppClientStateConnecting; // Request TURN. __weak ARDAppClient *weakSelf = self; NSURL *turnRequestURL = [NSURL URLWithString:kARDTurnRequestUrl]; [self requestTURNServersWithURL:turnRequestURL completionHandler:^(NSArray *turnServers) { ARDAppClient *strongSelf = weakSelf; [strongSelf.iceServers addObjectsFromArray:turnServers]; strongSelf.isTurnComplete = YES; [strongSelf startSignalingIfReady]; }]; // Register with room server. [self registerWithRoomServerForRoomId:roomId completionHandler:^(ARDRegisterResponse *response) { ARDAppClient *strongSelf = weakSelf; if (!response || response.result != kARDRegisterResultTypeSuccess) { NSLog(@"Failed to register with room server. Result:%d", (int)response.result); [strongSelf disconnect]; NSDictionary *userInfo = @{ NSLocalizedDescriptionKey: @"Room is full.", }; NSError *error = [[NSError alloc] initWithDomain:kARDAppClientErrorDomain code:kARDAppClientErrorRoomFull userInfo:userInfo]; [strongSelf.delegate appClient:strongSelf didError:error]; return; } NSLog(@"Registered with room server."); 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.isRegisteredWithRoomServer) { [self unregisterWithRoomServer]; } if (_channel) { if (_channel.state == kARDWebSocketChannelStateRegistered) { // Tell the other client we're hanging up. ARDByeMessage *byeMessage = [[ARDByeMessage alloc] init]; NSData *byeData = [byeMessage JSONData]; [_channel sendData:byeData]; } // Disconnect from collider. _channel = nil; } _clientId = nil; _roomId = nil; _isInitiator = NO; _hasReceivedSdp = NO; _messageQueue = [NSMutableArray array]; _peerConnection = nil; self.state = kARDAppClientStateDisconnected; } #pragma mark - ARDWebSocketChannelDelegate - (void)channel:(ARDWebSocketChannel *)channel didReceiveMessage:(ARDSignalingMessage *)message { switch (message.type) { case kARDSignalingMessageTypeOffer: case kARDSignalingMessageTypeAnswer: _hasReceivedSdp = YES; [_messageQueue insertObject:message atIndex:0]; break; case kARDSignalingMessageTypeCandidate: [_messageQueue addObject:message]; break; case kARDSignalingMessageTypeBye: [self processSignalingMessage:message]; return; } [self drainMessageQueueIfReady]; } - (void)channel:(ARDWebSocketChannel *)channel didChangeState:(ARDWebSocketChannelState)state { switch (state) { case kARDWebSocketChannelStateOpen: break; case kARDWebSocketChannelStateRegistered: break; case kARDWebSocketChannelStateClosed: case kARDWebSocketChannelStateError: // TODO(tkchin): reconnection scenarios. Right now we just disconnect // completely if the websocket connection fails. [self disconnect]; break; } } #pragma mark - RTCPeerConnectionDelegate - (void)peerConnection:(RTCPeerConnection *)peerConnection signalingStateChanged:(RTCSignalingState)stateChanged { NSLog(@"Signaling state changed: %d", stateChanged); } - (void)peerConnection:(RTCPeerConnection *)peerConnection addedStream:(RTCMediaStream *)stream { dispatch_async(dispatch_get_main_queue(), ^{ NSLog(@"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 { NSLog(@"Stream was removed."); } - (void)peerConnectionOnRenegotiationNeeded: (RTCPeerConnection *)peerConnection { NSLog(@"WARNING: Renegotiation needed but unimplemented."); } - (void)peerConnection:(RTCPeerConnection *)peerConnection iceConnectionChanged:(RTCICEConnectionState)newState { NSLog(@"ICE state changed: %d", newState); } - (void)peerConnection:(RTCPeerConnection *)peerConnection iceGatheringChanged:(RTCICEGatheringState)newState { NSLog(@"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 - RTCSessionDescriptionDelegate - (void)peerConnection:(RTCPeerConnection *)peerConnection didCreateSessionDescription:(RTCSessionDescription *)sdp error:(NSError *)error { dispatch_async(dispatch_get_main_queue(), ^{ if (error) { NSLog(@"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; } [_peerConnection setLocalDescriptionWithDelegate:self sessionDescription:sdp]; ARDSessionDescriptionMessage *message = [[ARDSessionDescriptionMessage alloc] initWithDescription:sdp]; [self sendSignalingMessage:message]; }); } - (void)peerConnection:(RTCPeerConnection *)peerConnection didSetSessionDescriptionWithError:(NSError *)error { dispatch_async(dispatch_get_main_queue(), ^{ if (error) { NSLog(@"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)isRegisteredWithRoomServer { return _clientId.length; } - (void)startSignalingIfReady { if (!_isTurnComplete || !self.isRegisteredWithRoomServer) { return; } self.state = kARDAppClientStateConnected; // Create peer connection. RTCMediaConstraints *constraints = [self defaultPeerConnectionConstraints]; _peerConnection = [_factory peerConnectionWithICEServers:_iceServers constraints:constraints delegate:self]; RTCMediaStream *localStream = [self createLocalMediaStream]; [_peerConnection addStream:localStream]; if (_isInitiator) { [self sendOffer]; } else { [self waitForAnswer]; } } - (void)sendOffer { [_peerConnection createOfferWithDelegate:self constraints:[self defaultOfferConstraints]]; } - (void)waitForAnswer { [self drainMessageQueueIfReady]; } - (void)drainMessageQueueIfReady { if (!_peerConnection || !_hasReceivedSdp) { return; } for (ARDSignalingMessage *message in _messageQueue) { [self processSignalingMessage:message]; } [_messageQueue removeAllObjects]; } - (void)processSignalingMessage:(ARDSignalingMessage *)message { NSParameterAssert(_peerConnection || message.type == kARDSignalingMessageTypeBye); switch (message.type) { case kARDSignalingMessageTypeOffer: case kARDSignalingMessageTypeAnswer: { ARDSessionDescriptionMessage *sdpMessage = (ARDSessionDescriptionMessage *)message; RTCSessionDescription *description = sdpMessage.sessionDescription; [_peerConnection setRemoteDescriptionWithDelegate:self sessionDescription:description]; 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; } } - (void)sendSignalingMessage:(ARDSignalingMessage *)message { if (_isInitiator) { [self sendSignalingMessageToRoomServer:message completionHandler:nil]; } else { [self sendSignalingMessageToCollider:message]; } } - (RTCMediaStream *)createLocalMediaStream { RTCMediaStream* localStream = [_factory mediaStreamWithLabel:@"ARDAMS"]; 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 NSString *cameraID = nil; for (AVCaptureDevice *captureDevice in [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo]) { if (captureDevice.position == AVCaptureDevicePositionFront) { cameraID = [captureDevice localizedName]; break; } } NSAssert(cameraID, @"Unable to get the front camera id"); RTCVideoCapturer *capturer = [RTCVideoCapturer capturerWithDeviceName:cameraID]; RTCMediaConstraints *mediaConstraints = [self defaultMediaStreamConstraints]; RTCVideoSource *videoSource = [_factory videoSourceWithCapturer:capturer constraints:mediaConstraints]; localVideoTrack = [_factory videoTrackWithID:@"ARDAMSv0" source:videoSource]; if (localVideoTrack) { [localStream addVideoTrack:localVideoTrack]; } [_delegate appClient:self didReceiveLocalVideoTrack:localVideoTrack]; #endif [localStream addAudioTrack:[_factory audioTrackWithID:@"ARDAMSa0"]]; return localStream; } - (void)requestTURNServersWithURL:(NSURL *)requestURL completionHandler:(void (^)(NSArray *turnServers))completionHandler { NSParameterAssert([requestURL absoluteString].length); NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:requestURL]; // We need to set origin because TURN provider whitelists requests based on // origin. [request addValue:@"Mozilla/5.0" forHTTPHeaderField:@"user-agent"]; [request addValue:kARDRoomServerHostUrl forHTTPHeaderField:@"origin"]; [NSURLConnection sendAsyncRequest:request completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) { NSArray *turnServers = [NSArray array]; if (error) { NSLog(@"Unable to get TURN server."); completionHandler(turnServers); return; } NSDictionary *dict = [NSDictionary dictionaryWithJSONData:data]; turnServers = [RTCICEServer serversFromCEODJSONDictionary:dict]; completionHandler(turnServers); }]; } #pragma mark - Room server methods - (void)registerWithRoomServerForRoomId:(NSString *)roomId completionHandler:(void (^)(ARDRegisterResponse *))completionHandler { NSString *urlString = [NSString stringWithFormat:kARDRoomServerRegisterFormat, roomId]; NSURL *roomURL = [NSURL URLWithString:urlString]; NSLog(@"Registering with room server."); __weak ARDAppClient *weakSelf = self; [NSURLConnection sendAsyncPostToURL:roomURL withData:nil completionHandler:^(BOOL succeeded, NSData *data) { ARDAppClient *strongSelf = weakSelf; if (!succeeded) { NSError *error = [self roomServerNetworkError]; [strongSelf.delegate appClient:strongSelf didError:error]; completionHandler(nil); return; } ARDRegisterResponse *response = [ARDRegisterResponse responseFromJSONData:data]; completionHandler(response); }]; } - (void)sendSignalingMessageToRoomServer:(ARDSignalingMessage *)message completionHandler:(void (^)(ARDMessageResponse *))completionHandler { NSData *data = [message JSONData]; NSString *urlString = [NSString stringWithFormat: kARDRoomServerMessageFormat, _roomId, _clientId]; NSURL *url = [NSURL URLWithString:urlString]; NSLog(@"C->RS POST: %@", message); __weak ARDAppClient *weakSelf = self; [NSURLConnection sendAsyncPostToURL:url withData:data completionHandler:^(BOOL succeeded, NSData *data) { ARDAppClient *strongSelf = weakSelf; if (!succeeded) { NSError *error = [self roomServerNetworkError]; [strongSelf.delegate appClient:strongSelf didError:error]; return; } ARDMessageResponse *response = [ARDMessageResponse responseFromJSONData:data]; NSError *error = nil; switch (response.result) { case kARDMessageResultTypeSuccess: break; case kARDMessageResultTypeUnknown: error = [[NSError alloc] initWithDomain:kARDAppClientErrorDomain code:kARDAppClientErrorUnknown userInfo:@{ NSLocalizedDescriptionKey: @"Unknown error.", }]; case kARDMessageResultTypeInvalidClient: error = [[NSError alloc] initWithDomain:kARDAppClientErrorDomain code:kARDAppClientErrorInvalidClient userInfo:@{ NSLocalizedDescriptionKey: @"Invalid client.", }]; break; case kARDMessageResultTypeInvalidRoom: error = [[NSError alloc] initWithDomain:kARDAppClientErrorDomain code:kARDAppClientErrorInvalidRoom userInfo:@{ NSLocalizedDescriptionKey: @"Invalid room.", }]; break; }; if (error) { [strongSelf.delegate appClient:strongSelf didError:error]; } if (completionHandler) { completionHandler(response); } }]; } - (void)unregisterWithRoomServer { NSString *urlString = [NSString stringWithFormat:kARDRoomServerByeFormat, _roomId, _clientId]; NSURL *url = [NSURL URLWithString:urlString]; NSURLRequest *request = [NSURLRequest requestWithURL:url]; NSURLResponse *response = nil; // We want a synchronous request so that we know that we're unregistered from // room server before we do any further unregistration. NSLog(@"C->RS: BYE"); NSError *error = nil; [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error]; if (error) { NSLog(@"Error unregistering from room server: %@", error); } NSLog(@"Unregistered from room server."); } - (NSError *)roomServerNetworkError { NSError *error = [[NSError alloc] initWithDomain:kARDAppClientErrorDomain code:kARDAppClientErrorNetwork userInfo:@{ NSLocalizedDescriptionKey: @"Room server network error", }]; return error; } #pragma mark - Collider methods - (void)registerWithColliderIfReady { if (!self.isRegisteredWithRoomServer) { return; } // Open WebSocket connection. _channel = [[ARDWebSocketChannel alloc] initWithURL:_websocketURL restURL:_websocketRestURL delegate:self]; [_channel registerForRoomId:_roomId clientId:_clientId]; } - (void)sendSignalingMessageToCollider:(ARDSignalingMessage *)message { NSData *data = [message JSONData]; [_channel sendData:data]; } #pragma mark - Defaults - (RTCMediaConstraints *)defaultMediaStreamConstraints { 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 { NSArray *optionalConstraints = @[ [[RTCPair alloc] initWithKey:@"DtlsSrtpKeyAgreement" value:@"true"] ]; RTCMediaConstraints* constraints = [[RTCMediaConstraints alloc] initWithMandatoryConstraints:nil optionalConstraints:optionalConstraints]; return constraints; } - (RTCICEServer *)defaultSTUNServer { NSURL *defaultSTUNServerURL = [NSURL URLWithString:kARDDefaultSTUNServerUrl]; return [[RTCICEServer alloc] initWithURI:defaultSTUNServerURL username:@"" password:@""]; } @end