1*d9f75844SAndroid Build Coastguard Worker/* 2*d9f75844SAndroid Build Coastguard Worker * Copyright 2017 The WebRTC project authors. All Rights Reserved. 3*d9f75844SAndroid Build Coastguard Worker * 4*d9f75844SAndroid Build Coastguard Worker * Use of this source code is governed by a BSD-style license 5*d9f75844SAndroid Build Coastguard Worker * that can be found in the LICENSE file in the root of the source 6*d9f75844SAndroid Build Coastguard Worker * tree. An additional intellectual property rights grant can be found 7*d9f75844SAndroid Build Coastguard Worker * in the file PATENTS. All contributing project authors may 8*d9f75844SAndroid Build Coastguard Worker * be found in the AUTHORS file in the root of the source tree. 9*d9f75844SAndroid Build Coastguard Worker */ 10*d9f75844SAndroid Build Coastguard Worker 11*d9f75844SAndroid Build Coastguard Worker#import <Foundation/Foundation.h> 12*d9f75844SAndroid Build Coastguard Worker 13*d9f75844SAndroid Build Coastguard Worker#import "RTCCameraVideoCapturer.h" 14*d9f75844SAndroid Build Coastguard Worker#import "base/RTCLogging.h" 15*d9f75844SAndroid Build Coastguard Worker#import "base/RTCVideoFrameBuffer.h" 16*d9f75844SAndroid Build Coastguard Worker#import "components/video_frame_buffer/RTCCVPixelBuffer.h" 17*d9f75844SAndroid Build Coastguard Worker 18*d9f75844SAndroid Build Coastguard Worker#if TARGET_OS_IPHONE 19*d9f75844SAndroid Build Coastguard Worker#import "helpers/UIDevice+RTCDevice.h" 20*d9f75844SAndroid Build Coastguard Worker#endif 21*d9f75844SAndroid Build Coastguard Worker 22*d9f75844SAndroid Build Coastguard Worker#import "helpers/AVCaptureSession+DevicePosition.h" 23*d9f75844SAndroid Build Coastguard Worker#import "helpers/RTCDispatcher+Private.h" 24*d9f75844SAndroid Build Coastguard Worker#include "rtc_base/system/gcd_helpers.h" 25*d9f75844SAndroid Build Coastguard Worker 26*d9f75844SAndroid Build Coastguard Workerconst int64_t kNanosecondsPerSecond = 1000000000; 27*d9f75844SAndroid Build Coastguard Worker 28*d9f75844SAndroid Build Coastguard Worker@interface RTC_OBJC_TYPE (RTCCameraVideoCapturer) 29*d9f75844SAndroid Build Coastguard Worker()<AVCaptureVideoDataOutputSampleBufferDelegate> @property(nonatomic, 30*d9f75844SAndroid Build Coastguard Worker readonly) dispatch_queue_t frameQueue; 31*d9f75844SAndroid Build Coastguard Worker@property(nonatomic, strong) AVCaptureDevice *currentDevice; 32*d9f75844SAndroid Build Coastguard Worker@property(nonatomic, assign) BOOL hasRetriedOnFatalError; 33*d9f75844SAndroid Build Coastguard Worker@property(nonatomic, assign) BOOL isRunning; 34*d9f75844SAndroid Build Coastguard Worker// Will the session be running once all asynchronous operations have been completed? 35*d9f75844SAndroid Build Coastguard Worker@property(nonatomic, assign) BOOL willBeRunning; 36*d9f75844SAndroid Build Coastguard Worker@end 37*d9f75844SAndroid Build Coastguard Worker 38*d9f75844SAndroid Build Coastguard Worker@implementation RTC_OBJC_TYPE (RTCCameraVideoCapturer) { 39*d9f75844SAndroid Build Coastguard Worker AVCaptureVideoDataOutput *_videoDataOutput; 40*d9f75844SAndroid Build Coastguard Worker AVCaptureSession *_captureSession; 41*d9f75844SAndroid Build Coastguard Worker FourCharCode _preferredOutputPixelFormat; 42*d9f75844SAndroid Build Coastguard Worker FourCharCode _outputPixelFormat; 43*d9f75844SAndroid Build Coastguard Worker RTCVideoRotation _rotation; 44*d9f75844SAndroid Build Coastguard Worker#if TARGET_OS_IPHONE 45*d9f75844SAndroid Build Coastguard Worker UIDeviceOrientation _orientation; 46*d9f75844SAndroid Build Coastguard Worker BOOL _generatingOrientationNotifications; 47*d9f75844SAndroid Build Coastguard Worker#endif 48*d9f75844SAndroid Build Coastguard Worker} 49*d9f75844SAndroid Build Coastguard Worker 50*d9f75844SAndroid Build Coastguard Worker@synthesize frameQueue = _frameQueue; 51*d9f75844SAndroid Build Coastguard Worker@synthesize captureSession = _captureSession; 52*d9f75844SAndroid Build Coastguard Worker@synthesize currentDevice = _currentDevice; 53*d9f75844SAndroid Build Coastguard Worker@synthesize hasRetriedOnFatalError = _hasRetriedOnFatalError; 54*d9f75844SAndroid Build Coastguard Worker@synthesize isRunning = _isRunning; 55*d9f75844SAndroid Build Coastguard Worker@synthesize willBeRunning = _willBeRunning; 56*d9f75844SAndroid Build Coastguard Worker 57*d9f75844SAndroid Build Coastguard Worker- (instancetype)init { 58*d9f75844SAndroid Build Coastguard Worker return [self initWithDelegate:nil captureSession:[[AVCaptureSession alloc] init]]; 59*d9f75844SAndroid Build Coastguard Worker} 60*d9f75844SAndroid Build Coastguard Worker 61*d9f75844SAndroid Build Coastguard Worker- (instancetype)initWithDelegate:(__weak id<RTC_OBJC_TYPE(RTCVideoCapturerDelegate)>)delegate { 62*d9f75844SAndroid Build Coastguard Worker return [self initWithDelegate:delegate captureSession:[[AVCaptureSession alloc] init]]; 63*d9f75844SAndroid Build Coastguard Worker} 64*d9f75844SAndroid Build Coastguard Worker 65*d9f75844SAndroid Build Coastguard Worker// This initializer is used for testing. 66*d9f75844SAndroid Build Coastguard Worker- (instancetype)initWithDelegate:(__weak id<RTC_OBJC_TYPE(RTCVideoCapturerDelegate)>)delegate 67*d9f75844SAndroid Build Coastguard Worker captureSession:(AVCaptureSession *)captureSession { 68*d9f75844SAndroid Build Coastguard Worker if (self = [super initWithDelegate:delegate]) { 69*d9f75844SAndroid Build Coastguard Worker // Create the capture session and all relevant inputs and outputs. We need 70*d9f75844SAndroid Build Coastguard Worker // to do this in init because the application may want the capture session 71*d9f75844SAndroid Build Coastguard Worker // before we start the capturer for e.g. AVCapturePreviewLayer. All objects 72*d9f75844SAndroid Build Coastguard Worker // created here are retained until dealloc and never recreated. 73*d9f75844SAndroid Build Coastguard Worker if (![self setupCaptureSession:captureSession]) { 74*d9f75844SAndroid Build Coastguard Worker return nil; 75*d9f75844SAndroid Build Coastguard Worker } 76*d9f75844SAndroid Build Coastguard Worker NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; 77*d9f75844SAndroid Build Coastguard Worker#if TARGET_OS_IPHONE 78*d9f75844SAndroid Build Coastguard Worker _orientation = UIDeviceOrientationPortrait; 79*d9f75844SAndroid Build Coastguard Worker _rotation = RTCVideoRotation_90; 80*d9f75844SAndroid Build Coastguard Worker [center addObserver:self 81*d9f75844SAndroid Build Coastguard Worker selector:@selector(deviceOrientationDidChange:) 82*d9f75844SAndroid Build Coastguard Worker name:UIDeviceOrientationDidChangeNotification 83*d9f75844SAndroid Build Coastguard Worker object:nil]; 84*d9f75844SAndroid Build Coastguard Worker [center addObserver:self 85*d9f75844SAndroid Build Coastguard Worker selector:@selector(handleCaptureSessionInterruption:) 86*d9f75844SAndroid Build Coastguard Worker name:AVCaptureSessionWasInterruptedNotification 87*d9f75844SAndroid Build Coastguard Worker object:_captureSession]; 88*d9f75844SAndroid Build Coastguard Worker [center addObserver:self 89*d9f75844SAndroid Build Coastguard Worker selector:@selector(handleCaptureSessionInterruptionEnded:) 90*d9f75844SAndroid Build Coastguard Worker name:AVCaptureSessionInterruptionEndedNotification 91*d9f75844SAndroid Build Coastguard Worker object:_captureSession]; 92*d9f75844SAndroid Build Coastguard Worker [center addObserver:self 93*d9f75844SAndroid Build Coastguard Worker selector:@selector(handleApplicationDidBecomeActive:) 94*d9f75844SAndroid Build Coastguard Worker name:UIApplicationDidBecomeActiveNotification 95*d9f75844SAndroid Build Coastguard Worker object:[UIApplication sharedApplication]]; 96*d9f75844SAndroid Build Coastguard Worker#endif 97*d9f75844SAndroid Build Coastguard Worker [center addObserver:self 98*d9f75844SAndroid Build Coastguard Worker selector:@selector(handleCaptureSessionRuntimeError:) 99*d9f75844SAndroid Build Coastguard Worker name:AVCaptureSessionRuntimeErrorNotification 100*d9f75844SAndroid Build Coastguard Worker object:_captureSession]; 101*d9f75844SAndroid Build Coastguard Worker [center addObserver:self 102*d9f75844SAndroid Build Coastguard Worker selector:@selector(handleCaptureSessionDidStartRunning:) 103*d9f75844SAndroid Build Coastguard Worker name:AVCaptureSessionDidStartRunningNotification 104*d9f75844SAndroid Build Coastguard Worker object:_captureSession]; 105*d9f75844SAndroid Build Coastguard Worker [center addObserver:self 106*d9f75844SAndroid Build Coastguard Worker selector:@selector(handleCaptureSessionDidStopRunning:) 107*d9f75844SAndroid Build Coastguard Worker name:AVCaptureSessionDidStopRunningNotification 108*d9f75844SAndroid Build Coastguard Worker object:_captureSession]; 109*d9f75844SAndroid Build Coastguard Worker } 110*d9f75844SAndroid Build Coastguard Worker return self; 111*d9f75844SAndroid Build Coastguard Worker} 112*d9f75844SAndroid Build Coastguard Worker 113*d9f75844SAndroid Build Coastguard Worker- (void)dealloc { 114*d9f75844SAndroid Build Coastguard Worker NSAssert(!_willBeRunning, 115*d9f75844SAndroid Build Coastguard Worker @"Session was still running in RTC_OBJC_TYPE(RTCCameraVideoCapturer) dealloc. Forgot to " 116*d9f75844SAndroid Build Coastguard Worker @"call stopCapture?"); 117*d9f75844SAndroid Build Coastguard Worker [[NSNotificationCenter defaultCenter] removeObserver:self]; 118*d9f75844SAndroid Build Coastguard Worker} 119*d9f75844SAndroid Build Coastguard Worker 120*d9f75844SAndroid Build Coastguard Worker+ (NSArray<AVCaptureDevice *> *)captureDevices { 121*d9f75844SAndroid Build Coastguard Worker#if defined(WEBRTC_IOS) && defined(__IPHONE_10_0) && \ 122*d9f75844SAndroid Build Coastguard Worker __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_10_0 123*d9f75844SAndroid Build Coastguard Worker AVCaptureDeviceDiscoverySession *session = [AVCaptureDeviceDiscoverySession 124*d9f75844SAndroid Build Coastguard Worker discoverySessionWithDeviceTypes:@[ AVCaptureDeviceTypeBuiltInWideAngleCamera ] 125*d9f75844SAndroid Build Coastguard Worker mediaType:AVMediaTypeVideo 126*d9f75844SAndroid Build Coastguard Worker position:AVCaptureDevicePositionUnspecified]; 127*d9f75844SAndroid Build Coastguard Worker return session.devices; 128*d9f75844SAndroid Build Coastguard Worker#else 129*d9f75844SAndroid Build Coastguard Worker return [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo]; 130*d9f75844SAndroid Build Coastguard Worker#endif 131*d9f75844SAndroid Build Coastguard Worker} 132*d9f75844SAndroid Build Coastguard Worker 133*d9f75844SAndroid Build Coastguard Worker+ (NSArray<AVCaptureDeviceFormat *> *)supportedFormatsForDevice:(AVCaptureDevice *)device { 134*d9f75844SAndroid Build Coastguard Worker // Support opening the device in any format. We make sure it's converted to a format we 135*d9f75844SAndroid Build Coastguard Worker // can handle, if needed, in the method `-setupVideoDataOutput`. 136*d9f75844SAndroid Build Coastguard Worker return device.formats; 137*d9f75844SAndroid Build Coastguard Worker} 138*d9f75844SAndroid Build Coastguard Worker 139*d9f75844SAndroid Build Coastguard Worker- (FourCharCode)preferredOutputPixelFormat { 140*d9f75844SAndroid Build Coastguard Worker return _preferredOutputPixelFormat; 141*d9f75844SAndroid Build Coastguard Worker} 142*d9f75844SAndroid Build Coastguard Worker 143*d9f75844SAndroid Build Coastguard Worker- (void)startCaptureWithDevice:(AVCaptureDevice *)device 144*d9f75844SAndroid Build Coastguard Worker format:(AVCaptureDeviceFormat *)format 145*d9f75844SAndroid Build Coastguard Worker fps:(NSInteger)fps { 146*d9f75844SAndroid Build Coastguard Worker [self startCaptureWithDevice:device format:format fps:fps completionHandler:nil]; 147*d9f75844SAndroid Build Coastguard Worker} 148*d9f75844SAndroid Build Coastguard Worker 149*d9f75844SAndroid Build Coastguard Worker- (void)stopCapture { 150*d9f75844SAndroid Build Coastguard Worker [self stopCaptureWithCompletionHandler:nil]; 151*d9f75844SAndroid Build Coastguard Worker} 152*d9f75844SAndroid Build Coastguard Worker 153*d9f75844SAndroid Build Coastguard Worker- (void)startCaptureWithDevice:(AVCaptureDevice *)device 154*d9f75844SAndroid Build Coastguard Worker format:(AVCaptureDeviceFormat *)format 155*d9f75844SAndroid Build Coastguard Worker fps:(NSInteger)fps 156*d9f75844SAndroid Build Coastguard Worker completionHandler:(nullable void (^)(NSError *_Nullable error))completionHandler { 157*d9f75844SAndroid Build Coastguard Worker _willBeRunning = YES; 158*d9f75844SAndroid Build Coastguard Worker [RTC_OBJC_TYPE(RTCDispatcher) 159*d9f75844SAndroid Build Coastguard Worker dispatchAsyncOnType:RTCDispatcherTypeCaptureSession 160*d9f75844SAndroid Build Coastguard Worker block:^{ 161*d9f75844SAndroid Build Coastguard Worker RTCLogInfo("startCaptureWithDevice %@ @ %ld fps", format, (long)fps); 162*d9f75844SAndroid Build Coastguard Worker 163*d9f75844SAndroid Build Coastguard Worker#if TARGET_OS_IPHONE 164*d9f75844SAndroid Build Coastguard Worker dispatch_async(dispatch_get_main_queue(), ^{ 165*d9f75844SAndroid Build Coastguard Worker if (!self->_generatingOrientationNotifications) { 166*d9f75844SAndroid Build Coastguard Worker [[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications]; 167*d9f75844SAndroid Build Coastguard Worker self->_generatingOrientationNotifications = YES; 168*d9f75844SAndroid Build Coastguard Worker } 169*d9f75844SAndroid Build Coastguard Worker }); 170*d9f75844SAndroid Build Coastguard Worker#endif 171*d9f75844SAndroid Build Coastguard Worker 172*d9f75844SAndroid Build Coastguard Worker self.currentDevice = device; 173*d9f75844SAndroid Build Coastguard Worker 174*d9f75844SAndroid Build Coastguard Worker NSError *error = nil; 175*d9f75844SAndroid Build Coastguard Worker if (![self.currentDevice lockForConfiguration:&error]) { 176*d9f75844SAndroid Build Coastguard Worker RTCLogError(@"Failed to lock device %@. Error: %@", 177*d9f75844SAndroid Build Coastguard Worker self.currentDevice, 178*d9f75844SAndroid Build Coastguard Worker error.userInfo); 179*d9f75844SAndroid Build Coastguard Worker if (completionHandler) { 180*d9f75844SAndroid Build Coastguard Worker completionHandler(error); 181*d9f75844SAndroid Build Coastguard Worker } 182*d9f75844SAndroid Build Coastguard Worker self.willBeRunning = NO; 183*d9f75844SAndroid Build Coastguard Worker return; 184*d9f75844SAndroid Build Coastguard Worker } 185*d9f75844SAndroid Build Coastguard Worker [self reconfigureCaptureSessionInput]; 186*d9f75844SAndroid Build Coastguard Worker [self updateOrientation]; 187*d9f75844SAndroid Build Coastguard Worker [self updateDeviceCaptureFormat:format fps:fps]; 188*d9f75844SAndroid Build Coastguard Worker [self updateVideoDataOutputPixelFormat:format]; 189*d9f75844SAndroid Build Coastguard Worker [self.captureSession startRunning]; 190*d9f75844SAndroid Build Coastguard Worker [self.currentDevice unlockForConfiguration]; 191*d9f75844SAndroid Build Coastguard Worker self.isRunning = YES; 192*d9f75844SAndroid Build Coastguard Worker if (completionHandler) { 193*d9f75844SAndroid Build Coastguard Worker completionHandler(nil); 194*d9f75844SAndroid Build Coastguard Worker } 195*d9f75844SAndroid Build Coastguard Worker }]; 196*d9f75844SAndroid Build Coastguard Worker} 197*d9f75844SAndroid Build Coastguard Worker 198*d9f75844SAndroid Build Coastguard Worker- (void)stopCaptureWithCompletionHandler:(nullable void (^)(void))completionHandler { 199*d9f75844SAndroid Build Coastguard Worker _willBeRunning = NO; 200*d9f75844SAndroid Build Coastguard Worker [RTC_OBJC_TYPE(RTCDispatcher) 201*d9f75844SAndroid Build Coastguard Worker dispatchAsyncOnType:RTCDispatcherTypeCaptureSession 202*d9f75844SAndroid Build Coastguard Worker block:^{ 203*d9f75844SAndroid Build Coastguard Worker RTCLogInfo("Stop"); 204*d9f75844SAndroid Build Coastguard Worker self.currentDevice = nil; 205*d9f75844SAndroid Build Coastguard Worker for (AVCaptureDeviceInput *oldInput in [self.captureSession.inputs copy]) { 206*d9f75844SAndroid Build Coastguard Worker [self.captureSession removeInput:oldInput]; 207*d9f75844SAndroid Build Coastguard Worker } 208*d9f75844SAndroid Build Coastguard Worker [self.captureSession stopRunning]; 209*d9f75844SAndroid Build Coastguard Worker 210*d9f75844SAndroid Build Coastguard Worker#if TARGET_OS_IPHONE 211*d9f75844SAndroid Build Coastguard Worker dispatch_async(dispatch_get_main_queue(), ^{ 212*d9f75844SAndroid Build Coastguard Worker if (self->_generatingOrientationNotifications) { 213*d9f75844SAndroid Build Coastguard Worker [[UIDevice currentDevice] endGeneratingDeviceOrientationNotifications]; 214*d9f75844SAndroid Build Coastguard Worker self->_generatingOrientationNotifications = NO; 215*d9f75844SAndroid Build Coastguard Worker } 216*d9f75844SAndroid Build Coastguard Worker }); 217*d9f75844SAndroid Build Coastguard Worker#endif 218*d9f75844SAndroid Build Coastguard Worker self.isRunning = NO; 219*d9f75844SAndroid Build Coastguard Worker if (completionHandler) { 220*d9f75844SAndroid Build Coastguard Worker completionHandler(); 221*d9f75844SAndroid Build Coastguard Worker } 222*d9f75844SAndroid Build Coastguard Worker }]; 223*d9f75844SAndroid Build Coastguard Worker} 224*d9f75844SAndroid Build Coastguard Worker 225*d9f75844SAndroid Build Coastguard Worker#pragma mark iOS notifications 226*d9f75844SAndroid Build Coastguard Worker 227*d9f75844SAndroid Build Coastguard Worker#if TARGET_OS_IPHONE 228*d9f75844SAndroid Build Coastguard Worker- (void)deviceOrientationDidChange:(NSNotification *)notification { 229*d9f75844SAndroid Build Coastguard Worker [RTC_OBJC_TYPE(RTCDispatcher) dispatchAsyncOnType:RTCDispatcherTypeCaptureSession 230*d9f75844SAndroid Build Coastguard Worker block:^{ 231*d9f75844SAndroid Build Coastguard Worker [self updateOrientation]; 232*d9f75844SAndroid Build Coastguard Worker }]; 233*d9f75844SAndroid Build Coastguard Worker} 234*d9f75844SAndroid Build Coastguard Worker#endif 235*d9f75844SAndroid Build Coastguard Worker 236*d9f75844SAndroid Build Coastguard Worker#pragma mark AVCaptureVideoDataOutputSampleBufferDelegate 237*d9f75844SAndroid Build Coastguard Worker 238*d9f75844SAndroid Build Coastguard Worker- (void)captureOutput:(AVCaptureOutput *)captureOutput 239*d9f75844SAndroid Build Coastguard Worker didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer 240*d9f75844SAndroid Build Coastguard Worker fromConnection:(AVCaptureConnection *)connection { 241*d9f75844SAndroid Build Coastguard Worker NSParameterAssert(captureOutput == _videoDataOutput); 242*d9f75844SAndroid Build Coastguard Worker 243*d9f75844SAndroid Build Coastguard Worker if (CMSampleBufferGetNumSamples(sampleBuffer) != 1 || !CMSampleBufferIsValid(sampleBuffer) || 244*d9f75844SAndroid Build Coastguard Worker !CMSampleBufferDataIsReady(sampleBuffer)) { 245*d9f75844SAndroid Build Coastguard Worker return; 246*d9f75844SAndroid Build Coastguard Worker } 247*d9f75844SAndroid Build Coastguard Worker 248*d9f75844SAndroid Build Coastguard Worker CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer); 249*d9f75844SAndroid Build Coastguard Worker if (pixelBuffer == nil) { 250*d9f75844SAndroid Build Coastguard Worker return; 251*d9f75844SAndroid Build Coastguard Worker } 252*d9f75844SAndroid Build Coastguard Worker 253*d9f75844SAndroid Build Coastguard Worker#if TARGET_OS_IPHONE 254*d9f75844SAndroid Build Coastguard Worker // Default to portrait orientation on iPhone. 255*d9f75844SAndroid Build Coastguard Worker BOOL usingFrontCamera = NO; 256*d9f75844SAndroid Build Coastguard Worker // Check the image's EXIF for the camera the image came from as the image could have been 257*d9f75844SAndroid Build Coastguard Worker // delayed as we set alwaysDiscardsLateVideoFrames to NO. 258*d9f75844SAndroid Build Coastguard Worker AVCaptureDevicePosition cameraPosition = 259*d9f75844SAndroid Build Coastguard Worker [AVCaptureSession devicePositionForSampleBuffer:sampleBuffer]; 260*d9f75844SAndroid Build Coastguard Worker if (cameraPosition != AVCaptureDevicePositionUnspecified) { 261*d9f75844SAndroid Build Coastguard Worker usingFrontCamera = AVCaptureDevicePositionFront == cameraPosition; 262*d9f75844SAndroid Build Coastguard Worker } else { 263*d9f75844SAndroid Build Coastguard Worker AVCaptureDeviceInput *deviceInput = 264*d9f75844SAndroid Build Coastguard Worker (AVCaptureDeviceInput *)((AVCaptureInputPort *)connection.inputPorts.firstObject).input; 265*d9f75844SAndroid Build Coastguard Worker usingFrontCamera = AVCaptureDevicePositionFront == deviceInput.device.position; 266*d9f75844SAndroid Build Coastguard Worker } 267*d9f75844SAndroid Build Coastguard Worker switch (_orientation) { 268*d9f75844SAndroid Build Coastguard Worker case UIDeviceOrientationPortrait: 269*d9f75844SAndroid Build Coastguard Worker _rotation = RTCVideoRotation_90; 270*d9f75844SAndroid Build Coastguard Worker break; 271*d9f75844SAndroid Build Coastguard Worker case UIDeviceOrientationPortraitUpsideDown: 272*d9f75844SAndroid Build Coastguard Worker _rotation = RTCVideoRotation_270; 273*d9f75844SAndroid Build Coastguard Worker break; 274*d9f75844SAndroid Build Coastguard Worker case UIDeviceOrientationLandscapeLeft: 275*d9f75844SAndroid Build Coastguard Worker _rotation = usingFrontCamera ? RTCVideoRotation_180 : RTCVideoRotation_0; 276*d9f75844SAndroid Build Coastguard Worker break; 277*d9f75844SAndroid Build Coastguard Worker case UIDeviceOrientationLandscapeRight: 278*d9f75844SAndroid Build Coastguard Worker _rotation = usingFrontCamera ? RTCVideoRotation_0 : RTCVideoRotation_180; 279*d9f75844SAndroid Build Coastguard Worker break; 280*d9f75844SAndroid Build Coastguard Worker case UIDeviceOrientationFaceUp: 281*d9f75844SAndroid Build Coastguard Worker case UIDeviceOrientationFaceDown: 282*d9f75844SAndroid Build Coastguard Worker case UIDeviceOrientationUnknown: 283*d9f75844SAndroid Build Coastguard Worker // Ignore. 284*d9f75844SAndroid Build Coastguard Worker break; 285*d9f75844SAndroid Build Coastguard Worker } 286*d9f75844SAndroid Build Coastguard Worker#else 287*d9f75844SAndroid Build Coastguard Worker // No rotation on Mac. 288*d9f75844SAndroid Build Coastguard Worker _rotation = RTCVideoRotation_0; 289*d9f75844SAndroid Build Coastguard Worker#endif 290*d9f75844SAndroid Build Coastguard Worker 291*d9f75844SAndroid Build Coastguard Worker RTC_OBJC_TYPE(RTCCVPixelBuffer) *rtcPixelBuffer = 292*d9f75844SAndroid Build Coastguard Worker [[RTC_OBJC_TYPE(RTCCVPixelBuffer) alloc] initWithPixelBuffer:pixelBuffer]; 293*d9f75844SAndroid Build Coastguard Worker int64_t timeStampNs = CMTimeGetSeconds(CMSampleBufferGetPresentationTimeStamp(sampleBuffer)) * 294*d9f75844SAndroid Build Coastguard Worker kNanosecondsPerSecond; 295*d9f75844SAndroid Build Coastguard Worker RTC_OBJC_TYPE(RTCVideoFrame) *videoFrame = 296*d9f75844SAndroid Build Coastguard Worker [[RTC_OBJC_TYPE(RTCVideoFrame) alloc] initWithBuffer:rtcPixelBuffer 297*d9f75844SAndroid Build Coastguard Worker rotation:_rotation 298*d9f75844SAndroid Build Coastguard Worker timeStampNs:timeStampNs]; 299*d9f75844SAndroid Build Coastguard Worker [self.delegate capturer:self didCaptureVideoFrame:videoFrame]; 300*d9f75844SAndroid Build Coastguard Worker} 301*d9f75844SAndroid Build Coastguard Worker 302*d9f75844SAndroid Build Coastguard Worker- (void)captureOutput:(AVCaptureOutput *)captureOutput 303*d9f75844SAndroid Build Coastguard Worker didDropSampleBuffer:(CMSampleBufferRef)sampleBuffer 304*d9f75844SAndroid Build Coastguard Worker fromConnection:(AVCaptureConnection *)connection { 305*d9f75844SAndroid Build Coastguard Worker#if TARGET_OS_IPHONE 306*d9f75844SAndroid Build Coastguard Worker CFStringRef droppedReason = 307*d9f75844SAndroid Build Coastguard Worker CMGetAttachment(sampleBuffer, kCMSampleBufferAttachmentKey_DroppedFrameReason, nil); 308*d9f75844SAndroid Build Coastguard Worker#else 309*d9f75844SAndroid Build Coastguard Worker // DroppedFrameReason unavailable on macOS. 310*d9f75844SAndroid Build Coastguard Worker CFStringRef droppedReason = nil; 311*d9f75844SAndroid Build Coastguard Worker#endif 312*d9f75844SAndroid Build Coastguard Worker RTCLogError(@"Dropped sample buffer. Reason: %@", (__bridge NSString *)droppedReason); 313*d9f75844SAndroid Build Coastguard Worker} 314*d9f75844SAndroid Build Coastguard Worker 315*d9f75844SAndroid Build Coastguard Worker#pragma mark - AVCaptureSession notifications 316*d9f75844SAndroid Build Coastguard Worker 317*d9f75844SAndroid Build Coastguard Worker- (void)handleCaptureSessionInterruption:(NSNotification *)notification { 318*d9f75844SAndroid Build Coastguard Worker NSString *reasonString = nil; 319*d9f75844SAndroid Build Coastguard Worker#if TARGET_OS_IPHONE 320*d9f75844SAndroid Build Coastguard Worker NSNumber *reason = notification.userInfo[AVCaptureSessionInterruptionReasonKey]; 321*d9f75844SAndroid Build Coastguard Worker if (reason) { 322*d9f75844SAndroid Build Coastguard Worker switch (reason.intValue) { 323*d9f75844SAndroid Build Coastguard Worker case AVCaptureSessionInterruptionReasonVideoDeviceNotAvailableInBackground: 324*d9f75844SAndroid Build Coastguard Worker reasonString = @"VideoDeviceNotAvailableInBackground"; 325*d9f75844SAndroid Build Coastguard Worker break; 326*d9f75844SAndroid Build Coastguard Worker case AVCaptureSessionInterruptionReasonAudioDeviceInUseByAnotherClient: 327*d9f75844SAndroid Build Coastguard Worker reasonString = @"AudioDeviceInUseByAnotherClient"; 328*d9f75844SAndroid Build Coastguard Worker break; 329*d9f75844SAndroid Build Coastguard Worker case AVCaptureSessionInterruptionReasonVideoDeviceInUseByAnotherClient: 330*d9f75844SAndroid Build Coastguard Worker reasonString = @"VideoDeviceInUseByAnotherClient"; 331*d9f75844SAndroid Build Coastguard Worker break; 332*d9f75844SAndroid Build Coastguard Worker case AVCaptureSessionInterruptionReasonVideoDeviceNotAvailableWithMultipleForegroundApps: 333*d9f75844SAndroid Build Coastguard Worker reasonString = @"VideoDeviceNotAvailableWithMultipleForegroundApps"; 334*d9f75844SAndroid Build Coastguard Worker break; 335*d9f75844SAndroid Build Coastguard Worker } 336*d9f75844SAndroid Build Coastguard Worker } 337*d9f75844SAndroid Build Coastguard Worker#endif 338*d9f75844SAndroid Build Coastguard Worker RTCLog(@"Capture session interrupted: %@", reasonString); 339*d9f75844SAndroid Build Coastguard Worker} 340*d9f75844SAndroid Build Coastguard Worker 341*d9f75844SAndroid Build Coastguard Worker- (void)handleCaptureSessionInterruptionEnded:(NSNotification *)notification { 342*d9f75844SAndroid Build Coastguard Worker RTCLog(@"Capture session interruption ended."); 343*d9f75844SAndroid Build Coastguard Worker} 344*d9f75844SAndroid Build Coastguard Worker 345*d9f75844SAndroid Build Coastguard Worker- (void)handleCaptureSessionRuntimeError:(NSNotification *)notification { 346*d9f75844SAndroid Build Coastguard Worker NSError *error = [notification.userInfo objectForKey:AVCaptureSessionErrorKey]; 347*d9f75844SAndroid Build Coastguard Worker RTCLogError(@"Capture session runtime error: %@", error); 348*d9f75844SAndroid Build Coastguard Worker 349*d9f75844SAndroid Build Coastguard Worker [RTC_OBJC_TYPE(RTCDispatcher) dispatchAsyncOnType:RTCDispatcherTypeCaptureSession 350*d9f75844SAndroid Build Coastguard Worker block:^{ 351*d9f75844SAndroid Build Coastguard Worker#if TARGET_OS_IPHONE 352*d9f75844SAndroid Build Coastguard Worker if (error.code == AVErrorMediaServicesWereReset) { 353*d9f75844SAndroid Build Coastguard Worker [self handleNonFatalError]; 354*d9f75844SAndroid Build Coastguard Worker } else { 355*d9f75844SAndroid Build Coastguard Worker [self handleFatalError]; 356*d9f75844SAndroid Build Coastguard Worker } 357*d9f75844SAndroid Build Coastguard Worker#else 358*d9f75844SAndroid Build Coastguard Worker [self handleFatalError]; 359*d9f75844SAndroid Build Coastguard Worker#endif 360*d9f75844SAndroid Build Coastguard Worker }]; 361*d9f75844SAndroid Build Coastguard Worker} 362*d9f75844SAndroid Build Coastguard Worker 363*d9f75844SAndroid Build Coastguard Worker- (void)handleCaptureSessionDidStartRunning:(NSNotification *)notification { 364*d9f75844SAndroid Build Coastguard Worker RTCLog(@"Capture session started."); 365*d9f75844SAndroid Build Coastguard Worker 366*d9f75844SAndroid Build Coastguard Worker [RTC_OBJC_TYPE(RTCDispatcher) dispatchAsyncOnType:RTCDispatcherTypeCaptureSession 367*d9f75844SAndroid Build Coastguard Worker block:^{ 368*d9f75844SAndroid Build Coastguard Worker // If we successfully restarted after an unknown 369*d9f75844SAndroid Build Coastguard Worker // error, allow future retries on fatal errors. 370*d9f75844SAndroid Build Coastguard Worker self.hasRetriedOnFatalError = NO; 371*d9f75844SAndroid Build Coastguard Worker }]; 372*d9f75844SAndroid Build Coastguard Worker} 373*d9f75844SAndroid Build Coastguard Worker 374*d9f75844SAndroid Build Coastguard Worker- (void)handleCaptureSessionDidStopRunning:(NSNotification *)notification { 375*d9f75844SAndroid Build Coastguard Worker RTCLog(@"Capture session stopped."); 376*d9f75844SAndroid Build Coastguard Worker} 377*d9f75844SAndroid Build Coastguard Worker 378*d9f75844SAndroid Build Coastguard Worker- (void)handleFatalError { 379*d9f75844SAndroid Build Coastguard Worker [RTC_OBJC_TYPE(RTCDispatcher) 380*d9f75844SAndroid Build Coastguard Worker dispatchAsyncOnType:RTCDispatcherTypeCaptureSession 381*d9f75844SAndroid Build Coastguard Worker block:^{ 382*d9f75844SAndroid Build Coastguard Worker if (!self.hasRetriedOnFatalError) { 383*d9f75844SAndroid Build Coastguard Worker RTCLogWarning(@"Attempting to recover from fatal capture error."); 384*d9f75844SAndroid Build Coastguard Worker [self handleNonFatalError]; 385*d9f75844SAndroid Build Coastguard Worker self.hasRetriedOnFatalError = YES; 386*d9f75844SAndroid Build Coastguard Worker } else { 387*d9f75844SAndroid Build Coastguard Worker RTCLogError(@"Previous fatal error recovery failed."); 388*d9f75844SAndroid Build Coastguard Worker } 389*d9f75844SAndroid Build Coastguard Worker }]; 390*d9f75844SAndroid Build Coastguard Worker} 391*d9f75844SAndroid Build Coastguard Worker 392*d9f75844SAndroid Build Coastguard Worker- (void)handleNonFatalError { 393*d9f75844SAndroid Build Coastguard Worker [RTC_OBJC_TYPE(RTCDispatcher) dispatchAsyncOnType:RTCDispatcherTypeCaptureSession 394*d9f75844SAndroid Build Coastguard Worker block:^{ 395*d9f75844SAndroid Build Coastguard Worker RTCLog(@"Restarting capture session after error."); 396*d9f75844SAndroid Build Coastguard Worker if (self.isRunning) { 397*d9f75844SAndroid Build Coastguard Worker [self.captureSession startRunning]; 398*d9f75844SAndroid Build Coastguard Worker } 399*d9f75844SAndroid Build Coastguard Worker }]; 400*d9f75844SAndroid Build Coastguard Worker} 401*d9f75844SAndroid Build Coastguard Worker 402*d9f75844SAndroid Build Coastguard Worker#if TARGET_OS_IPHONE 403*d9f75844SAndroid Build Coastguard Worker 404*d9f75844SAndroid Build Coastguard Worker#pragma mark - UIApplication notifications 405*d9f75844SAndroid Build Coastguard Worker 406*d9f75844SAndroid Build Coastguard Worker- (void)handleApplicationDidBecomeActive:(NSNotification *)notification { 407*d9f75844SAndroid Build Coastguard Worker [RTC_OBJC_TYPE(RTCDispatcher) 408*d9f75844SAndroid Build Coastguard Worker dispatchAsyncOnType:RTCDispatcherTypeCaptureSession 409*d9f75844SAndroid Build Coastguard Worker block:^{ 410*d9f75844SAndroid Build Coastguard Worker if (self.isRunning && !self.captureSession.isRunning) { 411*d9f75844SAndroid Build Coastguard Worker RTCLog(@"Restarting capture session on active."); 412*d9f75844SAndroid Build Coastguard Worker [self.captureSession startRunning]; 413*d9f75844SAndroid Build Coastguard Worker } 414*d9f75844SAndroid Build Coastguard Worker }]; 415*d9f75844SAndroid Build Coastguard Worker} 416*d9f75844SAndroid Build Coastguard Worker 417*d9f75844SAndroid Build Coastguard Worker#endif // TARGET_OS_IPHONE 418*d9f75844SAndroid Build Coastguard Worker 419*d9f75844SAndroid Build Coastguard Worker#pragma mark - Private 420*d9f75844SAndroid Build Coastguard Worker 421*d9f75844SAndroid Build Coastguard Worker- (dispatch_queue_t)frameQueue { 422*d9f75844SAndroid Build Coastguard Worker if (!_frameQueue) { 423*d9f75844SAndroid Build Coastguard Worker _frameQueue = RTCDispatchQueueCreateWithTarget( 424*d9f75844SAndroid Build Coastguard Worker "org.webrtc.cameravideocapturer.video", 425*d9f75844SAndroid Build Coastguard Worker DISPATCH_QUEUE_SERIAL, 426*d9f75844SAndroid Build Coastguard Worker dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)); 427*d9f75844SAndroid Build Coastguard Worker } 428*d9f75844SAndroid Build Coastguard Worker return _frameQueue; 429*d9f75844SAndroid Build Coastguard Worker} 430*d9f75844SAndroid Build Coastguard Worker 431*d9f75844SAndroid Build Coastguard Worker- (BOOL)setupCaptureSession:(AVCaptureSession *)captureSession { 432*d9f75844SAndroid Build Coastguard Worker NSAssert(_captureSession == nil, @"Setup capture session called twice."); 433*d9f75844SAndroid Build Coastguard Worker _captureSession = captureSession; 434*d9f75844SAndroid Build Coastguard Worker#if defined(WEBRTC_IOS) 435*d9f75844SAndroid Build Coastguard Worker _captureSession.sessionPreset = AVCaptureSessionPresetInputPriority; 436*d9f75844SAndroid Build Coastguard Worker _captureSession.usesApplicationAudioSession = NO; 437*d9f75844SAndroid Build Coastguard Worker#endif 438*d9f75844SAndroid Build Coastguard Worker [self setupVideoDataOutput]; 439*d9f75844SAndroid Build Coastguard Worker // Add the output. 440*d9f75844SAndroid Build Coastguard Worker if (![_captureSession canAddOutput:_videoDataOutput]) { 441*d9f75844SAndroid Build Coastguard Worker RTCLogError(@"Video data output unsupported."); 442*d9f75844SAndroid Build Coastguard Worker return NO; 443*d9f75844SAndroid Build Coastguard Worker } 444*d9f75844SAndroid Build Coastguard Worker [_captureSession addOutput:_videoDataOutput]; 445*d9f75844SAndroid Build Coastguard Worker 446*d9f75844SAndroid Build Coastguard Worker return YES; 447*d9f75844SAndroid Build Coastguard Worker} 448*d9f75844SAndroid Build Coastguard Worker 449*d9f75844SAndroid Build Coastguard Worker- (void)setupVideoDataOutput { 450*d9f75844SAndroid Build Coastguard Worker NSAssert(_videoDataOutput == nil, @"Setup video data output called twice."); 451*d9f75844SAndroid Build Coastguard Worker AVCaptureVideoDataOutput *videoDataOutput = [[AVCaptureVideoDataOutput alloc] init]; 452*d9f75844SAndroid Build Coastguard Worker 453*d9f75844SAndroid Build Coastguard Worker // `videoDataOutput.availableVideoCVPixelFormatTypes` returns the pixel formats supported by the 454*d9f75844SAndroid Build Coastguard Worker // device with the most efficient output format first. Find the first format that we support. 455*d9f75844SAndroid Build Coastguard Worker NSSet<NSNumber *> *supportedPixelFormats = 456*d9f75844SAndroid Build Coastguard Worker [RTC_OBJC_TYPE(RTCCVPixelBuffer) supportedPixelFormats]; 457*d9f75844SAndroid Build Coastguard Worker NSMutableOrderedSet *availablePixelFormats = 458*d9f75844SAndroid Build Coastguard Worker [NSMutableOrderedSet orderedSetWithArray:videoDataOutput.availableVideoCVPixelFormatTypes]; 459*d9f75844SAndroid Build Coastguard Worker [availablePixelFormats intersectSet:supportedPixelFormats]; 460*d9f75844SAndroid Build Coastguard Worker NSNumber *pixelFormat = availablePixelFormats.firstObject; 461*d9f75844SAndroid Build Coastguard Worker NSAssert(pixelFormat, @"Output device has no supported formats."); 462*d9f75844SAndroid Build Coastguard Worker 463*d9f75844SAndroid Build Coastguard Worker _preferredOutputPixelFormat = [pixelFormat unsignedIntValue]; 464*d9f75844SAndroid Build Coastguard Worker _outputPixelFormat = _preferredOutputPixelFormat; 465*d9f75844SAndroid Build Coastguard Worker videoDataOutput.videoSettings = @{(NSString *)kCVPixelBufferPixelFormatTypeKey : pixelFormat}; 466*d9f75844SAndroid Build Coastguard Worker videoDataOutput.alwaysDiscardsLateVideoFrames = NO; 467*d9f75844SAndroid Build Coastguard Worker [videoDataOutput setSampleBufferDelegate:self queue:self.frameQueue]; 468*d9f75844SAndroid Build Coastguard Worker _videoDataOutput = videoDataOutput; 469*d9f75844SAndroid Build Coastguard Worker} 470*d9f75844SAndroid Build Coastguard Worker 471*d9f75844SAndroid Build Coastguard Worker- (void)updateVideoDataOutputPixelFormat:(AVCaptureDeviceFormat *)format { 472*d9f75844SAndroid Build Coastguard Worker FourCharCode mediaSubType = CMFormatDescriptionGetMediaSubType(format.formatDescription); 473*d9f75844SAndroid Build Coastguard Worker if (![[RTC_OBJC_TYPE(RTCCVPixelBuffer) supportedPixelFormats] containsObject:@(mediaSubType)]) { 474*d9f75844SAndroid Build Coastguard Worker mediaSubType = _preferredOutputPixelFormat; 475*d9f75844SAndroid Build Coastguard Worker } 476*d9f75844SAndroid Build Coastguard Worker 477*d9f75844SAndroid Build Coastguard Worker if (mediaSubType != _outputPixelFormat) { 478*d9f75844SAndroid Build Coastguard Worker _outputPixelFormat = mediaSubType; 479*d9f75844SAndroid Build Coastguard Worker _videoDataOutput.videoSettings = 480*d9f75844SAndroid Build Coastguard Worker @{ (NSString *)kCVPixelBufferPixelFormatTypeKey : @(mediaSubType) }; 481*d9f75844SAndroid Build Coastguard Worker } 482*d9f75844SAndroid Build Coastguard Worker} 483*d9f75844SAndroid Build Coastguard Worker 484*d9f75844SAndroid Build Coastguard Worker#pragma mark - Private, called inside capture queue 485*d9f75844SAndroid Build Coastguard Worker 486*d9f75844SAndroid Build Coastguard Worker- (void)updateDeviceCaptureFormat:(AVCaptureDeviceFormat *)format fps:(NSInteger)fps { 487*d9f75844SAndroid Build Coastguard Worker NSAssert([RTC_OBJC_TYPE(RTCDispatcher) isOnQueueForType:RTCDispatcherTypeCaptureSession], 488*d9f75844SAndroid Build Coastguard Worker @"updateDeviceCaptureFormat must be called on the capture queue."); 489*d9f75844SAndroid Build Coastguard Worker @try { 490*d9f75844SAndroid Build Coastguard Worker _currentDevice.activeFormat = format; 491*d9f75844SAndroid Build Coastguard Worker _currentDevice.activeVideoMinFrameDuration = CMTimeMake(1, fps); 492*d9f75844SAndroid Build Coastguard Worker } @catch (NSException *exception) { 493*d9f75844SAndroid Build Coastguard Worker RTCLogError(@"Failed to set active format!\n User info:%@", exception.userInfo); 494*d9f75844SAndroid Build Coastguard Worker return; 495*d9f75844SAndroid Build Coastguard Worker } 496*d9f75844SAndroid Build Coastguard Worker} 497*d9f75844SAndroid Build Coastguard Worker 498*d9f75844SAndroid Build Coastguard Worker- (void)reconfigureCaptureSessionInput { 499*d9f75844SAndroid Build Coastguard Worker NSAssert([RTC_OBJC_TYPE(RTCDispatcher) isOnQueueForType:RTCDispatcherTypeCaptureSession], 500*d9f75844SAndroid Build Coastguard Worker @"reconfigureCaptureSessionInput must be called on the capture queue."); 501*d9f75844SAndroid Build Coastguard Worker NSError *error = nil; 502*d9f75844SAndroid Build Coastguard Worker AVCaptureDeviceInput *input = 503*d9f75844SAndroid Build Coastguard Worker [AVCaptureDeviceInput deviceInputWithDevice:_currentDevice error:&error]; 504*d9f75844SAndroid Build Coastguard Worker if (!input) { 505*d9f75844SAndroid Build Coastguard Worker RTCLogError(@"Failed to create front camera input: %@", error.localizedDescription); 506*d9f75844SAndroid Build Coastguard Worker return; 507*d9f75844SAndroid Build Coastguard Worker } 508*d9f75844SAndroid Build Coastguard Worker [_captureSession beginConfiguration]; 509*d9f75844SAndroid Build Coastguard Worker for (AVCaptureDeviceInput *oldInput in [_captureSession.inputs copy]) { 510*d9f75844SAndroid Build Coastguard Worker [_captureSession removeInput:oldInput]; 511*d9f75844SAndroid Build Coastguard Worker } 512*d9f75844SAndroid Build Coastguard Worker if ([_captureSession canAddInput:input]) { 513*d9f75844SAndroid Build Coastguard Worker [_captureSession addInput:input]; 514*d9f75844SAndroid Build Coastguard Worker } else { 515*d9f75844SAndroid Build Coastguard Worker RTCLogError(@"Cannot add camera as an input to the session."); 516*d9f75844SAndroid Build Coastguard Worker } 517*d9f75844SAndroid Build Coastguard Worker [_captureSession commitConfiguration]; 518*d9f75844SAndroid Build Coastguard Worker} 519*d9f75844SAndroid Build Coastguard Worker 520*d9f75844SAndroid Build Coastguard Worker- (void)updateOrientation { 521*d9f75844SAndroid Build Coastguard Worker NSAssert([RTC_OBJC_TYPE(RTCDispatcher) isOnQueueForType:RTCDispatcherTypeCaptureSession], 522*d9f75844SAndroid Build Coastguard Worker @"updateOrientation must be called on the capture queue."); 523*d9f75844SAndroid Build Coastguard Worker#if TARGET_OS_IPHONE 524*d9f75844SAndroid Build Coastguard Worker _orientation = [UIDevice currentDevice].orientation; 525*d9f75844SAndroid Build Coastguard Worker#endif 526*d9f75844SAndroid Build Coastguard Worker} 527*d9f75844SAndroid Build Coastguard Worker 528*d9f75844SAndroid Build Coastguard Worker@end 529