3 * Copyright 2013, Google Inc.
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions are met:
8 * 1. Redistributions of source code must retain the above copyright notice,
9 * this list of conditions and the following disclaimer.
10 * 2. Redistributions in binary form must reproduce the above copyright notice,
11 * this list of conditions and the following disclaimer in the documentation
12 * and/or other materials provided with the distribution.
13 * 3. The name of the author may not be used to endorse or promote products
14 * derived from this software without specific prior written permission.
16 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
17 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
18 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
19 * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
21 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
22 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
23 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
24 * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
25 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 #import "APPRTCAppDelegate.h"
30 #import "APPRTCViewController.h"
31 #import "RTCICECandidate.h"
32 #import "RTCICEServer.h"
33 #import "RTCMediaConstraints.h"
34 #import "RTCMediaStream.h"
36 #import "RTCPeerConnection.h"
37 #import "RTCPeerConnectionDelegate.h"
38 #import "RTCPeerConnectionFactory.h"
39 #import "RTCSessionDescription.h"
41 @interface PCObserver : NSObject<RTCPeerConnectionDelegate>
43 - (id)initWithDelegate:(id<APPRTCSendMessage>)delegate;
47 @implementation PCObserver {
48 id<APPRTCSendMessage> _delegate;
51 - (id)initWithDelegate:(id<APPRTCSendMessage>)delegate {
52 if (self = [super init]) {
58 - (void)peerConnectionOnError:(RTCPeerConnection *)peerConnection {
59 NSLog(@"PCO onError.");
60 NSAssert(NO, @"PeerConnection failed.");
63 - (void)peerConnection:(RTCPeerConnection *)peerConnection
64 signalingStateChanged:(RTCSignalingState)stateChanged {
65 NSLog(@"PCO onSignalingStateChange: %d", stateChanged);
68 - (void)peerConnection:(RTCPeerConnection *)peerConnection
69 addedStream:(RTCMediaStream *)stream {
70 NSLog(@"PCO onAddStream.");
71 dispatch_async(dispatch_get_main_queue(), ^(void) {
72 NSAssert([stream.audioTracks count] >= 1,
73 @"Expected at least 1 audio stream");
74 //NSAssert([stream.videoTracks count] >= 1,
75 // @"Expected at least 1 video stream");
76 // TODO(hughv): Add video support
80 - (void)peerConnection:(RTCPeerConnection *)peerConnection
81 removedStream:(RTCMediaStream *)stream {
82 NSLog(@"PCO onRemoveStream.");
83 // TODO(hughv): Remove video track.
87 peerConnectionOnRenegotiationNeeded:(RTCPeerConnection *)peerConnection {
88 NSLog(@"PCO onRenegotiationNeeded.");
89 // TODO(hughv): Handle this.
92 - (void)peerConnection:(RTCPeerConnection *)peerConnection
93 gotICECandidate:(RTCICECandidate *)candidate {
94 NSLog(@"PCO onICECandidate.\n Mid[%@] Index[%d] Sdp[%@]",
96 candidate.sdpMLineIndex,
99 @{ @"type" : @"candidate",
100 @"label" : [NSNumber numberWithInt:candidate.sdpMLineIndex],
101 @"id" : candidate.sdpMid,
102 @"candidate" : candidate.sdp };
105 [NSJSONSerialization dataWithJSONObject:json options:0 error:&error];
107 [_delegate sendData:data];
109 NSAssert(NO, @"Unable to serialize JSON object with error: %@",
110 error.localizedDescription);
114 - (void)peerConnection:(RTCPeerConnection *)peerConnection
115 iceGatheringChanged:(RTCICEGatheringState)newState {
116 NSLog(@"PCO onIceGatheringChange. %d", newState);
119 - (void)peerConnection:(RTCPeerConnection *)peerConnection
120 iceConnectionChanged:(RTCICEConnectionState)newState {
121 NSLog(@"PCO onIceConnectionChange. %d", newState);
122 if (newState == RTCICEConnectionConnected)
123 [self displayLogMessage:@"ICE Connection Connected."];
124 NSAssert(newState != RTCICEConnectionFailed, @"ICE Connection failed!");
127 - (void)displayLogMessage:(NSString *)message {
128 [_delegate displayLogMessage:message];
133 @interface APPRTCAppDelegate ()
135 @property(nonatomic, strong) APPRTCAppClient *client;
136 @property(nonatomic, strong) PCObserver *pcObserver;
137 @property(nonatomic, strong) RTCPeerConnection *peerConnection;
138 @property(nonatomic, strong) RTCPeerConnectionFactory *peerConnectionFactory;
139 @property(nonatomic, strong) NSMutableArray *queuedRemoteCandidates;
143 @implementation APPRTCAppDelegate
145 @synthesize window = _window;
146 @synthesize viewController = _viewController;
147 @synthesize client = _client;
148 @synthesize pcObserver = _pcObserver;
149 @synthesize peerConnection = _peerConnection;
150 @synthesize peerConnectionFactory = _peerConnectionFactory;
151 @synthesize queuedRemoteCandidates = _queuedRemoteCandidates;
153 #pragma mark - UIApplicationDelegate methods
155 - (BOOL)application:(UIApplication *)application
156 didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
157 [RTCPeerConnectionFactory initializeSSL];
158 self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
159 self.viewController =
160 [[APPRTCViewController alloc] initWithNibName:@"APPRTCViewController"
162 self.window.rootViewController = self.viewController;
163 [self.window makeKeyAndVisible];
167 - (void)applicationWillResignActive:(UIApplication *)application {
168 [self displayLogMessage:@"Application lost focus, connection broken."];
170 [self.viewController resetUI];
173 - (void)applicationDidEnterBackground:(UIApplication *)application {
176 - (void)applicationWillEnterForeground:(UIApplication *)application {
179 - (void)applicationDidBecomeActive:(UIApplication *)application {
182 - (void)applicationWillTerminate:(UIApplication *)application {
185 - (BOOL)application:(UIApplication *)application
187 sourceApplication:(NSString *)sourceApplication
188 annotation:(id)annotation {
192 self.client = [[APPRTCAppClient alloc] init];
193 self.client.ICEServerDelegate = self;
194 self.client.messageHandler = self;
195 [self.client connectToRoom:url];
199 - (void)displayLogMessage:(NSString *)message {
200 NSLog(@"%@", message);
201 [self.viewController displayText:message];
204 #pragma mark - RTCSendMessage method
206 - (void)sendData:(NSData *)data {
207 [self.client sendData:data];
210 #pragma mark - ICEServerDelegate method
212 - (void)onICEServers:(NSArray *)servers {
213 self.queuedRemoteCandidates = [NSMutableArray array];
214 self.peerConnectionFactory = [[RTCPeerConnectionFactory alloc] init];
215 RTCMediaConstraints *constraints = [[RTCMediaConstraints alloc] init];
216 self.pcObserver = [[PCObserver alloc] initWithDelegate:self];
217 self.peerConnection =
218 [self.peerConnectionFactory peerConnectionWithICEServers:servers
219 constraints:constraints
220 delegate:self.pcObserver];
221 RTCMediaStream *lms =
222 [self.peerConnectionFactory mediaStreamWithLabel:@"ARDAMS"];
223 // TODO(hughv): Add video.
224 [lms addAudioTrack:[self.peerConnectionFactory audioTrackWithID:@"ARDAMSa0"]];
225 [self.peerConnection addStream:lms constraints:constraints];
226 [self displayLogMessage:@"onICEServers - add local stream."];
229 #pragma mark - GAEMessageHandler methods
232 if (!self.client.initiator) {
233 [self displayLogMessage:@"Callee; waiting for remote offer"];
236 [self displayLogMessage:@"GAE onOpen - create offer."];
238 [[RTCPair alloc] initWithKey:@"OfferToReceiveAudio" value:@"true"];
239 // TODO(hughv): Add video.
240 // RTCPair *video = [[RTCPair alloc] initWithKey:@"OfferToReceiveVideo"
242 NSArray *mandatory = @[ audio /*, video*/ ];
243 RTCMediaConstraints *constraints =
244 [[RTCMediaConstraints alloc] initWithMandatoryConstraints:mandatory
245 optionalConstraints:nil];
246 [self.peerConnection createOfferWithDelegate:self constraints:constraints];
247 [self displayLogMessage:@"PC - createOffer."];
250 - (void)onMessage:(NSString *)data {
251 NSString *message = [self unHTMLifyString:data];
253 NSDictionary *objects = [NSJSONSerialization
254 JSONObjectWithData:[message dataUsingEncoding:NSUTF8StringEncoding]
259 [NSString stringWithFormat:@"Error: %@", error.description]);
260 NSAssert([objects count] > 0, @"Invalid JSON object");
261 NSString *value = [objects objectForKey:@"type"];
262 [self displayLogMessage:
263 [NSString stringWithFormat:@"GAE onMessage type - %@", value]];
264 if ([value compare:@"candidate"] == NSOrderedSame) {
265 NSString *mid = [objects objectForKey:@"id"];
266 NSNumber *sdpLineIndex = [objects objectForKey:@"label"];
267 NSString *sdp = [objects objectForKey:@"candidate"];
268 RTCICECandidate *candidate =
269 [[RTCICECandidate alloc] initWithMid:mid
270 index:sdpLineIndex.intValue
272 if (self.queuedRemoteCandidates) {
273 [self.queuedRemoteCandidates addObject:candidate];
275 [self.peerConnection addICECandidate:candidate];
277 } else if (([value compare:@"offer"] == NSOrderedSame) ||
278 ([value compare:@"answer"] == NSOrderedSame)) {
279 NSString *sdpString = [objects objectForKey:@"sdp"];
280 RTCSessionDescription *sdp = [[RTCSessionDescription alloc]
281 initWithType:value sdp:[APPRTCAppDelegate preferISAC:sdpString]];
282 [self.peerConnection setRemoteDescriptionWithDelegate:self
283 sessionDescription:sdp];
284 [self displayLogMessage:@"PC - setRemoteDescription."];
285 } else if ([value compare:@"bye"] == NSOrderedSame) {
288 NSAssert(NO, @"Invalid message: %@", data);
293 [self displayLogMessage:@"GAE onClose."];
297 - (void)onError:(int)code withDescription:(NSString *)description {
298 [self displayLogMessage:
299 [NSString stringWithFormat:@"GAE onError: %@", description]];
303 #pragma mark - RTCSessionDescriptonDelegate methods
305 // Match |pattern| to |string| and return the first group of the first
306 // match, or nil if no match was found.
307 + (NSString *)firstMatch:(NSRegularExpression *)pattern
308 withString:(NSString *)string {
309 NSTextCheckingResult* result =
310 [pattern firstMatchInString:string
312 range:NSMakeRange(0, [string length])];
315 return [string substringWithRange:[result rangeAtIndex:1]];
318 // Mangle |origSDP| to prefer the ISAC/16k audio codec.
319 + (NSString *)preferISAC:(NSString *)origSDP {
321 NSString* isac16kRtpMap = nil;
322 NSArray* lines = [origSDP componentsSeparatedByString:@"\n"];
323 NSRegularExpression* isac16kRegex = [NSRegularExpression
324 regularExpressionWithPattern:@"^a=rtpmap:(\\d+) ISAC/16000[\r]?$"
328 (i < [lines count]) && (mLineIndex == -1 || isac16kRtpMap == nil);
330 NSString* line = [lines objectAtIndex:i];
331 if ([line hasPrefix:@"m=audio "]) {
335 isac16kRtpMap = [self firstMatch:isac16kRegex withString:line];
337 if (mLineIndex == -1) {
338 NSLog(@"No m=audio line, so can't prefer iSAC");
341 if (isac16kRtpMap == nil) {
342 NSLog(@"No ISAC/16000 line, so can't prefer iSAC");
345 NSArray* origMLineParts =
346 [[lines objectAtIndex:mLineIndex] componentsSeparatedByString:@" "];
347 NSMutableArray* newMLine =
348 [NSMutableArray arrayWithCapacity:[origMLineParts count]];
349 int origPartIndex = 0;
350 // Format is: m=<media> <port> <proto> <fmt> ...
351 [newMLine addObject:[origMLineParts objectAtIndex:origPartIndex++]];
352 [newMLine addObject:[origMLineParts objectAtIndex:origPartIndex++]];
353 [newMLine addObject:[origMLineParts objectAtIndex:origPartIndex++]];
354 [newMLine addObject:isac16kRtpMap];
355 for (; origPartIndex < [origMLineParts count]; ++origPartIndex) {
356 if ([isac16kRtpMap compare:[origMLineParts objectAtIndex:origPartIndex]]
358 [newMLine addObject:[origMLineParts objectAtIndex:origPartIndex]];
361 NSMutableArray* newLines = [NSMutableArray arrayWithCapacity:[lines count]];
362 [newLines addObjectsFromArray:lines];
363 [newLines replaceObjectAtIndex:mLineIndex
364 withObject:[newMLine componentsJoinedByString:@" "]];
365 return [newLines componentsJoinedByString:@"\n"];
368 - (void)peerConnection:(RTCPeerConnection *)peerConnection
369 didCreateSessionDescription:(RTCSessionDescription *)origSdp
370 error:(NSError *)error {
372 [self displayLogMessage:@"SDP onFailure."];
373 NSAssert(NO, error.description);
377 [self displayLogMessage:@"SDP onSuccess(SDP) - set local description."];
378 RTCSessionDescription* sdp =
379 [[RTCSessionDescription alloc]
380 initWithType:origSdp.type
381 sdp:[APPRTCAppDelegate preferISAC:origSdp.description]];
382 [self.peerConnection setLocalDescriptionWithDelegate:self
383 sessionDescription:sdp];
384 [self displayLogMessage:@"PC setLocalDescription."];
385 dispatch_async(dispatch_get_main_queue(), ^(void) {
386 NSDictionary *json = @{ @"type" : sdp.type, @"sdp" : sdp.description };
389 [NSJSONSerialization dataWithJSONObject:json options:0 error:&error];
392 [NSString stringWithFormat:@"Error: %@", error.description]);
393 [self sendData:data];
397 - (void)peerConnection:(RTCPeerConnection *)peerConnection
398 didSetSessionDescriptionWithError:(NSError *)error {
400 [self displayLogMessage:@"SDP onFailure."];
401 NSAssert(NO, error.description);
405 [self displayLogMessage:@"SDP onSuccess() - possibly drain candidates"];
406 dispatch_async(dispatch_get_main_queue(), ^(void) {
407 if (!self.client.initiator) {
408 if (self.peerConnection.remoteDescription
409 && !self.peerConnection.localDescription) {
410 [self displayLogMessage:@"Callee, setRemoteDescription succeeded"];
413 initWithKey:@"OfferToReceiveAudio" value:@"true"];
414 // TODO(hughv): Add video.
417 // initWithKey:@"OfferToReceiveVideo" value:@"true"];
418 NSArray *mandatory = @[ audio /*, video*/ ];
419 RTCMediaConstraints *constraints =
420 [[RTCMediaConstraints alloc]
421 initWithMandatoryConstraints:mandatory
422 optionalConstraints:nil];
424 createAnswerWithDelegate:self constraints:constraints];
425 [self displayLogMessage:@"PC - createAnswer."];
427 [self displayLogMessage:@"SDP onSuccess - drain candidates"];
428 [self drainRemoteCandidates];
431 if (self.peerConnection.remoteDescription) {
432 [self displayLogMessage:@"SDP onSuccess - drain candidates"];
433 [self drainRemoteCandidates];
439 #pragma mark - internal methods
443 sendData:[@"{\"type\": \"bye\"}" dataUsingEncoding:NSUTF8StringEncoding]];
444 self.peerConnection = nil;
445 self.peerConnectionFactory = nil;
446 self.pcObserver = nil;
447 self.client.ICEServerDelegate = nil;
448 self.client.messageHandler = nil;
450 [RTCPeerConnectionFactory deinitializeSSL];
453 - (void)drainRemoteCandidates {
454 for (RTCICECandidate *candidate in self.queuedRemoteCandidates) {
455 [self.peerConnection addICECandidate:candidate];
457 self.queuedRemoteCandidates = nil;
460 - (NSString *)unHTMLifyString:(NSString *)base {
461 // TODO(hughv): Investigate why percent escapes are being added. Removing
462 // them isn't necessary on Android.
463 // convert HTML escaped characters to UTF8.
464 NSString *removePercent =
465 [base stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
466 // remove leading and trailing ".
468 range.length = [removePercent length] - 2;
470 NSString *removeQuotes = [removePercent substringWithRange:range];
472 NSString *removeEscapedQuotes =
473 [removeQuotes stringByReplacingOccurrencesOfString:@"\\\""
476 NSString *removeBackslash =
477 [removeEscapedQuotes stringByReplacingOccurrencesOfString:@"\\\\"
479 return removeBackslash;