iOS camera switching video capturer.

Introduces a new capture class derived from cricket::VideoCapturer that
provides the ability to switch cameras and updates AppRTCDemo to use it.
Some future work pending to clean up AppRTCDemo UI.

BUG=4070
R=magjed@webrtc.org

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

Cr-Commit-Position: refs/heads/master@{#9137}
diff --git a/talk/examples/objc/AppRTCDemo/ARDAppClient.m b/talk/examples/objc/AppRTCDemo/ARDAppClient.m
index 695b86e..e4c2f81 100644
--- a/talk/examples/objc/AppRTCDemo/ARDAppClient.m
+++ b/talk/examples/objc/AppRTCDemo/ARDAppClient.m
@@ -27,7 +27,15 @@
 
 #import "ARDAppClient+Internal.h"
 
-#import <AVFoundation/AVFoundation.h>
+#if defined(WEBRTC_IOS)
+#import "RTCAVFoundationVideoSource.h"
+#endif
+#import "RTCICEServer.h"
+#import "RTCMediaConstraints.h"
+#import "RTCMediaStream.h"
+#import "RTCPair.h"
+#import "RTCVideoCapturer.h"
+#import "RTCAVFoundationVideoSource.h"
 
 #import "ARDAppEngineClient.h"
 #import "ARDCEODTURNClient.h"
@@ -37,13 +45,8 @@
 #import "ARDUtilities.h"
 #import "ARDWebSocketChannel.h"
 #import "RTCICECandidate+JSON.h"
-#import "RTCICEServer.h"
-#import "RTCMediaConstraints.h"
-#import "RTCMediaStream.h"
-#import "RTCPair.h"
 #import "RTCSessionDescription+JSON.h"
-#import "RTCVideoCapturer.h"
-#import "RTCVideoTrack.h"
+
 
 static NSString * const kARDDefaultSTUNServerUrl =
     @"stun:stun.l.google.com:19302";
@@ -484,39 +487,33 @@
 
 - (RTCMediaStream *)createLocalMediaStream {
   RTCMediaStream* localStream = [_factory mediaStreamWithLabel:@"ARDAMS"];
-  RTCVideoTrack* localVideoTrack = nil;
+  RTCVideoTrack* localVideoTrack = [self createLocalVideoTrack];
+  if (localVideoTrack) {
+    [localStream addVideoTrack:localVideoTrack];
+    [_delegate appClient:self didReceiveLocalVideoTrack:localVideoTrack];
+  }
+  [localStream addAudioTrack:[_factory audioTrackWithID:@"ARDAMSa0"]];
+  return localStream;
+}
 
+- (RTCVideoTrack *)createLocalVideoTrack {
+  RTCVideoTrack* localVideoTrack = nil;
   // 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.
   // 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];
   RTCMediaConstraints *mediaConstraints = [self defaultMediaStreamConstraints];
-  RTCVideoSource *videoSource =
-      [_factory videoSourceWithCapturer:capturer
-                            constraints:mediaConstraints];
+  RTCAVFoundationVideoSource *source =
+      [[RTCAVFoundationVideoSource alloc] initWithFactory:_factory
+                                              constraints:mediaConstraints];
   localVideoTrack =
-      [_factory videoTrackWithID:@"ARDAMSv0" source:videoSource];
-  if (localVideoTrack) {
-    [localStream addVideoTrack:localVideoTrack];
-  }
-  [_delegate appClient:self didReceiveLocalVideoTrack:localVideoTrack];
+      [[RTCVideoTrack alloc] initWithFactory:_factory
+                                      source:source
+                                     trackId:@"ARDAMSv0"];
 #endif
-  [localStream addAudioTrack:[_factory audioTrackWithID:@"ARDAMSa0"]];
-  return localStream;
+  return localVideoTrack;
 }
 
 #pragma mark - Collider methods
diff --git a/talk/examples/objc/AppRTCDemo/ios/ARDVideoCallView.h b/talk/examples/objc/AppRTCDemo/ios/ARDVideoCallView.h
index 1bb5b5c..7c1decb 100644
--- a/talk/examples/objc/AppRTCDemo/ios/ARDVideoCallView.h
+++ b/talk/examples/objc/AppRTCDemo/ios/ARDVideoCallView.h
@@ -32,6 +32,9 @@
 @class ARDVideoCallView;
 @protocol ARDVideoCallViewDelegate <NSObject>
 
+// Called when the camera switch button is pressed.
+- (void)videoCallViewDidSwitchCamera:(ARDVideoCallView *)view;
+
 // Called when the hangup button is pressed.
 - (void)videoCallViewDidHangup:(ARDVideoCallView *)view;
 
diff --git a/talk/examples/objc/AppRTCDemo/ios/ARDVideoCallView.m b/talk/examples/objc/AppRTCDemo/ios/ARDVideoCallView.m
index 852951e..47bfe89 100644
--- a/talk/examples/objc/AppRTCDemo/ios/ARDVideoCallView.m
+++ b/talk/examples/objc/AppRTCDemo/ios/ARDVideoCallView.m
@@ -30,19 +30,20 @@
 #import <AVFoundation/AVFoundation.h>
 #import "UIImage+ARDUtilities.h"
 
-static CGFloat const kHangupButtonPadding = 16;
-static CGFloat const kHangupButtonSize = 48;
-static CGFloat const kLocalVideoViewWidth = 90;
-static CGFloat const kLocalVideoViewHeight = 120;
+static CGFloat const kButtonPadding = 16;
+static CGFloat const kButtonSize = 48;
+static CGFloat const kLocalVideoViewSize = 120;
 static CGFloat const kLocalVideoViewPadding = 8;
 
 @interface ARDVideoCallView () <RTCEAGLVideoViewDelegate>
 @end
 
 @implementation ARDVideoCallView {
+  UIButton *_cameraSwitchButton;
   UIButton *_hangupButton;
   CGSize _localVideoSize;
   CGSize _remoteVideoSize;
+  BOOL _useRearCamera;
 }
 
 @synthesize statusLabel = _statusLabel;
@@ -56,17 +57,30 @@
     _remoteVideoView.delegate = self;
     [self addSubview:_remoteVideoView];
 
+    // TODO(tkchin): replace this with a view that renders layer from
+    // AVCaptureSession.
     _localVideoView = [[RTCEAGLVideoView alloc] initWithFrame:CGRectZero];
-    _localVideoView.transform = CGAffineTransformMakeScale(-1, 1);
     _localVideoView.delegate = self;
     [self addSubview:_localVideoView];
 
+    // TODO(tkchin): don't display this if we can't actually do camera switch.
+    _cameraSwitchButton = [UIButton buttonWithType:UIButtonTypeCustom];
+    _cameraSwitchButton.backgroundColor = [UIColor whiteColor];
+    _cameraSwitchButton.layer.cornerRadius = kButtonSize / 2;
+    _cameraSwitchButton.layer.masksToBounds = YES;
+    UIImage *image = [UIImage imageNamed:@"ic_switch_video_black_24dp.png"];
+    [_cameraSwitchButton setImage:image forState:UIControlStateNormal];
+    [_cameraSwitchButton addTarget:self
+                      action:@selector(onCameraSwitch:)
+            forControlEvents:UIControlEventTouchUpInside];
+    [self addSubview:_cameraSwitchButton];
+
     _hangupButton = [UIButton buttonWithType:UIButtonTypeCustom];
     _hangupButton.backgroundColor = [UIColor redColor];
-    _hangupButton.layer.cornerRadius = kHangupButtonSize / 2;
+    _hangupButton.layer.cornerRadius = kButtonSize / 2;
     _hangupButton.layer.masksToBounds = YES;
-    UIImage *image = [UIImage imageForName:@"ic_call_end_black_24dp.png"
-                                     color:[UIColor whiteColor]];
+    image = [UIImage imageForName:@"ic_call_end_black_24dp.png"
+                            color:[UIColor whiteColor]];
     [_hangupButton setImage:image forState:UIControlStateNormal];
     [_hangupButton addTarget:self
                       action:@selector(onHangup:)
@@ -104,21 +118,36 @@
     _remoteVideoView.frame = bounds;
   }
 
-  CGRect localVideoFrame = CGRectZero;
-  localVideoFrame.origin.x =
-      CGRectGetMaxX(bounds) - kLocalVideoViewWidth - kLocalVideoViewPadding;
-  localVideoFrame.origin.y =
-      CGRectGetMaxY(bounds) - kLocalVideoViewHeight - kLocalVideoViewPadding;
-  localVideoFrame.size.width = kLocalVideoViewWidth;
-  localVideoFrame.size.height = kLocalVideoViewHeight;
-  _localVideoView.frame = localVideoFrame;
+  if (_localVideoSize.width && _localVideoSize.height > 0) {
+    // Aspect fit local video view into a square box.
+    CGRect localVideoFrame =
+        CGRectMake(0, 0, kLocalVideoViewSize, kLocalVideoViewSize);
+    localVideoFrame =
+        AVMakeRectWithAspectRatioInsideRect(_localVideoSize, localVideoFrame);
 
+    // Place the view in the bottom right.
+    localVideoFrame.origin.x = CGRectGetMaxX(bounds)
+        - localVideoFrame.size.width - kLocalVideoViewPadding;
+    localVideoFrame.origin.y = CGRectGetMaxY(bounds)
+        - localVideoFrame.size.height - kLocalVideoViewPadding;
+    _localVideoView.frame = localVideoFrame;
+  } else {
+    _localVideoView.frame = bounds;
+  }
+
+  // Place hangup button in the bottom left.
   _hangupButton.frame =
-      CGRectMake(CGRectGetMinX(bounds) + kHangupButtonPadding,
-                 CGRectGetMaxY(bounds) - kHangupButtonPadding -
-                     kHangupButtonSize,
-                 kHangupButtonSize,
-                 kHangupButtonSize);
+      CGRectMake(CGRectGetMinX(bounds) + kButtonPadding,
+                 CGRectGetMaxY(bounds) - kButtonPadding -
+                     kButtonSize,
+                 kButtonSize,
+                 kButtonSize);
+
+  // Place button to the right of hangup button.
+  CGRect cameraSwitchFrame = _hangupButton.frame;
+  cameraSwitchFrame.origin.x =
+      CGRectGetMaxX(cameraSwitchFrame) + kButtonPadding;
+  _cameraSwitchButton.frame = cameraSwitchFrame;
 
   [_statusLabel sizeToFit];
   _statusLabel.center =
@@ -130,6 +159,7 @@
 - (void)videoView:(RTCEAGLVideoView*)videoView didChangeVideoSize:(CGSize)size {
   if (videoView == _localVideoView) {
     _localVideoSize = size;
+    _localVideoView.hidden = CGSizeEqualToSize(CGSizeZero, _localVideoSize);
   } else if (videoView == _remoteVideoView) {
     _remoteVideoSize = size;
   }
@@ -138,6 +168,10 @@
 
 #pragma mark - Private
 
+- (void)onCameraSwitch:(id)sender {
+  [_delegate videoCallViewDidSwitchCamera:self];
+}
+
 - (void)onHangup:(id)sender {
   [_delegate videoCallViewDidHangup:self];
 }
diff --git a/talk/examples/objc/AppRTCDemo/ios/ARDVideoCallViewController.m b/talk/examples/objc/AppRTCDemo/ios/ARDVideoCallViewController.m
index af4aaff..b12a61a 100644
--- a/talk/examples/objc/AppRTCDemo/ios/ARDVideoCallViewController.m
+++ b/talk/examples/objc/AppRTCDemo/ios/ARDVideoCallViewController.m
@@ -27,11 +27,15 @@
 
 #import "ARDVideoCallViewController.h"
 
+#import "RTCAVFoundationVideoSource.h"
+
 #import "ARDAppClient.h"
 #import "ARDVideoCallView.h"
 
 @interface ARDVideoCallViewController () <ARDAppClientDelegate,
     ARDVideoCallViewDelegate>
+@property(nonatomic, strong) RTCVideoTrack *localVideoTrack;
+@property(nonatomic, strong) RTCVideoTrack *remoteVideoTrack;
 @property(nonatomic, readonly) ARDVideoCallView *videoCallView;
 @end
 
@@ -90,19 +94,13 @@
 
 - (void)appClient:(ARDAppClient *)client
     didReceiveLocalVideoTrack:(RTCVideoTrack *)localVideoTrack {
-  if (!_localVideoTrack) {
-    _localVideoTrack = localVideoTrack;
-    [_localVideoTrack addRenderer:_videoCallView.localVideoView];
-  }
+  self.localVideoTrack = localVideoTrack;
 }
 
 - (void)appClient:(ARDAppClient *)client
     didReceiveRemoteVideoTrack:(RTCVideoTrack *)remoteVideoTrack {
-  if (!_remoteVideoTrack) {
-    _remoteVideoTrack = remoteVideoTrack;
-    [_remoteVideoTrack addRenderer:_videoCallView.remoteVideoView];
-    _videoCallView.statusLabel.hidden = YES;
-  }
+  self.remoteVideoTrack = remoteVideoTrack;
+  _videoCallView.statusLabel.hidden = YES;
 }
 
 - (void)appClient:(ARDAppClient *)client
@@ -119,24 +117,54 @@
   [self hangup];
 }
 
+- (void)videoCallViewDidSwitchCamera:(ARDVideoCallView *)view {
+  // TODO(tkchin): Rate limit this so you can't tap continously on it.
+  // Probably through an animation.
+  [self switchCamera];
+}
+
 #pragma mark - Private
 
+- (void)setLocalVideoTrack:(RTCVideoTrack *)localVideoTrack {
+  if (_localVideoTrack == localVideoTrack) {
+    return;
+  }
+  [_localVideoTrack removeRenderer:_videoCallView.localVideoView];
+  _localVideoTrack = nil;
+  [_videoCallView.localVideoView renderFrame:nil];
+  _localVideoTrack = localVideoTrack;
+  [_localVideoTrack addRenderer:_videoCallView.localVideoView];
+}
+
+- (void)setRemoteVideoTrack:(RTCVideoTrack *)remoteVideoTrack {
+  if (_remoteVideoTrack == remoteVideoTrack) {
+    return;
+  }
+  [_remoteVideoTrack removeRenderer:_videoCallView.localVideoView];
+  _remoteVideoTrack = nil;
+  [_videoCallView.remoteVideoView renderFrame:nil];
+  _remoteVideoTrack = remoteVideoTrack;
+  [_remoteVideoTrack addRenderer:_videoCallView.remoteVideoView];
+}
+
 - (void)hangup {
-  if (_remoteVideoTrack) {
-    [_remoteVideoTrack removeRenderer:_videoCallView.remoteVideoView];
-    _remoteVideoTrack = nil;
-    [_videoCallView.remoteVideoView renderFrame:nil];
-  }
-  if (_localVideoTrack) {
-    [_localVideoTrack removeRenderer:_videoCallView.localVideoView];
-    _localVideoTrack = nil;
-    [_videoCallView.localVideoView renderFrame:nil];
-  }
+  self.remoteVideoTrack = nil;
+  self.localVideoTrack = nil;
   [_client disconnect];
   [self.presentingViewController dismissViewControllerAnimated:YES
                                                     completion:nil];
 }
 
+- (void)switchCamera {
+  RTCVideoSource* source = self.localVideoTrack.source;
+  if ([source isKindOfClass:[RTCAVFoundationVideoSource class]]) {
+    RTCAVFoundationVideoSource* avSource = (RTCAVFoundationVideoSource*)source;
+    avSource.useBackCamera = !avSource.useBackCamera;
+    _videoCallView.localVideoView.transform = avSource.useBackCamera ?
+        CGAffineTransformIdentity : CGAffineTransformMakeScale(-1, 1);
+  }
+}
+
 - (NSString *)statusTextForState:(RTCICEConnectionState)state {
   switch (state) {
     case RTCICEConnectionNew:
diff --git a/talk/examples/objc/AppRTCDemo/ios/resources/ic_switch_video_black_24dp.png b/talk/examples/objc/AppRTCDemo/ios/resources/ic_switch_video_black_24dp.png
new file mode 100644
index 0000000..85271c8
--- /dev/null
+++ b/talk/examples/objc/AppRTCDemo/ios/resources/ic_switch_video_black_24dp.png
Binary files differ
diff --git a/talk/examples/objc/AppRTCDemo/ios/resources/ic_switch_video_black_24dp@2x.png b/talk/examples/objc/AppRTCDemo/ios/resources/ic_switch_video_black_24dp@2x.png
new file mode 100644
index 0000000..62b13a6
--- /dev/null
+++ b/talk/examples/objc/AppRTCDemo/ios/resources/ic_switch_video_black_24dp@2x.png
Binary files differ