| OLD | NEW |
| (Empty) |
| 1 /* | |
| 2 * Copyright 2017 The WebRTC project authors. All Rights Reserved. | |
| 3 * | |
| 4 * Use of this source code is governed by a BSD-style license | |
| 5 * that can be found in the LICENSE file in the root of the source | |
| 6 * tree. An additional intellectual property rights grant can be found | |
| 7 * in the file PATENTS. All contributing project authors may | |
| 8 * be found in the AUTHORS file in the root of the source tree. | |
| 9 */ | |
| 10 | |
| 11 #import <Foundation/Foundation.h> | |
| 12 | |
| 13 #import "WebRTC/RTCCameraVideoCapturer.h" | |
| 14 #import "WebRTC/RTCLogging.h" | |
| 15 #import "WebRTC/RTCVideoFrameBuffer.h" | |
| 16 | |
| 17 #if TARGET_OS_IPHONE | |
| 18 #import "WebRTC/UIDevice+RTCDevice.h" | |
| 19 #endif | |
| 20 | |
| 21 #import "AVCaptureSession+Device.h" | |
| 22 #import "RTCDispatcher+Private.h" | |
| 23 | |
| 24 const int64_t kNanosecondsPerSecond = 1000000000; | |
| 25 | |
| 26 static inline BOOL IsMediaSubTypeSupported(FourCharCode mediaSubType) { | |
| 27 return (mediaSubType == kCVPixelFormatType_420YpCbCr8PlanarFullRange || | |
| 28 mediaSubType == kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange); | |
| 29 } | |
| 30 | |
| 31 @interface RTCCameraVideoCapturer ()<AVCaptureVideoDataOutputSampleBufferDelegat
e> | |
| 32 @property(nonatomic, readonly) dispatch_queue_t frameQueue; | |
| 33 @end | |
| 34 | |
| 35 @implementation RTCCameraVideoCapturer { | |
| 36 AVCaptureVideoDataOutput *_videoDataOutput; | |
| 37 AVCaptureSession *_captureSession; | |
| 38 AVCaptureDevice *_currentDevice; | |
| 39 BOOL _hasRetriedOnFatalError; | |
| 40 BOOL _isRunning; | |
| 41 // Will the session be running once all asynchronous operations have been comp
leted? | |
| 42 BOOL _willBeRunning; | |
| 43 #if TARGET_OS_IPHONE | |
| 44 UIDeviceOrientation _orientation; | |
| 45 #endif | |
| 46 } | |
| 47 | |
| 48 @synthesize frameQueue = _frameQueue; | |
| 49 @synthesize captureSession = _captureSession; | |
| 50 | |
| 51 - (instancetype)initWithDelegate:(__weak id<RTCVideoCapturerDelegate>)delegate { | |
| 52 if (self = [super initWithDelegate:delegate]) { | |
| 53 // Create the capture session and all relevant inputs and outputs. We need | |
| 54 // to do this in init because the application may want the capture session | |
| 55 // before we start the capturer for e.g. AVCapturePreviewLayer. All objects | |
| 56 // created here are retained until dealloc and never recreated. | |
| 57 if (![self setupCaptureSession]) { | |
| 58 return nil; | |
| 59 } | |
| 60 NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; | |
| 61 #if TARGET_OS_IPHONE | |
| 62 _orientation = UIDeviceOrientationPortrait; | |
| 63 [center addObserver:self | |
| 64 selector:@selector(deviceOrientationDidChange:) | |
| 65 name:UIDeviceOrientationDidChangeNotification | |
| 66 object:nil]; | |
| 67 [center addObserver:self | |
| 68 selector:@selector(handleCaptureSessionInterruption:) | |
| 69 name:AVCaptureSessionWasInterruptedNotification | |
| 70 object:_captureSession]; | |
| 71 [center addObserver:self | |
| 72 selector:@selector(handleCaptureSessionInterruptionEnded:) | |
| 73 name:AVCaptureSessionInterruptionEndedNotification | |
| 74 object:_captureSession]; | |
| 75 [center addObserver:self | |
| 76 selector:@selector(handleApplicationDidBecomeActive:) | |
| 77 name:UIApplicationDidBecomeActiveNotification | |
| 78 object:[UIApplication sharedApplication]]; | |
| 79 #endif | |
| 80 [center addObserver:self | |
| 81 selector:@selector(handleCaptureSessionRuntimeError:) | |
| 82 name:AVCaptureSessionRuntimeErrorNotification | |
| 83 object:_captureSession]; | |
| 84 [center addObserver:self | |
| 85 selector:@selector(handleCaptureSessionDidStartRunning:) | |
| 86 name:AVCaptureSessionDidStartRunningNotification | |
| 87 object:_captureSession]; | |
| 88 [center addObserver:self | |
| 89 selector:@selector(handleCaptureSessionDidStopRunning:) | |
| 90 name:AVCaptureSessionDidStopRunningNotification | |
| 91 object:_captureSession]; | |
| 92 } | |
| 93 return self; | |
| 94 } | |
| 95 | |
| 96 - (void)dealloc { | |
| 97 NSAssert( | |
| 98 !_willBeRunning, | |
| 99 @"Session was still running in RTCCameraVideoCapturer dealloc. Forgot to c
all stopCapture?"); | |
| 100 [[NSNotificationCenter defaultCenter] removeObserver:self]; | |
| 101 } | |
| 102 | |
| 103 + (NSArray<AVCaptureDevice *> *)captureDevices { | |
| 104 return [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo]; | |
| 105 } | |
| 106 | |
| 107 + (NSArray<AVCaptureDeviceFormat *> *)supportedFormatsForDevice:(AVCaptureDevice
*)device { | |
| 108 NSMutableArray<AVCaptureDeviceFormat *> *eligibleDeviceFormats = [NSMutableArr
ay array]; | |
| 109 | |
| 110 for (AVCaptureDeviceFormat *format in device.formats) { | |
| 111 // Filter out subTypes that we currently don't support in the stack | |
| 112 FourCharCode mediaSubType = CMFormatDescriptionGetMediaSubType(format.format
Description); | |
| 113 if (IsMediaSubTypeSupported(mediaSubType)) { | |
| 114 [eligibleDeviceFormats addObject:format]; | |
| 115 } | |
| 116 } | |
| 117 | |
| 118 return eligibleDeviceFormats; | |
| 119 } | |
| 120 | |
| 121 - (void)startCaptureWithDevice:(AVCaptureDevice *)device | |
| 122 format:(AVCaptureDeviceFormat *)format | |
| 123 fps:(NSInteger)fps { | |
| 124 _willBeRunning = YES; | |
| 125 [RTCDispatcher | |
| 126 dispatchAsyncOnType:RTCDispatcherTypeCaptureSession | |
| 127 block:^{ | |
| 128 RTCLogInfo("startCaptureWithDevice %@ @ %ld fps", format,
(long)fps); | |
| 129 | |
| 130 #if TARGET_OS_IPHONE | |
| 131 [[UIDevice currentDevice] beginGeneratingDeviceOrientation
Notifications]; | |
| 132 #endif | |
| 133 | |
| 134 _currentDevice = device; | |
| 135 | |
| 136 NSError *error = nil; | |
| 137 if (![_currentDevice lockForConfiguration:&error]) { | |
| 138 RTCLogError( | |
| 139 @"Failed to lock device %@. Error: %@", _currentDevi
ce, error.userInfo); | |
| 140 return; | |
| 141 } | |
| 142 [self reconfigureCaptureSessionInput]; | |
| 143 [self updateOrientation]; | |
| 144 [_captureSession startRunning]; | |
| 145 [self updateDeviceCaptureFormat:format fps:fps]; | |
| 146 [_currentDevice unlockForConfiguration]; | |
| 147 _isRunning = YES; | |
| 148 }]; | |
| 149 } | |
| 150 | |
| 151 - (void)stopCapture { | |
| 152 _willBeRunning = NO; | |
| 153 [RTCDispatcher | |
| 154 dispatchAsyncOnType:RTCDispatcherTypeCaptureSession | |
| 155 block:^{ | |
| 156 RTCLogInfo("Stop"); | |
| 157 _currentDevice = nil; | |
| 158 for (AVCaptureDeviceInput *oldInput in [_captureSession.in
puts copy]) { | |
| 159 [_captureSession removeInput:oldInput]; | |
| 160 } | |
| 161 [_captureSession stopRunning]; | |
| 162 | |
| 163 #if TARGET_OS_IPHONE | |
| 164 [[UIDevice currentDevice] endGeneratingDeviceOrientationNo
tifications]; | |
| 165 #endif | |
| 166 _isRunning = NO; | |
| 167 }]; | |
| 168 } | |
| 169 | |
| 170 #pragma mark iOS notifications | |
| 171 | |
| 172 #if TARGET_OS_IPHONE | |
| 173 - (void)deviceOrientationDidChange:(NSNotification *)notification { | |
| 174 [RTCDispatcher dispatchAsyncOnType:RTCDispatcherTypeCaptureSession | |
| 175 block:^{ | |
| 176 [self updateOrientation]; | |
| 177 }]; | |
| 178 } | |
| 179 #endif | |
| 180 | |
| 181 #pragma mark AVCaptureVideoDataOutputSampleBufferDelegate | |
| 182 | |
| 183 - (void)captureOutput:(AVCaptureOutput *)captureOutput | |
| 184 didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer | |
| 185 fromConnection:(AVCaptureConnection *)connection { | |
| 186 NSParameterAssert(captureOutput == _videoDataOutput); | |
| 187 | |
| 188 if (CMSampleBufferGetNumSamples(sampleBuffer) != 1 || !CMSampleBufferIsValid(s
ampleBuffer) || | |
| 189 !CMSampleBufferDataIsReady(sampleBuffer)) { | |
| 190 return; | |
| 191 } | |
| 192 | |
| 193 CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer); | |
| 194 if (pixelBuffer == nil) { | |
| 195 return; | |
| 196 } | |
| 197 | |
| 198 #if TARGET_OS_IPHONE | |
| 199 // Default to portrait orientation on iPhone. | |
| 200 RTCVideoRotation rotation = RTCVideoRotation_90; | |
| 201 BOOL usingFrontCamera; | |
| 202 // Check the image's EXIF for the camera the image came from as the image coul
d have been | |
| 203 // delayed as we set alwaysDiscardsLateVideoFrames to NO. | |
| 204 AVCaptureDevicePosition cameraPosition = | |
| 205 [AVCaptureSession devicePositionForSampleBuffer:sampleBuffer]; | |
| 206 if (cameraPosition != AVCaptureDevicePositionUnspecified) { | |
| 207 usingFrontCamera = AVCaptureDevicePositionFront == cameraPosition; | |
| 208 } else { | |
| 209 AVCaptureDeviceInput *deviceInput = | |
| 210 (AVCaptureDeviceInput *)((AVCaptureInputPort *)connection.inputPorts.fir
stObject).input; | |
| 211 usingFrontCamera = AVCaptureDevicePositionFront == deviceInput.device.positi
on; | |
| 212 } | |
| 213 | |
| 214 switch (_orientation) { | |
| 215 case UIDeviceOrientationPortrait: | |
| 216 rotation = RTCVideoRotation_90; | |
| 217 break; | |
| 218 case UIDeviceOrientationPortraitUpsideDown: | |
| 219 rotation = RTCVideoRotation_270; | |
| 220 break; | |
| 221 case UIDeviceOrientationLandscapeLeft: | |
| 222 rotation = usingFrontCamera ? RTCVideoRotation_180 : RTCVideoRotation_0; | |
| 223 break; | |
| 224 case UIDeviceOrientationLandscapeRight: | |
| 225 rotation = usingFrontCamera ? RTCVideoRotation_0 : RTCVideoRotation_180; | |
| 226 break; | |
| 227 case UIDeviceOrientationFaceUp: | |
| 228 case UIDeviceOrientationFaceDown: | |
| 229 case UIDeviceOrientationUnknown: | |
| 230 // Ignore. | |
| 231 break; | |
| 232 } | |
| 233 #else | |
| 234 // No rotation on Mac. | |
| 235 RTCVideoRotation rotation = RTCVideoRotation_0; | |
| 236 #endif | |
| 237 | |
| 238 RTCCVPixelBuffer *rtcPixelBuffer = [[RTCCVPixelBuffer alloc] initWithPixelBuff
er:pixelBuffer]; | |
| 239 int64_t timeStampNs = CMTimeGetSeconds(CMSampleBufferGetPresentationTimeStamp(
sampleBuffer)) * | |
| 240 kNanosecondsPerSecond; | |
| 241 RTCVideoFrame *videoFrame = [[RTCVideoFrame alloc] initWithBuffer:rtcPixelBuff
er | |
| 242 rotation:rotation | |
| 243 timeStampNs:timeStampNs]
; | |
| 244 [self.delegate capturer:self didCaptureVideoFrame:videoFrame]; | |
| 245 } | |
| 246 | |
| 247 - (void)captureOutput:(AVCaptureOutput *)captureOutput | |
| 248 didDropSampleBuffer:(CMSampleBufferRef)sampleBuffer | |
| 249 fromConnection:(AVCaptureConnection *)connection { | |
| 250 RTCLogError(@"Dropped sample buffer."); | |
| 251 } | |
| 252 | |
| 253 #pragma mark - AVCaptureSession notifications | |
| 254 | |
| 255 - (void)handleCaptureSessionInterruption:(NSNotification *)notification { | |
| 256 NSString *reasonString = nil; | |
| 257 #if defined(__IPHONE_9_0) && defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && \ | |
| 258 __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_9_0 | |
| 259 if ([UIDevice isIOS9OrLater]) { | |
| 260 NSNumber *reason = notification.userInfo[AVCaptureSessionInterruptionReasonK
ey]; | |
| 261 if (reason) { | |
| 262 switch (reason.intValue) { | |
| 263 case AVCaptureSessionInterruptionReasonVideoDeviceNotAvailableInBackgrou
nd: | |
| 264 reasonString = @"VideoDeviceNotAvailableInBackground"; | |
| 265 break; | |
| 266 case AVCaptureSessionInterruptionReasonAudioDeviceInUseByAnotherClient: | |
| 267 reasonString = @"AudioDeviceInUseByAnotherClient"; | |
| 268 break; | |
| 269 case AVCaptureSessionInterruptionReasonVideoDeviceInUseByAnotherClient: | |
| 270 reasonString = @"VideoDeviceInUseByAnotherClient"; | |
| 271 break; | |
| 272 case AVCaptureSessionInterruptionReasonVideoDeviceNotAvailableWithMultip
leForegroundApps: | |
| 273 reasonString = @"VideoDeviceNotAvailableWithMultipleForegroundApps"; | |
| 274 break; | |
| 275 } | |
| 276 } | |
| 277 } | |
| 278 #endif | |
| 279 RTCLog(@"Capture session interrupted: %@", reasonString); | |
| 280 } | |
| 281 | |
| 282 - (void)handleCaptureSessionInterruptionEnded:(NSNotification *)notification { | |
| 283 RTCLog(@"Capture session interruption ended."); | |
| 284 } | |
| 285 | |
| 286 - (void)handleCaptureSessionRuntimeError:(NSNotification *)notification { | |
| 287 NSError *error = [notification.userInfo objectForKey:AVCaptureSessionErrorKey]
; | |
| 288 RTCLogError(@"Capture session runtime error: %@", error); | |
| 289 | |
| 290 [RTCDispatcher dispatchAsyncOnType:RTCDispatcherTypeCaptureSession | |
| 291 block:^{ | |
| 292 #if TARGET_OS_IPHONE | |
| 293 if (error.code == AVErrorMediaServicesWereReset
) { | |
| 294 [self handleNonFatalError]; | |
| 295 } else { | |
| 296 [self handleFatalError]; | |
| 297 } | |
| 298 #else | |
| 299 [self handleFatalError]; | |
| 300 #endif | |
| 301 }]; | |
| 302 } | |
| 303 | |
| 304 - (void)handleCaptureSessionDidStartRunning:(NSNotification *)notification { | |
| 305 RTCLog(@"Capture session started."); | |
| 306 | |
| 307 [RTCDispatcher dispatchAsyncOnType:RTCDispatcherTypeCaptureSession | |
| 308 block:^{ | |
| 309 // If we successfully restarted after an unknow
n error, | |
| 310 // allow future retries on fatal errors. | |
| 311 _hasRetriedOnFatalError = NO; | |
| 312 }]; | |
| 313 } | |
| 314 | |
| 315 - (void)handleCaptureSessionDidStopRunning:(NSNotification *)notification { | |
| 316 RTCLog(@"Capture session stopped."); | |
| 317 } | |
| 318 | |
| 319 - (void)handleFatalError { | |
| 320 [RTCDispatcher | |
| 321 dispatchAsyncOnType:RTCDispatcherTypeCaptureSession | |
| 322 block:^{ | |
| 323 if (!_hasRetriedOnFatalError) { | |
| 324 RTCLogWarning(@"Attempting to recover from fatal capture
error."); | |
| 325 [self handleNonFatalError]; | |
| 326 _hasRetriedOnFatalError = YES; | |
| 327 } else { | |
| 328 RTCLogError(@"Previous fatal error recovery failed."); | |
| 329 } | |
| 330 }]; | |
| 331 } | |
| 332 | |
| 333 - (void)handleNonFatalError { | |
| 334 [RTCDispatcher dispatchAsyncOnType:RTCDispatcherTypeCaptureSession | |
| 335 block:^{ | |
| 336 RTCLog(@"Restarting capture session after error
."); | |
| 337 if (_isRunning) { | |
| 338 [_captureSession startRunning]; | |
| 339 } | |
| 340 }]; | |
| 341 } | |
| 342 | |
| 343 #if TARGET_OS_IPHONE | |
| 344 | |
| 345 #pragma mark - UIApplication notifications | |
| 346 | |
| 347 - (void)handleApplicationDidBecomeActive:(NSNotification *)notification { | |
| 348 [RTCDispatcher dispatchAsyncOnType:RTCDispatcherTypeCaptureSession | |
| 349 block:^{ | |
| 350 if (_isRunning && !_captureSession.isRunning) { | |
| 351 RTCLog(@"Restarting capture session on active
."); | |
| 352 [_captureSession startRunning]; | |
| 353 } | |
| 354 }]; | |
| 355 } | |
| 356 | |
| 357 #endif // TARGET_OS_IPHONE | |
| 358 | |
| 359 #pragma mark - Private | |
| 360 | |
| 361 - (dispatch_queue_t)frameQueue { | |
| 362 if (!_frameQueue) { | |
| 363 _frameQueue = | |
| 364 dispatch_queue_create("org.webrtc.avfoundationvideocapturer.video", DISP
ATCH_QUEUE_SERIAL); | |
| 365 dispatch_set_target_queue(_frameQueue, | |
| 366 dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_
HIGH, 0)); | |
| 367 } | |
| 368 return _frameQueue; | |
| 369 } | |
| 370 | |
| 371 - (BOOL)setupCaptureSession { | |
| 372 NSAssert(_captureSession == nil, @"Setup capture session called twice."); | |
| 373 _captureSession = [[AVCaptureSession alloc] init]; | |
| 374 #if defined(WEBRTC_IOS) | |
| 375 _captureSession.sessionPreset = AVCaptureSessionPresetInputPriority; | |
| 376 _captureSession.usesApplicationAudioSession = NO; | |
| 377 #endif | |
| 378 [self setupVideoDataOutput]; | |
| 379 // Add the output. | |
| 380 if (![_captureSession canAddOutput:_videoDataOutput]) { | |
| 381 RTCLogError(@"Video data output unsupported."); | |
| 382 return NO; | |
| 383 } | |
| 384 [_captureSession addOutput:_videoDataOutput]; | |
| 385 | |
| 386 return YES; | |
| 387 } | |
| 388 | |
| 389 - (void)setupVideoDataOutput { | |
| 390 NSAssert(_videoDataOutput == nil, @"Setup video data output called twice."); | |
| 391 // Make the capturer output NV12. Ideally we want I420 but that's not | |
| 392 // currently supported on iPhone / iPad. | |
| 393 AVCaptureVideoDataOutput *videoDataOutput = [[AVCaptureVideoDataOutput alloc]
init]; | |
| 394 videoDataOutput.videoSettings = @{ | |
| 395 (NSString *) | |
| 396 // TODO(denicija): Remove this color conversion and use the original capture
format directly. | |
| 397 kCVPixelBufferPixelFormatTypeKey : @(kCVPixelFormatType_420YpCbCr8BiPlanarFu
llRange) | |
| 398 }; | |
| 399 videoDataOutput.alwaysDiscardsLateVideoFrames = NO; | |
| 400 [videoDataOutput setSampleBufferDelegate:self queue:self.frameQueue]; | |
| 401 _videoDataOutput = videoDataOutput; | |
| 402 } | |
| 403 | |
| 404 #pragma mark - Private, called inside capture queue | |
| 405 | |
| 406 - (void)updateDeviceCaptureFormat:(AVCaptureDeviceFormat *)format fps:(NSInteger
)fps { | |
| 407 NSAssert([RTCDispatcher isOnQueueForType:RTCDispatcherTypeCaptureSession], | |
| 408 @"updateDeviceCaptureFormat must be called on the capture queue."); | |
| 409 @try { | |
| 410 _currentDevice.activeFormat = format; | |
| 411 _currentDevice.activeVideoMinFrameDuration = CMTimeMake(1, fps); | |
| 412 _currentDevice.activeVideoMaxFrameDuration = CMTimeMake(1, fps); | |
| 413 } @catch (NSException *exception) { | |
| 414 RTCLogError(@"Failed to set active format!\n User info:%@", exception.userIn
fo); | |
| 415 return; | |
| 416 } | |
| 417 } | |
| 418 | |
| 419 - (void)reconfigureCaptureSessionInput { | |
| 420 NSAssert([RTCDispatcher isOnQueueForType:RTCDispatcherTypeCaptureSession], | |
| 421 @"reconfigureCaptureSessionInput must be called on the capture queue.
"); | |
| 422 NSError *error = nil; | |
| 423 AVCaptureDeviceInput *input = | |
| 424 [AVCaptureDeviceInput deviceInputWithDevice:_currentDevice error:&error]; | |
| 425 if (!input) { | |
| 426 RTCLogError(@"Failed to create front camera input: %@", error.localizedDescr
iption); | |
| 427 return; | |
| 428 } | |
| 429 [_captureSession beginConfiguration]; | |
| 430 for (AVCaptureDeviceInput *oldInput in [_captureSession.inputs copy]) { | |
| 431 [_captureSession removeInput:oldInput]; | |
| 432 } | |
| 433 if ([_captureSession canAddInput:input]) { | |
| 434 [_captureSession addInput:input]; | |
| 435 } else { | |
| 436 RTCLogError(@"Cannot add camera as an input to the session."); | |
| 437 } | |
| 438 [_captureSession commitConfiguration]; | |
| 439 } | |
| 440 | |
| 441 - (void)updateOrientation { | |
| 442 NSAssert([RTCDispatcher isOnQueueForType:RTCDispatcherTypeCaptureSession], | |
| 443 @"updateOrientation must be called on the capture queue."); | |
| 444 #if TARGET_OS_IPHONE | |
| 445 _orientation = [UIDevice currentDevice].orientation; | |
| 446 #endif | |
| 447 } | |
| 448 | |
| 449 @end | |
| OLD | NEW |