1/*
2 * libjingle
3 * Copyright 2014, Google Inc.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions are met:
7 *
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.
15 *
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.
26 */
27
28#import "APPRTCConnectionManager.h"
29
30#import <AVFoundation/AVFoundation.h>
31#import "APPRTCAppClient.h"
32#import "GAEChannelClient.h"
33#import "RTCICECandidate.h"
34#import "RTCMediaConstraints.h"
35#import "RTCMediaStream.h"
36#import "RTCPair.h"
37#import "RTCPeerConnection.h"
38#import "RTCPeerConnectionDelegate.h"
39#import "RTCPeerConnectionFactory.h"
40#import "RTCSessionDescription.h"
41#import "RTCSessionDescriptionDelegate.h"
42#import "RTCStatsDelegate.h"
43#import "RTCVideoCapturer.h"
44#import "RTCVideoSource.h"
45
46@interface APPRTCConnectionManager ()
47    <APPRTCAppClientDelegate, GAEMessageHandler, RTCPeerConnectionDelegate,
48     RTCSessionDescriptionDelegate, RTCStatsDelegate>
49
50@property(nonatomic, strong) APPRTCAppClient* client;
51@property(nonatomic, strong) RTCPeerConnection* peerConnection;
52@property(nonatomic, strong) RTCPeerConnectionFactory* peerConnectionFactory;
53@property(nonatomic, strong) RTCVideoSource* videoSource;
54@property(nonatomic, strong) NSMutableArray* queuedRemoteCandidates;
55
56@end
57
58@implementation APPRTCConnectionManager {
59  NSTimer* _statsTimer;
60}
61
62- (instancetype)initWithDelegate:(id<APPRTCConnectionManagerDelegate>)delegate
63                          logger:(id<APPRTCLogger>)logger {
64  if (self = [super init]) {
65    self.delegate = delegate;
66    self.logger = logger;
67    self.peerConnectionFactory = [[RTCPeerConnectionFactory alloc] init];
68    // TODO(tkchin): turn this into a button.
69    // Uncomment for stat logs.
70    // _statsTimer =
71    //     [NSTimer scheduledTimerWithTimeInterval:10
72    //                                      target:self
73    //                                    selector:@selector(didFireStatsTimer:)
74    //                                    userInfo:nil
75    //                                     repeats:YES];
76  }
77  return self;
78}
79
80- (void)dealloc {
81  [self disconnect];
82}
83
84- (BOOL)connectToRoomWithURL:(NSURL*)url {
85  if (self.client) {
86    // Already have a connection.
87    return NO;
88  }
89  self.client = [[APPRTCAppClient alloc] initWithDelegate:self
90                                           messageHandler:self];
91  [self.client connectToRoom:url];
92  return YES;
93}
94
95- (void)disconnect {
96  if (!self.client) {
97    return;
98  }
99  [self.client
100      sendData:[@"{\"type\": \"bye\"}" dataUsingEncoding:NSUTF8StringEncoding]];
101  [self.peerConnection close];
102  self.peerConnection = nil;
103  self.client = nil;
104  self.videoSource = nil;
105  self.queuedRemoteCandidates = nil;
106}
107
108#pragma mark - APPRTCAppClientDelegate
109
110- (void)appClient:(APPRTCAppClient*)appClient
111    didErrorWithMessage:(NSString*)message {
112  [self.delegate connectionManager:self
113               didErrorWithMessage:message];
114}
115
116- (void)appClient:(APPRTCAppClient*)appClient
117    didReceiveICEServers:(NSArray*)servers {
118  self.queuedRemoteCandidates = [NSMutableArray array];
119  RTCMediaConstraints* constraints = [[RTCMediaConstraints alloc]
120      initWithMandatoryConstraints:
121          @[
122            [[RTCPair alloc] initWithKey:@"OfferToReceiveAudio" value:@"true"],
123            [[RTCPair alloc] initWithKey:@"OfferToReceiveVideo" value:@"true"]
124          ]
125               optionalConstraints:
126                   @[
127                     [[RTCPair alloc] initWithKey:@"internalSctpDataChannels"
128                                            value:@"true"],
129                     [[RTCPair alloc] initWithKey:@"DtlsSrtpKeyAgreement"
130                                            value:@"true"]
131                   ]];
132  self.peerConnection =
133      [self.peerConnectionFactory peerConnectionWithICEServers:servers
134                                                   constraints:constraints
135                                                      delegate:self];
136  RTCMediaStream* lms =
137      [self.peerConnectionFactory mediaStreamWithLabel:@"ARDAMS"];
138
139  // The iOS simulator doesn't provide any sort of camera capture
140  // support or emulation (http://goo.gl/rHAnC1) so don't bother
141  // trying to open a local stream.
142  RTCVideoTrack* localVideoTrack;
143
144  // TODO(tkchin): local video capture for OSX. See
145  // https://code.google.com/p/webrtc/issues/detail?id=3417.
146#if !TARGET_IPHONE_SIMULATOR && TARGET_OS_IPHONE
147  NSString* cameraID = nil;
148  for (AVCaptureDevice* captureDevice in
149       [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo]) {
150    if (captureDevice.position == AVCaptureDevicePositionFront) {
151      cameraID = [captureDevice localizedName];
152      break;
153    }
154  }
155  NSAssert(cameraID, @"Unable to get the front camera id");
156
157  RTCVideoCapturer* capturer =
158      [RTCVideoCapturer capturerWithDeviceName:cameraID];
159  self.videoSource = [self.peerConnectionFactory
160      videoSourceWithCapturer:capturer
161                  constraints:self.client.videoConstraints];
162  localVideoTrack =
163      [self.peerConnectionFactory videoTrackWithID:@"ARDAMSv0"
164                                            source:self.videoSource];
165  if (localVideoTrack) {
166    [lms addVideoTrack:localVideoTrack];
167  }
168  [self.delegate connectionManager:self
169         didReceiveLocalVideoTrack:localVideoTrack];
170#endif
171
172  [lms addAudioTrack:[self.peerConnectionFactory audioTrackWithID:@"ARDAMSa0"]];
173  [self.peerConnection addStream:lms constraints:constraints];
174  [self.logger logMessage:@"onICEServers - added local stream."];
175}
176
177#pragma mark - GAEMessageHandler methods
178
179- (void)onOpen {
180  if (!self.client.initiator) {
181    [self.logger logMessage:@"Callee; waiting for remote offer"];
182    return;
183  }
184  [self.logger logMessage:@"GAE onOpen - create offer."];
185  RTCPair* audio =
186      [[RTCPair alloc] initWithKey:@"OfferToReceiveAudio" value:@"true"];
187  RTCPair* video =
188      [[RTCPair alloc] initWithKey:@"OfferToReceiveVideo" value:@"true"];
189  NSArray* mandatory = @[ audio, video ];
190  RTCMediaConstraints* constraints =
191      [[RTCMediaConstraints alloc] initWithMandatoryConstraints:mandatory
192                                            optionalConstraints:nil];
193  [self.peerConnection createOfferWithDelegate:self constraints:constraints];
194  [self.logger logMessage:@"PC - createOffer."];
195}
196
197- (void)onMessage:(NSDictionary*)messageData {
198  NSString* type = messageData[@"type"];
199  NSAssert(type, @"Missing type: %@", messageData);
200  [self.logger logMessage:[NSString stringWithFormat:@"GAE onMessage type - %@",
201                                                      type]];
202  if ([type isEqualToString:@"candidate"]) {
203    NSString* mid = messageData[@"id"];
204    NSNumber* sdpLineIndex = messageData[@"label"];
205    NSString* sdp = messageData[@"candidate"];
206    RTCICECandidate* candidate =
207        [[RTCICECandidate alloc] initWithMid:mid
208                                       index:sdpLineIndex.intValue
209                                         sdp:sdp];
210    if (self.queuedRemoteCandidates) {
211      [self.queuedRemoteCandidates addObject:candidate];
212    } else {
213      [self.peerConnection addICECandidate:candidate];
214    }
215  } else if ([type isEqualToString:@"offer"] ||
216             [type isEqualToString:@"answer"]) {
217    NSString* sdpString = messageData[@"sdp"];
218    RTCSessionDescription* sdp = [[RTCSessionDescription alloc]
219        initWithType:type
220                 sdp:[[self class] preferISAC:sdpString]];
221    [self.peerConnection setRemoteDescriptionWithDelegate:self
222                                       sessionDescription:sdp];
223    [self.logger logMessage:@"PC - setRemoteDescription."];
224  } else if ([type isEqualToString:@"bye"]) {
225    [self.delegate connectionManagerDidReceiveHangup:self];
226  } else {
227    NSAssert(NO, @"Invalid message: %@", messageData);
228  }
229}
230
231- (void)onClose {
232  [self.logger logMessage:@"GAE onClose."];
233  [self.delegate connectionManagerDidReceiveHangup:self];
234}
235
236- (void)onError:(int)code withDescription:(NSString*)description {
237  NSString* message = [NSString stringWithFormat:@"GAE onError: %d, %@",
238                                code, description];
239  [self.logger logMessage:message];
240  [self.delegate connectionManager:self
241               didErrorWithMessage:message];
242}
243
244#pragma mark - RTCPeerConnectionDelegate
245
246- (void)peerConnectionOnError:(RTCPeerConnection*)peerConnection {
247  dispatch_async(dispatch_get_main_queue(), ^{
248    NSString* message = @"PeerConnection error";
249    NSLog(@"%@", message);
250    NSAssert(NO, @"PeerConnection failed.");
251    [self.delegate connectionManager:self
252                 didErrorWithMessage:message];
253  });
254}
255
256- (void)peerConnection:(RTCPeerConnection*)peerConnection
257    signalingStateChanged:(RTCSignalingState)stateChanged {
258  dispatch_async(dispatch_get_main_queue(), ^{
259    NSLog(@"PCO onSignalingStateChange: %d", stateChanged);
260  });
261}
262
263- (void)peerConnection:(RTCPeerConnection*)peerConnection
264           addedStream:(RTCMediaStream*)stream {
265  dispatch_async(dispatch_get_main_queue(), ^{
266    NSLog(@"PCO onAddStream.");
267    NSAssert([stream.audioTracks count] == 1 || [stream.videoTracks count] == 1,
268             @"Expected audio or video track");
269    NSAssert([stream.audioTracks count] <= 1,
270             @"Expected at most 1 audio stream");
271    NSAssert([stream.videoTracks count] <= 1,
272             @"Expected at most 1 video stream");
273    if ([stream.videoTracks count] != 0) {
274      [self.delegate connectionManager:self
275            didReceiveRemoteVideoTrack:stream.videoTracks[0]];
276    }
277  });
278}
279
280- (void)peerConnection:(RTCPeerConnection*)peerConnection
281         removedStream:(RTCMediaStream*)stream {
282  dispatch_async(dispatch_get_main_queue(),
283                 ^{ NSLog(@"PCO onRemoveStream."); });
284}
285
286- (void)peerConnectionOnRenegotiationNeeded:(RTCPeerConnection*)peerConnection {
287  dispatch_async(dispatch_get_main_queue(), ^{
288    NSLog(@"PCO onRenegotiationNeeded - ignoring because AppRTC has a "
289           "predefined negotiation strategy");
290  });
291}
292
293- (void)peerConnection:(RTCPeerConnection*)peerConnection
294       gotICECandidate:(RTCICECandidate*)candidate {
295  dispatch_async(dispatch_get_main_queue(), ^{
296    NSLog(@"PCO onICECandidate.\n  Mid[%@] Index[%li] Sdp[%@]",
297          candidate.sdpMid,
298          (long)candidate.sdpMLineIndex,
299          candidate.sdp);
300    NSDictionary* json = @{
301      @"type" : @"candidate",
302      @"label" : @(candidate.sdpMLineIndex),
303      @"id" : candidate.sdpMid,
304      @"candidate" : candidate.sdp
305    };
306    NSError* error;
307    NSData* data =
308        [NSJSONSerialization dataWithJSONObject:json options:0 error:&error];
309    if (!error) {
310      [self.client sendData:data];
311    } else {
312      NSAssert(NO,
313               @"Unable to serialize JSON object with error: %@",
314               error.localizedDescription);
315    }
316  });
317}
318
319- (void)peerConnection:(RTCPeerConnection*)peerConnection
320    iceGatheringChanged:(RTCICEGatheringState)newState {
321  dispatch_async(dispatch_get_main_queue(),
322                 ^{ NSLog(@"PCO onIceGatheringChange. %d", newState); });
323}
324
325- (void)peerConnection:(RTCPeerConnection*)peerConnection
326    iceConnectionChanged:(RTCICEConnectionState)newState {
327  dispatch_async(dispatch_get_main_queue(), ^{
328    NSLog(@"PCO onIceConnectionChange. %d", newState);
329    if (newState == RTCICEConnectionConnected)
330      [self.logger logMessage:@"ICE Connection Connected."];
331    NSAssert(newState != RTCICEConnectionFailed, @"ICE Connection failed!");
332  });
333}
334
335- (void)peerConnection:(RTCPeerConnection*)peerConnection
336    didOpenDataChannel:(RTCDataChannel*)dataChannel {
337  NSAssert(NO, @"AppRTC doesn't use DataChannels");
338}
339
340#pragma mark - RTCSessionDescriptionDelegate
341
342- (void)peerConnection:(RTCPeerConnection*)peerConnection
343    didCreateSessionDescription:(RTCSessionDescription*)origSdp
344                          error:(NSError*)error {
345  dispatch_async(dispatch_get_main_queue(), ^{
346    if (error) {
347      [self.logger logMessage:@"SDP onFailure."];
348      NSAssert(NO, error.description);
349      return;
350    }
351    [self.logger logMessage:@"SDP onSuccess(SDP) - set local description."];
352    RTCSessionDescription* sdp = [[RTCSessionDescription alloc]
353        initWithType:origSdp.type
354                 sdp:[[self class] preferISAC:origSdp.description]];
355    [self.peerConnection setLocalDescriptionWithDelegate:self
356                                      sessionDescription:sdp];
357    [self.logger logMessage:@"PC setLocalDescription."];
358    NSDictionary* json = @{@"type" : sdp.type, @"sdp" : sdp.description};
359    NSError* jsonError;
360    NSData* data = [NSJSONSerialization dataWithJSONObject:json
361                                                   options:0
362                                                     error:&jsonError];
363    NSAssert(!jsonError, @"Error: %@", jsonError.description);
364    [self.client sendData:data];
365  });
366}
367
368- (void)peerConnection:(RTCPeerConnection*)peerConnection
369    didSetSessionDescriptionWithError:(NSError*)error {
370  dispatch_async(dispatch_get_main_queue(), ^{
371    if (error) {
372      [self.logger logMessage:@"SDP onFailure."];
373      NSAssert(NO, error.description);
374      return;
375    }
376    [self.logger logMessage:@"SDP onSuccess() - possibly drain candidates"];
377    if (!self.client.initiator) {
378      if (self.peerConnection.remoteDescription &&
379          !self.peerConnection.localDescription) {
380        [self.logger logMessage:@"Callee, setRemoteDescription succeeded"];
381        RTCPair* audio = [[RTCPair alloc] initWithKey:@"OfferToReceiveAudio"
382                                                value:@"true"];
383        RTCPair* video = [[RTCPair alloc] initWithKey:@"OfferToReceiveVideo"
384                                                value:@"true"];
385        NSArray* mandatory = @[ audio, video ];
386        RTCMediaConstraints* constraints = [[RTCMediaConstraints alloc]
387            initWithMandatoryConstraints:mandatory
388                     optionalConstraints:nil];
389        [self.peerConnection createAnswerWithDelegate:self
390                                          constraints:constraints];
391        [self.logger logMessage:@"PC - createAnswer."];
392      } else {
393        [self.logger logMessage:@"SDP onSuccess - drain candidates"];
394        [self drainRemoteCandidates];
395      }
396    } else {
397      if (self.peerConnection.remoteDescription) {
398        [self.logger logMessage:@"SDP onSuccess - drain candidates"];
399        [self drainRemoteCandidates];
400      }
401    }
402  });
403}
404
405#pragma mark - RTCStatsDelegate methods
406
407- (void)peerConnection:(RTCPeerConnection*)peerConnection
408           didGetStats:(NSArray*)stats {
409  dispatch_async(dispatch_get_main_queue(), ^{
410    NSString* message = [NSString stringWithFormat:@"Stats:\n %@", stats];
411    [self.logger logMessage:message];
412  });
413}
414
415#pragma mark - Private
416
417// Match |pattern| to |string| and return the first group of the first
418// match, or nil if no match was found.
419+ (NSString*)firstMatch:(NSRegularExpression*)pattern
420             withString:(NSString*)string {
421  NSTextCheckingResult* result =
422      [pattern firstMatchInString:string
423                          options:0
424                            range:NSMakeRange(0, [string length])];
425  if (!result)
426    return nil;
427  return [string substringWithRange:[result rangeAtIndex:1]];
428}
429
430// Mangle |origSDP| to prefer the ISAC/16k audio codec.
431+ (NSString*)preferISAC:(NSString*)origSDP {
432  int mLineIndex = -1;
433  NSString* isac16kRtpMap = nil;
434  NSArray* lines = [origSDP componentsSeparatedByString:@"\n"];
435  NSRegularExpression* isac16kRegex = [NSRegularExpression
436      regularExpressionWithPattern:@"^a=rtpmap:(\\d+) ISAC/16000[\r]?$"
437                           options:0
438                             error:nil];
439  for (int i = 0;
440       (i < [lines count]) && (mLineIndex == -1 || isac16kRtpMap == nil);
441       ++i) {
442    NSString* line = [lines objectAtIndex:i];
443    if ([line hasPrefix:@"m=audio "]) {
444      mLineIndex = i;
445      continue;
446    }
447    isac16kRtpMap = [self firstMatch:isac16kRegex withString:line];
448  }
449  if (mLineIndex == -1) {
450    NSLog(@"No m=audio line, so can't prefer iSAC");
451    return origSDP;
452  }
453  if (isac16kRtpMap == nil) {
454    NSLog(@"No ISAC/16000 line, so can't prefer iSAC");
455    return origSDP;
456  }
457  NSArray* origMLineParts =
458      [[lines objectAtIndex:mLineIndex] componentsSeparatedByString:@" "];
459  NSMutableArray* newMLine =
460      [NSMutableArray arrayWithCapacity:[origMLineParts count]];
461  int origPartIndex = 0;
462  // Format is: m=<media> <port> <proto> <fmt> ...
463  [newMLine addObject:[origMLineParts objectAtIndex:origPartIndex++]];
464  [newMLine addObject:[origMLineParts objectAtIndex:origPartIndex++]];
465  [newMLine addObject:[origMLineParts objectAtIndex:origPartIndex++]];
466  [newMLine addObject:isac16kRtpMap];
467  for (; origPartIndex < [origMLineParts count]; ++origPartIndex) {
468    if (![isac16kRtpMap
469            isEqualToString:[origMLineParts objectAtIndex:origPartIndex]]) {
470      [newMLine addObject:[origMLineParts objectAtIndex:origPartIndex]];
471    }
472  }
473  NSMutableArray* newLines = [NSMutableArray arrayWithCapacity:[lines count]];
474  [newLines addObjectsFromArray:lines];
475  [newLines replaceObjectAtIndex:mLineIndex
476                      withObject:[newMLine componentsJoinedByString:@" "]];
477  return [newLines componentsJoinedByString:@"\n"];
478}
479
480- (void)drainRemoteCandidates {
481  for (RTCICECandidate* candidate in self.queuedRemoteCandidates) {
482    [self.peerConnection addICECandidate:candidate];
483  }
484  self.queuedRemoteCandidates = nil;
485}
486
487- (void)didFireStatsTimer:(NSTimer*)timer {
488  if (self.peerConnection) {
489    [self.peerConnection getStatsWithDelegate:self
490                             mediaStreamTrack:nil
491                             statsOutputLevel:RTCStatsOutputLevelDebug];
492  }
493}
494
495@end
496