/* * libjingle * Copyright 2013, 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 #import "APPRTCAppDelegate.h" #import "APPRTCViewController.h" #import "RTCICECandidate.h" #import "RTCICEServer.h" #import "RTCMediaConstraints.h" #import "RTCMediaStream.h" #import "RTCPair.h" #import "RTCPeerConnection.h" #import "RTCPeerConnectionDelegate.h" #import "RTCPeerConnectionFactory.h" #import "RTCSessionDescription.h" #import "RTCStatsDelegate.h" #import "RTCVideoRenderer.h" #import "RTCVideoCapturer.h" #import "RTCVideoTrack.h" #import "APPRTCVideoView.h" @interface PCObserver : NSObject - (id)initWithDelegate:(id)delegate; @property(nonatomic, strong) APPRTCVideoView* videoView; @end @implementation PCObserver { id _delegate; } - (id)initWithDelegate:(id)delegate { if (self = [super init]) { _delegate = delegate; } return self; } #pragma mark - RTCPeerConnectionDelegate - (void)peerConnectionOnError:(RTCPeerConnection*)peerConnection { dispatch_async(dispatch_get_main_queue(), ^(void) { NSLog(@"PCO onError."); NSAssert(NO, @"PeerConnection failed."); }); } - (void)peerConnection:(RTCPeerConnection*)peerConnection signalingStateChanged:(RTCSignalingState)stateChanged { dispatch_async(dispatch_get_main_queue(), ^(void) { NSLog(@"PCO onSignalingStateChange: %d", stateChanged); }); } - (void)peerConnection:(RTCPeerConnection*)peerConnection addedStream:(RTCMediaStream*)stream { dispatch_async(dispatch_get_main_queue(), ^(void) { NSLog(@"PCO onAddStream."); 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.videoView renderVideoTrackInterface:[stream.videoTracks objectAtIndex:0]]; } }); } - (void)peerConnection:(RTCPeerConnection*)peerConnection removedStream:(RTCMediaStream*)stream { dispatch_async(dispatch_get_main_queue(), ^(void) { NSLog(@"PCO onRemoveStream."); }); } - (void)peerConnectionOnRenegotiationNeeded:(RTCPeerConnection*)peerConnection { dispatch_async(dispatch_get_main_queue(), ^(void) { NSLog(@"PCO onRenegotiationNeeded - ignoring because AppRTC has a " "predefined negotiation strategy"); }); } - (void)peerConnection:(RTCPeerConnection*)peerConnection gotICECandidate:(RTCICECandidate*)candidate { dispatch_async(dispatch_get_main_queue(), ^(void) { NSLog(@"PCO onICECandidate.\n Mid[%@] Index[%d] Sdp[%@]", candidate.sdpMid, candidate.sdpMLineIndex, candidate.sdp); NSDictionary* json = @{ @"type" : @"candidate", @"label" : [NSNumber numberWithInt:candidate.sdpMLineIndex], @"id" : candidate.sdpMid, @"candidate" : candidate.sdp }; NSError* error; NSData* data = [NSJSONSerialization dataWithJSONObject:json options:0 error:&error]; if (!error) { [_delegate 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(), ^(void) { NSLog(@"PCO onIceGatheringChange. %d", newState); }); } - (void)peerConnection:(RTCPeerConnection*)peerConnection iceConnectionChanged:(RTCICEConnectionState)newState { dispatch_async(dispatch_get_main_queue(), ^(void) { NSLog(@"PCO onIceConnectionChange. %d", newState); if (newState == RTCICEConnectionConnected) [self displayLogMessage:@"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 - Private - (void)displayLogMessage:(NSString*)message { [_delegate displayLogMessage:message]; } @end @interface APPRTCAppDelegate () @property(nonatomic, strong) APPRTCAppClient* client; @property(nonatomic, strong) PCObserver* pcObserver; @property(nonatomic, strong) RTCPeerConnection* peerConnection; @property(nonatomic, strong) RTCPeerConnectionFactory* peerConnectionFactory; @property(nonatomic, strong) NSMutableArray* queuedRemoteCandidates; @end @implementation APPRTCAppDelegate { NSTimer* _statsTimer; } #pragma mark - UIApplicationDelegate methods - (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions { [RTCPeerConnectionFactory initializeSSL]; self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; self.viewController = [[APPRTCViewController alloc] initWithNibName:@"APPRTCViewController" bundle:nil]; self.window.rootViewController = self.viewController; _statsTimer = [NSTimer scheduledTimerWithTimeInterval:10 target:self selector:@selector(didFireStatsTimer:) userInfo:nil repeats:YES]; [self.window makeKeyAndVisible]; return YES; } - (void)applicationWillResignActive:(UIApplication*)application { [self displayLogMessage:@"Application lost focus, connection broken."]; [self closeVideoUI]; } - (void)applicationDidEnterBackground:(UIApplication*)application { } - (void)applicationWillEnterForeground:(UIApplication*)application { } - (void)applicationDidBecomeActive:(UIApplication*)application { } - (void)applicationWillTerminate:(UIApplication*)application { } - (BOOL)application:(UIApplication*)application openURL:(NSURL*)url sourceApplication:(NSString*)sourceApplication annotation:(id)annotation { if (self.client) { return NO; } self.client = [[APPRTCAppClient alloc] initWithICEServerDelegate:self messageHandler:self]; [self.client connectToRoom:url]; return YES; } - (void)displayLogMessage:(NSString*)message { NSAssert([NSThread isMainThread], @"Called off main thread!"); NSLog(@"%@", message); [self.viewController displayText:message]; } #pragma mark - RTCSendMessage method - (void)sendData:(NSData*)data { [self.client sendData:data]; } #pragma mark - ICEServerDelegate method - (void)onICEServers:(NSArray*)servers { self.queuedRemoteCandidates = [NSMutableArray array]; self.peerConnectionFactory = [[RTCPeerConnectionFactory alloc] init]; 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.pcObserver = [[PCObserver alloc] initWithDelegate:self]; self.peerConnection = [self.peerConnectionFactory peerConnectionWithICEServers:servers constraints:constraints delegate:self.pcObserver]; 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; #if !TARGET_IPHONE_SIMULATOR 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]; } #endif [self.viewController.localVideoView renderVideoTrackInterface:localVideoTrack]; self.pcObserver.videoView = self.viewController.remoteVideoView; [lms addAudioTrack:[self.peerConnectionFactory audioTrackWithID:@"ARDAMSa0"]]; [self.peerConnection addStream:lms constraints:constraints]; [self displayLogMessage:@"onICEServers - added local stream."]; } #pragma mark - GAEMessageHandler methods - (void)onOpen { if (!self.client.initiator) { [self displayLogMessage:@"Callee; waiting for remote offer"]; return; } [self displayLogMessage:@"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 displayLogMessage:@"PC - createOffer."]; } - (void)onMessage:(NSDictionary*)messageData { NSString* type = messageData[@"type"]; NSAssert(type, @"Missing type: %@", messageData); [self displayLogMessage:[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:[APPRTCAppDelegate preferISAC:sdpString]]; [self.peerConnection setRemoteDescriptionWithDelegate:self sessionDescription:sdp]; [self displayLogMessage:@"PC - setRemoteDescription."]; } else if ([type isEqualToString:@"bye"]) { [self closeVideoUI]; UIAlertView* alertView = [[UIAlertView alloc] initWithTitle:@"Remote end hung up" message:@"dropping PeerConnection" delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil]; [alertView show]; } else { NSAssert(NO, @"Invalid message: %@", messageData); } } - (void)onClose { [self displayLogMessage:@"GAE onClose."]; [self closeVideoUI]; } - (void)onError:(int)code withDescription:(NSString*)description { [self displayLogMessage:[NSString stringWithFormat:@"GAE onError: %d, %@", code, description]]; [self closeVideoUI]; } #pragma mark - RTCSessionDescriptionDelegate methods // 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)peerConnection:(RTCPeerConnection*)peerConnection didCreateSessionDescription:(RTCSessionDescription*)origSdp error:(NSError*)error { dispatch_async(dispatch_get_main_queue(), ^(void) { if (error) { [self displayLogMessage:@"SDP onFailure."]; NSAssert(NO, error.description); return; } [self displayLogMessage:@"SDP onSuccess(SDP) - set local description."]; RTCSessionDescription* sdp = [[RTCSessionDescription alloc] initWithType:origSdp.type sdp:[APPRTCAppDelegate preferISAC:origSdp.description]]; [self.peerConnection setLocalDescriptionWithDelegate:self sessionDescription:sdp]; [self displayLogMessage:@"PC setLocalDescription."]; NSDictionary* json = @{@"type" : sdp.type, @"sdp" : sdp.description}; NSError* error; NSData* data = [NSJSONSerialization dataWithJSONObject:json options:0 error:&error]; NSAssert(!error, @"%@", [NSString stringWithFormat:@"Error: %@", error.description]); [self sendData:data]; }); } - (void)peerConnection:(RTCPeerConnection*)peerConnection didSetSessionDescriptionWithError:(NSError*)error { dispatch_async(dispatch_get_main_queue(), ^(void) { if (error) { [self displayLogMessage:@"SDP onFailure."]; NSAssert(NO, error.description); return; } [self displayLogMessage:@"SDP onSuccess() - possibly drain candidates"]; if (!self.client.initiator) { if (self.peerConnection.remoteDescription && !self.peerConnection.localDescription) { [self displayLogMessage:@"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 displayLogMessage:@"PC - createAnswer."]; } else { [self displayLogMessage:@"SDP onSuccess - drain candidates"]; [self drainRemoteCandidates]; } } else { if (self.peerConnection.remoteDescription) { [self displayLogMessage:@"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 displayLogMessage:message]; }); } #pragma mark - internal methods - (void)disconnect { [self.client sendData:[@"{\"type\": \"bye\"}" dataUsingEncoding:NSUTF8StringEncoding]]; [self.peerConnection close]; self.peerConnection = nil; self.pcObserver = nil; self.client = nil; self.videoSource = nil; self.peerConnectionFactory = nil; [RTCPeerConnectionFactory deinitializeSSL]; } - (void)drainRemoteCandidates { for (RTCICECandidate* candidate in self.queuedRemoteCandidates) { [self.peerConnection addICECandidate:candidate]; } self.queuedRemoteCandidates = nil; } - (NSString*)unHTMLifyString:(NSString*)base { // TODO(hughv): Investigate why percent escapes are being added. Removing // them isn't necessary on Android. // convert HTML escaped characters to UTF8. NSString* removePercent = [base stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; // remove leading and trailing ". NSRange range; range.length = [removePercent length] - 2; range.location = 1; NSString* removeQuotes = [removePercent substringWithRange:range]; // convert \" to ". NSString* removeEscapedQuotes = [removeQuotes stringByReplacingOccurrencesOfString:@"\\\"" withString:@"\""]; // convert \\ to \. NSString* removeBackslash = [removeEscapedQuotes stringByReplacingOccurrencesOfString:@"\\\\" withString:@"\\"]; return removeBackslash; } - (void)didFireStatsTimer:(NSTimer *)timer { if (self.peerConnection) { [self.peerConnection getStatsWithDelegate:self mediaStreamTrack:nil statsOutputLevel:RTCStatsOutputLevelDebug]; } } #pragma mark - public methods - (void)closeVideoUI { [self.viewController resetUI]; [self disconnect]; } @end