/* * 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 "APPRTCConnectionManager.h" #import #import "APPRTCAppClient.h" #import "GAEChannelClient.h" #import "RTCICECandidate.h" #import "RTCMediaConstraints.h" #import "RTCMediaStream.h" #import "RTCPair.h" #import "RTCPeerConnection.h" #import "RTCPeerConnectionDelegate.h" #import "RTCPeerConnectionFactory.h" #import "RTCSessionDescription.h" #import "RTCSessionDescriptionDelegate.h" #import "RTCStatsDelegate.h" #import "RTCVideoCapturer.h" #import "RTCVideoSource.h" @interface APPRTCConnectionManager () @property(nonatomic, strong) APPRTCAppClient* client; @property(nonatomic, strong) RTCPeerConnection* peerConnection; @property(nonatomic, strong) RTCPeerConnectionFactory* peerConnectionFactory; @property(nonatomic, strong) RTCVideoSource* videoSource; @property(nonatomic, strong) NSMutableArray* queuedRemoteCandidates; @end @implementation APPRTCConnectionManager { NSTimer* _statsTimer; } - (instancetype)initWithDelegate:(id)delegate logger:(id)logger { if (self = [super init]) { self.delegate = delegate; self.logger = logger; self.peerConnectionFactory = [[RTCPeerConnectionFactory alloc] init]; // TODO(tkchin): turn this into a button. // Uncomment for stat logs. // _statsTimer = // [NSTimer scheduledTimerWithTimeInterval:10 // target:self // selector:@selector(didFireStatsTimer:) // userInfo:nil // repeats:YES]; } return self; } - (void)dealloc { [self disconnect]; } - (BOOL)connectToRoomWithURL:(NSURL*)url { if (self.client) { // Already have a connection. return NO; } self.client = [[APPRTCAppClient alloc] initWithDelegate:self messageHandler:self]; [self.client connectToRoom:url]; return YES; } - (void)disconnect { if (!self.client) { return; } [self.client sendData:[@"{\"type\": \"bye\"}" dataUsingEncoding:NSUTF8StringEncoding]]; [self.peerConnection close]; self.peerConnection = nil; self.client = nil; self.videoSource = nil; self.queuedRemoteCandidates = nil; } #pragma mark - APPRTCAppClientDelegate - (void)appClient:(APPRTCAppClient*)appClient didErrorWithMessage:(NSString*)message { [self.delegate connectionManager:self didErrorWithMessage:message]; } - (void)appClient:(APPRTCAppClient*)appClient didReceiveICEServers:(NSArray*)servers { self.queuedRemoteCandidates = [NSMutableArray array]; RTCMediaConstraints* constraints = [[RTCMediaConstraints alloc] initWithMandatoryConstraints: @[ [[RTCPair alloc] initWithKey:@"OfferToReceiveAudio" value:@"true"], [[RTCPair alloc] initWithKey:@"OfferToReceiveVideo" value:@"true"] ] optionalConstraints: @[ [[RTCPair alloc] initWithKey:@"internalSctpDataChannels" value:@"true"], [[RTCPair alloc] initWithKey:@"DtlsSrtpKeyAgreement" value:@"true"] ]]; self.peerConnection = [self.peerConnectionFactory peerConnectionWithICEServers:servers constraints:constraints delegate:self]; RTCMediaStream* lms = [self.peerConnectionFactory mediaStreamWithLabel:@"ARDAMS"]; // 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. RTCVideoTrack* localVideoTrack; // 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]; self.videoSource = [self.peerConnectionFactory videoSourceWithCapturer:capturer constraints:self.client.videoConstraints]; localVideoTrack = [self.peerConnectionFactory videoTrackWithID:@"ARDAMSv0" source:self.videoSource]; if (localVideoTrack) { [lms addVideoTrack:localVideoTrack]; } [self.delegate connectionManager:self didReceiveLocalVideoTrack:localVideoTrack]; #endif [lms addAudioTrack:[self.peerConnectionFactory audioTrackWithID:@"ARDAMSa0"]]; [self.peerConnection addStream:lms]; [self.logger logMessage:@"onICEServers - added local stream."]; } #pragma mark - GAEMessageHandler methods - (void)onOpen { if (!self.client.initiator) { [self.logger logMessage:@"Callee; waiting for remote offer"]; return; } [self.logger logMessage:@"GAE onOpen - create offer."]; RTCPair* audio = [[RTCPair alloc] initWithKey:@"OfferToReceiveAudio" value:@"true"]; RTCPair* video = [[RTCPair alloc] initWithKey:@"OfferToReceiveVideo" value:@"true"]; NSArray* mandatory = @[ audio, video ]; RTCMediaConstraints* constraints = [[RTCMediaConstraints alloc] initWithMandatoryConstraints:mandatory optionalConstraints:nil]; [self.peerConnection createOfferWithDelegate:self constraints:constraints]; [self.logger logMessage:@"PC - createOffer."]; } - (void)onMessage:(NSDictionary*)messageData { NSString* type = messageData[@"type"]; NSAssert(type, @"Missing type: %@", messageData); [self.logger logMessage:[NSString stringWithFormat:@"GAE onMessage type - %@", type]]; if ([type isEqualToString:@"candidate"]) { NSString* mid = messageData[@"id"]; NSNumber* sdpLineIndex = messageData[@"label"]; NSString* sdp = messageData[@"candidate"]; RTCICECandidate* candidate = [[RTCICECandidate alloc] initWithMid:mid index:sdpLineIndex.intValue sdp:sdp]; if (self.queuedRemoteCandidates) { [self.queuedRemoteCandidates addObject:candidate]; } else { [self.peerConnection addICECandidate:candidate]; } } else if ([type isEqualToString:@"offer"] || [type isEqualToString:@"answer"]) { NSString* sdpString = messageData[@"sdp"]; RTCSessionDescription* sdp = [[RTCSessionDescription alloc] initWithType:type sdp:[[self class] preferISAC:sdpString]]; [self.peerConnection setRemoteDescriptionWithDelegate:self sessionDescription:sdp]; [self.logger logMessage:@"PC - setRemoteDescription."]; } else if ([type isEqualToString:@"bye"]) { [self.delegate connectionManagerDidReceiveHangup:self]; } else { NSAssert(NO, @"Invalid message: %@", messageData); } } - (void)onClose { [self.logger logMessage:@"GAE onClose."]; [self.delegate connectionManagerDidReceiveHangup:self]; } - (void)onError:(int)code withDescription:(NSString*)description { NSString* message = [NSString stringWithFormat:@"GAE onError: %d, %@", code, description]; [self.logger logMessage:message]; [self.delegate connectionManager:self didErrorWithMessage:message]; } #pragma mark - RTCPeerConnectionDelegate - (void)peerConnection:(RTCPeerConnection*)peerConnection signalingStateChanged:(RTCSignalingState)stateChanged { dispatch_async(dispatch_get_main_queue(), ^{ NSLog(@"PCO onSignalingStateChange: %d", stateChanged); }); } - (void)peerConnection:(RTCPeerConnection*)peerConnection addedStream:(RTCMediaStream*)stream { dispatch_async(dispatch_get_main_queue(), ^{ NSLog(@"PCO onAddStream."); NSAssert([stream.audioTracks count] == 1 || [stream.videoTracks count] == 1, @"Expected audio or video track"); NSAssert([stream.audioTracks count] <= 1, @"Expected at most 1 audio stream"); NSAssert([stream.videoTracks count] <= 1, @"Expected at most 1 video stream"); if ([stream.videoTracks count] != 0) { [self.delegate connectionManager:self didReceiveRemoteVideoTrack:stream.videoTracks[0]]; } }); } - (void)peerConnection:(RTCPeerConnection*)peerConnection removedStream:(RTCMediaStream*)stream { dispatch_async(dispatch_get_main_queue(), ^{ NSLog(@"PCO onRemoveStream."); }); } - (void)peerConnectionOnRenegotiationNeeded:(RTCPeerConnection*)peerConnection { dispatch_async(dispatch_get_main_queue(), ^{ NSLog(@"PCO onRenegotiationNeeded - ignoring because AppRTC has a " "predefined negotiation strategy"); }); } - (void)peerConnection:(RTCPeerConnection*)peerConnection gotICECandidate:(RTCICECandidate*)candidate { dispatch_async(dispatch_get_main_queue(), ^{ NSLog(@"PCO onICECandidate.\n Mid[%@] Index[%li] Sdp[%@]", candidate.sdpMid, (long)candidate.sdpMLineIndex, candidate.sdp); NSDictionary* json = @{ @"type" : @"candidate", @"label" : @(candidate.sdpMLineIndex), @"id" : candidate.sdpMid, @"candidate" : candidate.sdp }; NSError* error; NSData* data = [NSJSONSerialization dataWithJSONObject:json options:0 error:&error]; if (!error) { [self.client sendData:data]; } else { NSAssert(NO, @"Unable to serialize JSON object with error: %@", error.localizedDescription); } }); } - (void)peerConnection:(RTCPeerConnection*)peerConnection iceGatheringChanged:(RTCICEGatheringState)newState { dispatch_async(dispatch_get_main_queue(), ^{ NSLog(@"PCO onIceGatheringChange. %d", newState); }); } - (void)peerConnection:(RTCPeerConnection*)peerConnection iceConnectionChanged:(RTCICEConnectionState)newState { dispatch_async(dispatch_get_main_queue(), ^{ NSLog(@"PCO onIceConnectionChange. %d", newState); if (newState == RTCICEConnectionConnected) [self.logger logMessage:@"ICE Connection Connected."]; NSAssert(newState != RTCICEConnectionFailed, @"ICE Connection failed!"); }); } - (void)peerConnection:(RTCPeerConnection*)peerConnection didOpenDataChannel:(RTCDataChannel*)dataChannel { NSAssert(NO, @"AppRTC doesn't use DataChannels"); } #pragma mark - RTCSessionDescriptionDelegate - (void)peerConnection:(RTCPeerConnection*)peerConnection didCreateSessionDescription:(RTCSessionDescription*)origSdp error:(NSError*)error { dispatch_async(dispatch_get_main_queue(), ^{ if (error) { [self.logger logMessage:@"SDP onFailure."]; NSAssert(NO, error.description); return; } [self.logger logMessage:@"SDP onSuccess(SDP) - set local description."]; RTCSessionDescription* sdp = [[RTCSessionDescription alloc] initWithType:origSdp.type sdp:[[self class] preferISAC:origSdp.description]]; [self.peerConnection setLocalDescriptionWithDelegate:self sessionDescription:sdp]; [self.logger logMessage:@"PC setLocalDescription."]; NSDictionary* json = @{@"type" : sdp.type, @"sdp" : sdp.description}; NSError* jsonError; NSData* data = [NSJSONSerialization dataWithJSONObject:json options:0 error:&jsonError]; NSAssert(!jsonError, @"Error: %@", jsonError.description); [self.client sendData:data]; }); } - (void)peerConnection:(RTCPeerConnection*)peerConnection didSetSessionDescriptionWithError:(NSError*)error { dispatch_async(dispatch_get_main_queue(), ^{ if (error) { [self.logger logMessage:@"SDP onFailure."]; NSAssert(NO, error.description); return; } [self.logger logMessage:@"SDP onSuccess() - possibly drain candidates"]; if (!self.client.initiator) { if (self.peerConnection.remoteDescription && !self.peerConnection.localDescription) { [self.logger logMessage:@"Callee, setRemoteDescription succeeded"]; RTCPair* audio = [[RTCPair alloc] initWithKey:@"OfferToReceiveAudio" value:@"true"]; RTCPair* video = [[RTCPair alloc] initWithKey:@"OfferToReceiveVideo" value:@"true"]; NSArray* mandatory = @[ audio, video ]; RTCMediaConstraints* constraints = [[RTCMediaConstraints alloc] initWithMandatoryConstraints:mandatory optionalConstraints:nil]; [self.peerConnection createAnswerWithDelegate:self constraints:constraints]; [self.logger logMessage:@"PC - createAnswer."]; } else { [self.logger logMessage:@"SDP onSuccess - drain candidates"]; [self drainRemoteCandidates]; } } else { if (self.peerConnection.remoteDescription) { [self.logger logMessage:@"SDP onSuccess - drain candidates"]; [self drainRemoteCandidates]; } } }); } #pragma mark - RTCStatsDelegate methods - (void)peerConnection:(RTCPeerConnection*)peerConnection didGetStats:(NSArray*)stats { dispatch_async(dispatch_get_main_queue(), ^{ NSString* message = [NSString stringWithFormat:@"Stats:\n %@", stats]; [self.logger logMessage:message]; }); } #pragma mark - Private // Match |pattern| to |string| and return the first group of the first // match, or nil if no match was found. + (NSString*)firstMatch:(NSRegularExpression*)pattern withString:(NSString*)string { NSTextCheckingResult* result = [pattern firstMatchInString:string options:0 range:NSMakeRange(0, [string length])]; if (!result) return nil; return [string substringWithRange:[result rangeAtIndex:1]]; } // Mangle |origSDP| to prefer the ISAC/16k audio codec. + (NSString*)preferISAC:(NSString*)origSDP { int mLineIndex = -1; NSString* isac16kRtpMap = nil; NSArray* lines = [origSDP componentsSeparatedByString:@"\n"]; NSRegularExpression* isac16kRegex = [NSRegularExpression regularExpressionWithPattern:@"^a=rtpmap:(\\d+) ISAC/16000[\r]?$" options:0 error:nil]; for (int i = 0; (i < [lines count]) && (mLineIndex == -1 || isac16kRtpMap == nil); ++i) { NSString* line = [lines objectAtIndex:i]; if ([line hasPrefix:@"m=audio "]) { mLineIndex = i; continue; } isac16kRtpMap = [self firstMatch:isac16kRegex withString:line]; } if (mLineIndex == -1) { NSLog(@"No m=audio line, so can't prefer iSAC"); return origSDP; } if (isac16kRtpMap == nil) { NSLog(@"No ISAC/16000 line, so can't prefer iSAC"); return origSDP; } NSArray* origMLineParts = [[lines objectAtIndex:mLineIndex] componentsSeparatedByString:@" "]; NSMutableArray* newMLine = [NSMutableArray arrayWithCapacity:[origMLineParts count]]; int origPartIndex = 0; // Format is: m= ... [newMLine addObject:[origMLineParts objectAtIndex:origPartIndex++]]; [newMLine addObject:[origMLineParts objectAtIndex:origPartIndex++]]; [newMLine addObject:[origMLineParts objectAtIndex:origPartIndex++]]; [newMLine addObject:isac16kRtpMap]; for (; origPartIndex < [origMLineParts count]; ++origPartIndex) { if (![isac16kRtpMap isEqualToString:[origMLineParts objectAtIndex:origPartIndex]]) { [newMLine addObject:[origMLineParts objectAtIndex:origPartIndex]]; } } NSMutableArray* newLines = [NSMutableArray arrayWithCapacity:[lines count]]; [newLines addObjectsFromArray:lines]; [newLines replaceObjectAtIndex:mLineIndex withObject:[newMLine componentsJoinedByString:@" "]]; return [newLines componentsJoinedByString:@"\n"]; } - (void)drainRemoteCandidates { for (RTCICECandidate* candidate in self.queuedRemoteCandidates) { [self.peerConnection addICECandidate:candidate]; } self.queuedRemoteCandidates = nil; } - (void)didFireStatsTimer:(NSTimer*)timer { if (self.peerConnection) { [self.peerConnection getStatsWithDelegate:self mediaStreamTrack:nil statsOutputLevel:RTCStatsOutputLevelDebug]; } } @end