Implement mac version of AppRTCDemo.

- Refactored and moved AppRTCDemo to support sharing AppRTC connection code between iOS and mac counterparts.
- Refactored OpenGL rendering code to be shared between iOS and mac counterparts.
- iOS AppRTCDemo now respects video aspect ratio.

BUG=2168
R=fischman@webrtc.org

Review URL: https://webrtc-codereview.appspot.com/17589004

git-svn-id: http://webrtc.googlecode.com/svn/trunk@6291 4adac7df-926f-26a2-2b94-8c16560cd09d
diff --git a/talk/examples/objc/AppRTCDemo/APPRTCConnectionManager.m b/talk/examples/objc/AppRTCDemo/APPRTCConnectionManager.m
new file mode 100644
index 0000000..6d0a5a2
--- /dev/null
+++ b/talk/examples/objc/AppRTCDemo/APPRTCConnectionManager.m
@@ -0,0 +1,494 @@
+/*
+ * 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 <AVFoundation/AVFoundation.h>
+#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 ()
+    <APPRTCAppClientDelegate, GAEMessageHandler, RTCPeerConnectionDelegate,
+     RTCSessionDescriptionDelegate, RTCStatsDelegate>
+
+@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<APPRTCConnectionManagerDelegate>)delegate
+                          logger:(id<APPRTCLogger>)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.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 constraints:constraints];
+  [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)peerConnectionOnError:(RTCPeerConnection*)peerConnection {
+  dispatch_async(dispatch_get_main_queue(), ^{
+    NSString* message = @"PeerConnection error";
+    NSLog(@"%@", message);
+    NSAssert(NO, @"PeerConnection failed.");
+    [self.delegate connectionManager:self
+                 didErrorWithMessage:message];
+  });
+}
+
+- (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=<media> <port> <proto> <fmt> ...
+  [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