| /* |
| * 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/RTCAudioTrack.h> |
| #import <WebRTC/RTCCameraVideoCapturer.h> |
| #import <WebRTC/RTCConfiguration.h> |
| #import <WebRTC/RTCDefaultVideoDecoderFactory.h> |
| #import <WebRTC/RTCDefaultVideoEncoderFactory.h> |
| #import <WebRTC/RTCFileLogger.h> |
| #import <WebRTC/RTCFileVideoCapturer.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/RTCRtpTransceiver.h> |
| #import <WebRTC/RTCTracing.h> |
| #import <WebRTC/RTCVideoSource.h> |
| #import <WebRTC/RTCVideoTrack.h> |
| |
| #import "ARDAppEngineClient.h" |
| #import "ARDExternalSampleCapturer.h" |
| #import "ARDJoinResponse.h" |
| #import "ARDMessageResponse.h" |
| #import "ARDSettingsModel.h" |
| #import "ARDSignalingMessage.h" |
| #import "ARDTURNClient+Internal.h" |
| #import "ARDUtilities.h" |
| #import "ARDWebSocketChannel.h" |
| #import "RTCIceCandidate+JSON.h" |
| #import "RTCSessionDescription+JSON.h" |
| |
| static NSString * const kARDIceServerRequestUrl = @"https://appr.tc/params"; |
| |
| 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"; |
| static NSString * const kARDVideoTrackKind = @"video"; |
| |
| // TODO(tkchin): Add these as UI options. |
| #if defined(WEBRTC_IOS) |
| 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. |
| #endif |
| static int const kKbpsMultiplier = 1000; |
| |
| // 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; |
| ARDSettingsModel *_settings; |
| RTCVideoTrack *_localVideoTrack; |
| } |
| |
| @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 broadcast = _broadcast; |
| |
| - (instancetype)init { |
| return [self initWithDelegate:nil]; |
| } |
| |
| - (instancetype)initWithDelegate:(id<ARDAppClientDelegate>)delegate { |
| if (self = [super init]) { |
| _roomServerClient = [[ARDAppEngineClient alloc] init]; |
| _delegate = delegate; |
| NSURL *turnRequestURL = [NSURL URLWithString:kARDIceServerRequestUrl]; |
| _turnClient = [[ARDTURNClient 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 { |
| _messageQueue = [NSMutableArray array]; |
| _iceServers = [NSMutableArray array]; |
| _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 |
| settings:(ARDSettingsModel *)settings |
| isLoopback:(BOOL)isLoopback { |
| NSParameterAssert(roomId.length); |
| NSParameterAssert(_state == kARDAppClientStateDisconnected); |
| _settings = settings; |
| _isLoopback = isLoopback; |
| self.state = kARDAppClientStateConnecting; |
| |
| RTCDefaultVideoDecoderFactory *decoderFactory = [[RTCDefaultVideoDecoderFactory alloc] init]; |
| RTCDefaultVideoEncoderFactory *encoderFactory = [[RTCDefaultVideoEncoderFactory alloc] init]; |
| encoderFactory.preferredCodec = [settings currentVideoCodecSettingFromStore]; |
| _factory = [[RTCPeerConnectionFactory alloc] initWithEncoderFactory:encoderFactory |
| decoderFactory:decoderFactory]; |
| |
| #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]; |
| _localVideoTrack = nil; |
| #if defined(WEBRTC_IOS) |
| [_factory stopAecDump]; |
| [_peerConnection stopRtcEventLog]; |
| #endif |
| [_peerConnection close]; |
| _peerConnection = nil; |
| self.state = kARDAppClientStateDisconnected; |
| #if defined(WEBRTC_IOS) |
| if (kARDAppClientEnableTracing) { |
| 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 { |
| RTCLog(@"Stream with %lu video tracks and %lu audio tracks was added.", |
| (unsigned long)stream.videoTracks.count, |
| (unsigned long)stream.audioTracks.count); |
| } |
| |
| - (void)peerConnection:(RTCPeerConnection *)peerConnection |
| didStartReceivingOnTransceiver:(RTCRtpTransceiver *)transceiver { |
| RTCMediaStreamTrack *track = transceiver.receiver.track; |
| RTCLog(@"Now receiving %@ on track %@.", track.kind, track.trackId); |
| } |
| |
| - (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(), ^{ |
| [self.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]; |
| [self.delegate appClient:self didError:sdpError]; |
| return; |
| } |
| __weak ARDAppClient *weakSelf = self; |
| [self.peerConnection setLocalDescription:sdp |
| completionHandler:^(NSError *error) { |
| ARDAppClient *strongSelf = weakSelf; |
| [strongSelf peerConnection:strongSelf.peerConnection |
| didSetSessionDescriptionWithError:error]; |
| }]; |
| ARDSessionDescriptionMessage *message = |
| [[ARDSessionDescriptionMessage alloc] initWithDescription:sdp]; |
| [self sendSignalingMessage:message]; |
| [self setMaxBitrateForPeerConnectionVideoSender]; |
| }); |
| } |
| |
| - (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]; |
| [self.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 (!self.isInitiator && !self.peerConnection.localDescription) { |
| RTCMediaConstraints *constraints = [self defaultAnswerConstraints]; |
| __weak ARDAppClient *weakSelf = self; |
| [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]; |
| RTCCertificate *pcert = [RTCCertificate generateCertificateWithParams:@{ |
| @"expires" : @100000, |
| @"name" : @"RSASSA-PKCS1-v1_5" |
| }]; |
| config.iceServers = _iceServers; |
| config.sdpSemantics = RTCSdpSemanticsUnifiedPlan; |
| config.certificate = pcert; |
| |
| _peerConnection = [_factory peerConnectionWithConfiguration:config |
| constraints:constraints |
| delegate:self]; |
| // Create AV senders. |
| [self createMediaSenders]; |
| 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 ([_settings currentCreateAecDumpSettingFromStore]) { |
| 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; |
| __weak ARDAppClient *weakSelf = self; |
| [_peerConnection setRemoteDescription:description |
| 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]; |
| } |
| } |
| |
| - (void)setMaxBitrateForPeerConnectionVideoSender { |
| for (RTCRtpSender *sender in _peerConnection.senders) { |
| if (sender.track != nil) { |
| if ([sender.track.kind isEqualToString:kARDVideoTrackKind]) { |
| [self setMaxBitrate:[_settings currentMaxBitrateSettingFromStore] forVideoSender:sender]; |
| } |
| } |
| } |
| } |
| |
| - (void)setMaxBitrate:(NSNumber *)maxBitrate forVideoSender:(RTCRtpSender *)sender { |
| if (maxBitrate.intValue <= 0) { |
| return; |
| } |
| |
| RTCRtpParameters *parametersToModify = sender.parameters; |
| for (RTCRtpEncodingParameters *encoding in parametersToModify.encodings) { |
| encoding.maxBitrateBps = @(maxBitrate.intValue * kKbpsMultiplier); |
| } |
| [sender setParameters:parametersToModify]; |
| } |
| |
| - (RTCRtpTransceiver *)videoTransceiver { |
| for (RTCRtpTransceiver *transceiver in _peerConnection.transceivers) { |
| if (transceiver.mediaType == RTCRtpMediaTypeVideo) { |
| return transceiver; |
| } |
| } |
| return nil; |
| } |
| |
| - (void)createMediaSenders { |
| RTCMediaConstraints *constraints = [self defaultMediaAudioConstraints]; |
| RTCAudioSource *source = [_factory audioSourceWithConstraints:constraints]; |
| RTCAudioTrack *track = [_factory audioTrackWithSource:source |
| trackId:kARDAudioTrackId]; |
| [_peerConnection addTrack:track streamIds:@[ kARDMediaStreamId ]]; |
| _localVideoTrack = [self createLocalVideoTrack]; |
| if (_localVideoTrack) { |
| [_peerConnection addTrack:_localVideoTrack streamIds:@[ kARDMediaStreamId ]]; |
| [_delegate appClient:self didReceiveLocalVideoTrack:_localVideoTrack]; |
| // We can set up rendering for the remote track right away since the transceiver already has an |
| // RTCRtpReceiver with a track. The track will automatically get unmuted and produce frames |
| // once RTP is received. |
| RTCVideoTrack *track = (RTCVideoTrack *)([self videoTransceiver].receiver.track); |
| [_delegate appClient:self didReceiveRemoteVideoTrack:track]; |
| } |
| } |
| |
| - (RTCVideoTrack *)createLocalVideoTrack { |
| if ([_settings currentAudioOnlySettingFromStore]) { |
| return nil; |
| } |
| |
| RTCVideoSource *source = [_factory videoSource]; |
| |
| #if !TARGET_IPHONE_SIMULATOR |
| if (self.isBroadcast) { |
| ARDExternalSampleCapturer *capturer = |
| [[ARDExternalSampleCapturer alloc] initWithDelegate:source]; |
| [_delegate appClient:self didCreateLocalExternalSampleCapturer:capturer]; |
| } else { |
| RTCCameraVideoCapturer *capturer = [[RTCCameraVideoCapturer alloc] initWithDelegate:source]; |
| [_delegate appClient:self didCreateLocalCapturer:capturer]; |
| } |
| #else |
| #if defined(__IPHONE_11_0) && (__IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_11_0) |
| if (@available(iOS 10, *)) { |
| RTCFileVideoCapturer *fileCapturer = [[RTCFileVideoCapturer alloc] initWithDelegate:source]; |
| [_delegate appClient:self didCreateLocalFileCapturer:fileCapturer]; |
| } |
| #endif |
| #endif |
| |
| return [_factory videoTrackWithSource:source trackId:kARDVideoTrackId]; |
| } |
| |
| #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 { |
| NSDictionary *mandatoryConstraints = @{}; |
| RTCMediaConstraints *constraints = |
| [[RTCMediaConstraints alloc] initWithMandatoryConstraints:mandatoryConstraints |
| 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; |
| } |
| |
| #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 |