You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2165 lines
100 KiB
2165 lines
100 KiB
//
|
|
// SCManagedCapturer.m
|
|
// Snapchat
|
|
//
|
|
// Created by Liu Liu on 4/20/15.
|
|
// Copyright (c) 2015 Liu Liu. All rights reserved.
|
|
//
|
|
|
|
#import "SCManagedCapturerV1.h"
|
|
#import "SCManagedCapturerV1_Private.h"
|
|
|
|
#import "ARConfiguration+SCConfiguration.h"
|
|
#import "NSURL+Asset.h"
|
|
#import "SCBlackCameraDetector.h"
|
|
#import "SCBlackCameraNoOutputDetector.h"
|
|
#import "SCCameraTweaks.h"
|
|
#import "SCCaptureResource.h"
|
|
#import "SCCaptureSessionFixer.h"
|
|
#import "SCCaptureUninitializedState.h"
|
|
#import "SCCaptureWorker.h"
|
|
#import "SCCapturerToken.h"
|
|
#import "SCManagedAudioStreamer.h"
|
|
#import "SCManagedCaptureDevice+SCManagedCapturer.h"
|
|
#import "SCManagedCaptureDeviceDefaultZoomHandler.h"
|
|
#import "SCManagedCaptureDeviceHandler.h"
|
|
#import "SCManagedCaptureDeviceSubjectAreaHandler.h"
|
|
#import "SCManagedCapturePreviewLayerController.h"
|
|
#import "SCManagedCaptureSession.h"
|
|
#import "SCManagedCapturerARImageCaptureProvider.h"
|
|
#import "SCManagedCapturerGLViewManagerAPI.h"
|
|
#import "SCManagedCapturerLSAComponentTrackerAPI.h"
|
|
#import "SCManagedCapturerLensAPI.h"
|
|
#import "SCManagedCapturerListenerAnnouncer.h"
|
|
#import "SCManagedCapturerLogging.h"
|
|
#import "SCManagedCapturerSampleMetadata.h"
|
|
#import "SCManagedCapturerState.h"
|
|
#import "SCManagedCapturerStateBuilder.h"
|
|
#import "SCManagedDeviceCapacityAnalyzer.h"
|
|
#import "SCManagedDroppedFramesReporter.h"
|
|
#import "SCManagedFrameHealthChecker.h"
|
|
#import "SCManagedFrontFlashController.h"
|
|
#import "SCManagedStillImageCapturer.h"
|
|
#import "SCManagedStillImageCapturerHandler.h"
|
|
#import "SCManagedVideoARDataSource.h"
|
|
#import "SCManagedVideoCapturer.h"
|
|
#import "SCManagedVideoFileStreamer.h"
|
|
#import "SCManagedVideoFrameSampler.h"
|
|
#import "SCManagedVideoScanner.h"
|
|
#import "SCManagedVideoStreamReporter.h"
|
|
#import "SCManagedVideoStreamer.h"
|
|
#import "SCMetalUtils.h"
|
|
#import "SCProcessingPipeline.h"
|
|
#import "SCProcessingPipelineBuilder.h"
|
|
#import "SCScanConfiguration.h"
|
|
#import "SCSingleFrameStreamCapturer.h"
|
|
#import "SCSnapCreationTriggers.h"
|
|
#import "SCTimedTask.h"
|
|
|
|
#import <SCBase/SCAssignment.h>
|
|
#import <SCBase/SCLazyLoadingProxy.h>
|
|
#import <SCBatteryLogger/SCBatteryLogger.h>
|
|
#import <SCFoundation/NSData+Random.h>
|
|
#import <SCFoundation/NSError+Helpers.h>
|
|
#import <SCFoundation/NSString+SCFormat.h>
|
|
#import <SCFoundation/SCAppEnvironment.h>
|
|
#import <SCFoundation/SCDeviceName.h>
|
|
#import <SCFoundation/SCQueuePerformer.h>
|
|
#import <SCFoundation/SCThreadHelpers.h>
|
|
#import <SCFoundation/SCTrace.h>
|
|
#import <SCFoundation/SCTraceODPCompatible.h>
|
|
#import <SCFoundation/SCZeroDependencyExperiments.h>
|
|
#import <SCGhostToSnappable/SCGhostToSnappableSignal.h>
|
|
#import <SCImageProcess/SCImageProcessVideoPlaybackSession.h>
|
|
#import <SCLenses/SCLens.h>
|
|
#import <SCLogger/SCCameraMetrics.h>
|
|
#import <SCLogger/SCLogger+Performance.h>
|
|
#import <SCUserTraceLogger/SCUserTraceLogger.h>
|
|
|
|
#import <Looksery/Looksery.h>
|
|
|
|
@import ARKit;
|
|
|
|
static NSUInteger const kSCManagedCapturerFixInconsistencyMaxRetriesWithCurrentSession = 22;
|
|
static CGFloat const kSCManagedCapturerFixInconsistencyARSessionDelayThreshold = 2;
|
|
static CGFloat const kSCManagedCapturerFixInconsistencyARSessionHungInitThreshold = 5;
|
|
|
|
static NSTimeInterval const kMinFixAVSessionRunningInterval = 1; // Interval to run _fixAVSessionIfNecessary
|
|
static NSTimeInterval const kMinFixSessionRuntimeErrorInterval =
|
|
1; // Min interval that RuntimeError calls _startNewSession
|
|
|
|
static NSString *const kSCManagedCapturerErrorDomain = @"kSCManagedCapturerErrorDomain";
|
|
|
|
NSString *const kSCLensesTweaksDidChangeFileInput = @"kSCLensesTweaksDidChangeFileInput";
|
|
|
|
@implementation SCManagedCapturerV1 {
|
|
// No ivars for CapturerV1 please, they should be in resource.
|
|
SCCaptureResource *_captureResource;
|
|
}
|
|
|
|
+ (SCManagedCapturerV1 *)sharedInstance
|
|
{
|
|
static dispatch_once_t onceToken;
|
|
static SCManagedCapturerV1 *managedCapturerV1;
|
|
dispatch_once(&onceToken, ^{
|
|
managedCapturerV1 = [[SCManagedCapturerV1 alloc] init];
|
|
});
|
|
return managedCapturerV1;
|
|
}
|
|
|
|
- (instancetype)init
|
|
{
|
|
SCTraceStart();
|
|
SCAssertMainThread();
|
|
SCCaptureResource *resource = [SCCaptureWorker generateCaptureResource];
|
|
return [self initWithResource:resource];
|
|
}
|
|
|
|
- (instancetype)initWithResource:(SCCaptureResource *)resource
|
|
{
|
|
SCTraceODPCompatibleStart(2);
|
|
SCAssertMainThread();
|
|
self = [super init];
|
|
if (self) {
|
|
// Assuming I am not in background. I can be more defensive here and fetch the app state.
|
|
// But to avoid potential problems, won't do that until later.
|
|
SCLogCapturerInfo(@"======================= cool startup =======================");
|
|
// Initialization of capture resource should be done in worker to be shared between V1 and V2.
|
|
_captureResource = resource;
|
|
_captureResource.handleAVSessionStatusChange = @selector(_handleAVSessionStatusChange:);
|
|
_captureResource.sessionRuntimeError = @selector(_sessionRuntimeError:);
|
|
_captureResource.livenessConsistency = @selector(_livenessConsistency:);
|
|
_captureResource.deviceSubjectAreaHandler =
|
|
[[SCManagedCaptureDeviceSubjectAreaHandler alloc] initWithCaptureResource:_captureResource];
|
|
_captureResource.snapCreationTriggers = [SCSnapCreationTriggers new];
|
|
if (SCIsMasterBuild()) {
|
|
// We call _sessionRuntimeError to reset _captureResource.videoDataSource if input changes
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
selector:@selector(_sessionRuntimeError:)
|
|
name:kSCLensesTweaksDidChangeFileInput
|
|
object:nil];
|
|
}
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (SCBlackCameraDetector *)blackCameraDetector
|
|
{
|
|
return _captureResource.blackCameraDetector;
|
|
}
|
|
|
|
- (void)recreateAVCaptureSession
|
|
{
|
|
SCTraceODPCompatibleStart(2);
|
|
[self _startRunningWithNewCaptureSessionIfNecessary];
|
|
}
|
|
|
|
- (void)_handleAVSessionStatusChange:(NSDictionary *)change
|
|
{
|
|
SCTraceODPCompatibleStart(2);
|
|
SC_GUARD_ELSE_RETURN(!_captureResource.state.arSessionActive);
|
|
SC_GUARD_ELSE_RETURN(!_captureResource.appInBackground);
|
|
BOOL wasRunning = [change[NSKeyValueChangeOldKey] boolValue];
|
|
BOOL isRunning = [change[NSKeyValueChangeNewKey] boolValue];
|
|
SCLogCapturerInfo(@"avSession running status changed: %@ -> %@", wasRunning ? @"running" : @"stopped",
|
|
isRunning ? @"running" : @"stopped");
|
|
|
|
[_captureResource.blackCameraDetector sessionDidChangeIsRunning:isRunning];
|
|
|
|
if (_captureResource.isRecreateSessionFixScheduled) {
|
|
SCLogCapturerInfo(@"Scheduled AVCaptureSession recreation, return");
|
|
return;
|
|
}
|
|
|
|
if (wasRunning != isRunning) {
|
|
runOnMainThreadAsynchronously(^{
|
|
if (isRunning) {
|
|
[_captureResource.announcer managedCapturer:self didStartRunning:_captureResource.state];
|
|
} else {
|
|
[_captureResource.announcer managedCapturer:self didStopRunning:_captureResource.state];
|
|
}
|
|
});
|
|
}
|
|
|
|
if (!isRunning) {
|
|
[_captureResource.queuePerformer perform:^{
|
|
[self _fixAVSessionIfNecessary];
|
|
}];
|
|
} else {
|
|
if (!SCDeviceSupportsMetal()) {
|
|
[self _fixNonMetalSessionPreviewInconsistency];
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void)_fixAVSessionIfNecessary
|
|
{
|
|
SCTraceODPCompatibleStart(2);
|
|
SCAssert([_captureResource.queuePerformer isCurrentPerformer], @"");
|
|
SC_GUARD_ELSE_RETURN(!_captureResource.appInBackground);
|
|
SC_GUARD_ELSE_RETURN(_captureResource.status == SCManagedCapturerStatusRunning);
|
|
[[SCLogger sharedInstance] logStepToEvent:kSCCameraFixAVCaptureSession
|
|
uniqueId:@""
|
|
stepName:@"startConsistencyCheckAndFix"];
|
|
|
|
NSTimeInterval timeNow = [NSDate timeIntervalSinceReferenceDate];
|
|
if (timeNow - _captureResource.lastFixSessionTimestamp < kMinFixAVSessionRunningInterval) {
|
|
SCLogCoreCameraInfo(@"Fixing session in less than %f, skip", kMinFixAVSessionRunningInterval);
|
|
return;
|
|
}
|
|
_captureResource.lastFixSessionTimestamp = timeNow;
|
|
|
|
if (!_captureResource.managedSession.isRunning) {
|
|
SCTraceStartSection("Fix AVSession")
|
|
{
|
|
_captureResource.numRetriesFixAVCaptureSessionWithCurrentSession++;
|
|
SCGhostToSnappableSignalCameraFixInconsistency();
|
|
if (_captureResource.numRetriesFixAVCaptureSessionWithCurrentSession <=
|
|
kSCManagedCapturerFixInconsistencyARSessionDelayThreshold) {
|
|
SCLogCapturerInfo(@"Fixing AVSession");
|
|
[_captureResource.managedSession startRunning];
|
|
SCLogCapturerInfo(@"Fixed AVSession, success : %@", @(_captureResource.managedSession.isRunning));
|
|
[[SCLogger sharedInstance] logStepToEvent:kSCCameraFixAVCaptureSession
|
|
uniqueId:@""
|
|
stepName:@"finishCaptureSessionFix"];
|
|
} else {
|
|
// start running with new capture session if the inconsistency fixing not succeeds
|
|
SCLogCapturerInfo(@"*** Recreate and run new capture session to fix the inconsistency ***");
|
|
[self _startRunningWithNewCaptureSessionIfNecessary];
|
|
[[SCLogger sharedInstance] logStepToEvent:kSCCameraFixAVCaptureSession
|
|
uniqueId:@""
|
|
stepName:@"finishNewCaptureSessionCreation"];
|
|
}
|
|
}
|
|
SCTraceEndSection();
|
|
[[SCLogger sharedInstance]
|
|
logTimedEventEnd:kSCCameraFixAVCaptureSession
|
|
uniqueId:@""
|
|
parameters:@{
|
|
@"success" : @(_captureResource.managedSession.isRunning),
|
|
@"count" : @(_captureResource.numRetriesFixAVCaptureSessionWithCurrentSession)
|
|
}];
|
|
} else {
|
|
_captureResource.numRetriesFixAVCaptureSessionWithCurrentSession = 0;
|
|
[[SCLogger sharedInstance] cancelLogTimedEvent:kSCCameraFixAVCaptureSession uniqueId:@""];
|
|
}
|
|
if (_captureResource.managedSession.isRunning) {
|
|
// If it is fixed, we signal received the first frame.
|
|
SCGhostToSnappableSignalDidReceiveFirstPreviewFrame();
|
|
|
|
// For non-metal preview render, we need to make sure preview is not hidden
|
|
if (!SCDeviceSupportsMetal()) {
|
|
[self _fixNonMetalSessionPreviewInconsistency];
|
|
}
|
|
runOnMainThreadAsynchronously(^{
|
|
[_captureResource.announcer managedCapturer:self didStartRunning:_captureResource.state];
|
|
// To approximate this did render timer, it is not accurate.
|
|
SCGhostToSnappableSignalDidRenderFirstPreviewFrame(CACurrentMediaTime());
|
|
});
|
|
} else {
|
|
[_captureResource.queuePerformer perform:^{
|
|
[self _fixAVSessionIfNecessary];
|
|
}
|
|
after:1];
|
|
}
|
|
|
|
[_captureResource.blackCameraDetector sessionDidChangeIsRunning:_captureResource.managedSession.isRunning];
|
|
}
|
|
|
|
- (void)_fixNonMetalSessionPreviewInconsistency
|
|
{
|
|
SCTraceODPCompatibleStart(2);
|
|
SC_GUARD_ELSE_RETURN(_captureResource.status == SCManagedCapturerStatusRunning);
|
|
if ((!_captureResource.videoPreviewLayer.hidden) != _captureResource.managedSession.isRunning) {
|
|
SCTraceStartSection("Fix non-Metal VideoPreviewLayer");
|
|
{
|
|
[CATransaction begin];
|
|
[CATransaction setDisableActions:YES];
|
|
[SCCaptureWorker setupVideoPreviewLayer:_captureResource];
|
|
[CATransaction commit];
|
|
}
|
|
SCTraceEndSection();
|
|
}
|
|
}
|
|
|
|
- (SCCaptureResource *)captureResource
|
|
{
|
|
SCTraceODPCompatibleStart(2);
|
|
return _captureResource;
|
|
}
|
|
|
|
- (id<SCManagedCapturerLensAPI>)lensProcessingCore
|
|
{
|
|
SCTraceODPCompatibleStart(2);
|
|
@weakify(self);
|
|
return (id<SCManagedCapturerLensAPI>)[[SCLazyLoadingProxy alloc] initWithInitializationBlock:^id {
|
|
@strongify(self);
|
|
SCReportErrorIf(self.captureResource.state.lensProcessorReady, @"[Lenses] Lens processing core is not ready");
|
|
return self.captureResource.lensProcessingCore;
|
|
}];
|
|
}
|
|
|
|
- (SCVideoCaptureSessionInfo)activeSession
|
|
{
|
|
SCTraceODPCompatibleStart(2);
|
|
return [SCCaptureWorker activeSession:_captureResource];
|
|
}
|
|
|
|
- (BOOL)isLensApplied
|
|
{
|
|
SCTraceODPCompatibleStart(2);
|
|
return [SCCaptureWorker isLensApplied:_captureResource];
|
|
}
|
|
|
|
- (BOOL)isVideoMirrored
|
|
{
|
|
SCTraceODPCompatibleStart(2);
|
|
return [SCCaptureWorker isVideoMirrored:_captureResource];
|
|
}
|
|
|
|
#pragma mark - Setup, Start & Stop
|
|
|
|
- (void)_updateHRSIEnabled
|
|
{
|
|
SCTraceODPCompatibleStart(2);
|
|
// Since night mode is low-res, we set high resolution still image output when night mode is enabled
|
|
// SoftwareZoom requires higher resolution image to get better zooming result too.
|
|
// We also want a higher resolution on newer devices
|
|
BOOL is1080pSupported = [SCManagedCaptureDevice is1080pSupported];
|
|
BOOL shouldHRSIEnabled =
|
|
(_captureResource.device.isNightModeActive || _captureResource.device.softwareZoom || is1080pSupported);
|
|
SCLogCapturerInfo(@"Setting HRSIEnabled to: %d. isNightModeActive:%d softwareZoom:%d is1080pSupported:%d",
|
|
shouldHRSIEnabled, _captureResource.device.isNightModeActive,
|
|
_captureResource.device.softwareZoom, is1080pSupported);
|
|
[_captureResource.stillImageCapturer setHighResolutionStillImageOutputEnabled:shouldHRSIEnabled];
|
|
}
|
|
|
|
- (void)_updateStillImageStabilizationEnabled
|
|
{
|
|
SCTraceODPCompatibleStart(2);
|
|
SCLogCapturerInfo(@"Enabling still image stabilization");
|
|
[_captureResource.stillImageCapturer enableStillImageStabilization];
|
|
}
|
|
|
|
- (void)setupWithDevicePositionAsynchronously:(SCManagedCaptureDevicePosition)devicePosition
|
|
completionHandler:(dispatch_block_t)completionHandler
|
|
context:(NSString *)context
|
|
{
|
|
SCTraceODPCompatibleStart(2);
|
|
SCLogCapturerInfo(@"Setting up with devicePosition:%lu", (unsigned long)devicePosition);
|
|
SCTraceResumeToken token = SCTraceCapture();
|
|
[[SCManagedCapturePreviewLayerController sharedInstance] setupPreviewLayer];
|
|
[_captureResource.queuePerformer perform:^{
|
|
SCTraceResume(token);
|
|
[self setupWithDevicePosition:devicePosition completionHandler:completionHandler];
|
|
}];
|
|
}
|
|
|
|
- (void)setupWithDevicePosition:(SCManagedCaptureDevicePosition)devicePosition
|
|
completionHandler:(dispatch_block_t)completionHandler
|
|
{
|
|
SCTraceODPCompatibleStart(2);
|
|
SCAssertPerformer(_captureResource.queuePerformer);
|
|
[SCCaptureWorker setupWithCaptureResource:_captureResource devicePosition:devicePosition];
|
|
|
|
[self addListener:_captureResource.stillImageCapturer];
|
|
[self addListener:_captureResource.blackCameraDetector.blackCameraNoOutputDetector];
|
|
[self addListener:_captureResource.lensProcessingCore];
|
|
|
|
[self _updateHRSIEnabled];
|
|
[self _updateStillImageStabilizationEnabled];
|
|
|
|
[SCCaptureWorker updateLensesFieldOfViewTracking:_captureResource];
|
|
|
|
if (!SCDeviceSupportsMetal()) {
|
|
[SCCaptureWorker makeVideoPreviewLayer:_captureResource];
|
|
}
|
|
|
|
// I need to do this setup now. Thus, it is off the main thread. This also means my preview layer controller is
|
|
// entangled with the capturer.
|
|
[[SCManagedCapturePreviewLayerController sharedInstance] setupRenderPipeline];
|
|
[[SCManagedCapturePreviewLayerController sharedInstance] setManagedCapturer:self];
|
|
_captureResource.status = SCManagedCapturerStatusReady;
|
|
|
|
SCManagedCapturerState *state = [_captureResource.state copy];
|
|
AVCaptureVideoPreviewLayer *videoPreviewLayer = _captureResource.videoPreviewLayer;
|
|
runOnMainThreadAsynchronously(^{
|
|
SCLogCapturerInfo(@"Did setup with devicePosition:%lu", (unsigned long)devicePosition);
|
|
[_captureResource.announcer managedCapturer:self didChangeState:state];
|
|
[_captureResource.announcer managedCapturer:self didChangeCaptureDevicePosition:state];
|
|
if (!SCDeviceSupportsMetal()) {
|
|
[_captureResource.announcer managedCapturer:self didChangeVideoPreviewLayer:videoPreviewLayer];
|
|
}
|
|
if (completionHandler) {
|
|
completionHandler();
|
|
}
|
|
});
|
|
}
|
|
|
|
- (void)addSampleBufferDisplayController:(id<SCManagedSampleBufferDisplayController>)sampleBufferDisplayController
|
|
context:(NSString *)context
|
|
{
|
|
SCTraceODPCompatibleStart(2);
|
|
[_captureResource.queuePerformer perform:^{
|
|
_captureResource.sampleBufferDisplayController = sampleBufferDisplayController;
|
|
[_captureResource.videoDataSource addSampleBufferDisplayController:sampleBufferDisplayController];
|
|
}];
|
|
}
|
|
|
|
- (SCCapturerToken *)startRunningAsynchronouslyWithCompletionHandler:(dispatch_block_t)completionHandler
|
|
context:(NSString *)context
|
|
{
|
|
SCTraceODPCompatibleStart(2);
|
|
SCTraceResumeToken resumeToken = SCTraceCapture();
|
|
[[SCLogger sharedInstance] updateLogTimedEventStart:kSCCameraMetricsOpen uniqueId:@""];
|
|
SCCapturerToken *token = [[SCCapturerToken alloc] initWithIdentifier:context];
|
|
SCLogCapturerInfo(@"startRunningAsynchronouslyWithCompletionHandler called. token: %@", token);
|
|
[_captureResource.queuePerformer perform:^{
|
|
SCTraceResume(resumeToken);
|
|
[SCCaptureWorker startRunningWithCaptureResource:_captureResource
|
|
token:token
|
|
completionHandler:completionHandler];
|
|
// After startRunning, we need to make sure _fixAVSessionIfNecessary start running.
|
|
// The problem: with the new KVO fix strategy, it may happen that AVCaptureSession is in stopped state, thus no
|
|
// KVO callback is triggered.
|
|
// And calling startRunningAsynchronouslyWithCompletionHandler has no effect because SCManagedCapturerStatus is
|
|
// in SCManagedCapturerStatusRunning state
|
|
[self _fixAVSessionIfNecessary];
|
|
}];
|
|
return token;
|
|
}
|
|
|
|
- (BOOL)stopRunningWithCaptureToken:(SCCapturerToken *)token
|
|
completionHandler:(sc_managed_capturer_stop_running_completion_handler_t)completionHandler
|
|
context:(NSString *)context
|
|
{
|
|
SCTraceODPCompatibleStart(2);
|
|
SCAssertPerformer(_captureResource.queuePerformer);
|
|
SCLogCapturerInfo(@"Stop running. token:%@ context:%@", token, context);
|
|
return [SCCaptureWorker stopRunningWithCaptureResource:_captureResource
|
|
token:token
|
|
completionHandler:completionHandler];
|
|
}
|
|
|
|
- (void)stopRunningAsynchronously:(SCCapturerToken *)token
|
|
completionHandler:(sc_managed_capturer_stop_running_completion_handler_t)completionHandler
|
|
context:(NSString *)context
|
|
{
|
|
SCTraceODPCompatibleStart(2);
|
|
SCLogCapturerInfo(@"Stop running asynchronously. token:%@ context:%@", token, context);
|
|
SCTraceResumeToken resumeToken = SCTraceCapture();
|
|
[_captureResource.queuePerformer perform:^{
|
|
SCTraceResume(resumeToken);
|
|
[SCCaptureWorker stopRunningWithCaptureResource:_captureResource
|
|
token:token
|
|
completionHandler:completionHandler];
|
|
}];
|
|
}
|
|
|
|
- (void)stopRunningAsynchronously:(SCCapturerToken *)token
|
|
completionHandler:(sc_managed_capturer_stop_running_completion_handler_t)completionHandler
|
|
after:(NSTimeInterval)delay
|
|
context:(NSString *)context
|
|
{
|
|
SCTraceODPCompatibleStart(2);
|
|
SCLogCapturerInfo(@"Stop running asynchronously. token:%@ delay:%f", token, delay);
|
|
NSTimeInterval startTime = CACurrentMediaTime();
|
|
[_captureResource.queuePerformer perform:^{
|
|
NSTimeInterval elapsedTime = CACurrentMediaTime() - startTime;
|
|
[_captureResource.queuePerformer perform:^{
|
|
SCTraceStart();
|
|
// If we haven't started a new running sequence yet, stop running now
|
|
[SCCaptureWorker stopRunningWithCaptureResource:_captureResource
|
|
token:token
|
|
completionHandler:completionHandler];
|
|
}
|
|
after:MAX(delay - elapsedTime, 0)];
|
|
}];
|
|
}
|
|
|
|
- (void)startStreamingAsynchronouslyWithCompletionHandler:(dispatch_block_t)completionHandler
|
|
context:(NSString *)context
|
|
{
|
|
SCTraceODPCompatibleStart(2);
|
|
SCLogCapturerInfo(@"Start streaming asynchronously");
|
|
[_captureResource.queuePerformer perform:^{
|
|
SCTraceStart();
|
|
[SCCaptureWorker startStreaming:_captureResource];
|
|
if (completionHandler) {
|
|
runOnMainThreadAsynchronously(completionHandler);
|
|
}
|
|
}];
|
|
}
|
|
|
|
#pragma mark - Recording / Capture
|
|
|
|
- (void)captureStillImageAsynchronouslyWithAspectRatio:(CGFloat)aspectRatio
|
|
captureSessionID:(NSString *)captureSessionID
|
|
completionHandler:
|
|
(sc_managed_capturer_capture_still_image_completion_handler_t)completionHandler
|
|
context:(NSString *)context
|
|
{
|
|
SCTraceODPCompatibleStart(2);
|
|
[_captureResource.queuePerformer perform:^{
|
|
[SCCaptureWorker captureStillImageWithCaptureResource:_captureResource
|
|
aspectRatio:aspectRatio
|
|
captureSessionID:captureSessionID
|
|
shouldCaptureFromVideo:[self _shouldCaptureImageFromVideo]
|
|
completionHandler:completionHandler
|
|
context:context];
|
|
}];
|
|
}
|
|
|
|
- (void)captureSingleVideoFrameAsynchronouslyWithCompletionHandler:
|
|
(sc_managed_capturer_capture_video_frame_completion_handler_t)completionHandler
|
|
context:(NSString *)context
|
|
{
|
|
SCTraceODPCompatibleStart(2);
|
|
[_captureResource.queuePerformer perform:^{
|
|
SCTraceStart();
|
|
SCLogCapturerInfo(@"Start capturing single video frame");
|
|
_captureResource.frameCap = [[SCSingleFrameStreamCapturer alloc] initWithCompletion:^void(UIImage *image) {
|
|
[_captureResource.queuePerformer perform:^{
|
|
[_captureResource.videoDataSource removeListener:_captureResource.frameCap];
|
|
_captureResource.frameCap = nil;
|
|
}];
|
|
runOnMainThreadAsynchronously(^{
|
|
[_captureResource.device setTorchActive:NO];
|
|
SCLogCapturerInfo(@"End capturing single video frame");
|
|
completionHandler(image);
|
|
});
|
|
}];
|
|
|
|
BOOL waitForTorch = NO;
|
|
if (!_captureResource.state.torchActive) {
|
|
if (_captureResource.state.flashActive) {
|
|
waitForTorch = YES;
|
|
[_captureResource.device setTorchActive:YES];
|
|
}
|
|
}
|
|
[_captureResource.queuePerformer perform:^{
|
|
[_captureResource.videoDataSource addListener:_captureResource.frameCap];
|
|
[SCCaptureWorker startStreaming:_captureResource];
|
|
}
|
|
after:(waitForTorch ? 0.5 : 0)];
|
|
|
|
}];
|
|
}
|
|
|
|
- (void)prepareForRecordingAsynchronouslyWithContext:(NSString *)context
|
|
audioConfiguration:(SCAudioConfiguration *)configuration
|
|
{
|
|
SCTraceODPCompatibleStart(2);
|
|
[_captureResource.queuePerformer perform:^{
|
|
SCLogCapturerInfo(@"prepare for recording");
|
|
[_captureResource.videoCapturer prepareForRecordingWithAudioConfiguration:configuration];
|
|
}];
|
|
}
|
|
|
|
- (void)startRecordingAsynchronouslyWithOutputSettings:(SCManagedVideoCapturerOutputSettings *)outputSettings
|
|
audioConfiguration:(SCAudioConfiguration *)configuration
|
|
maxDuration:(NSTimeInterval)maxDuration
|
|
fileURL:(NSURL *)fileURL
|
|
captureSessionID:(NSString *)captureSessionID
|
|
completionHandler:
|
|
(sc_managed_capturer_start_recording_completion_handler_t)completionHandler
|
|
context:(NSString *)context
|
|
{
|
|
SCTraceODPCompatibleStart(2);
|
|
[_captureResource.queuePerformer perform:^{
|
|
[SCCaptureWorker startRecordingWithCaptureResource:_captureResource
|
|
outputSettings:outputSettings
|
|
audioConfiguration:configuration
|
|
maxDuration:maxDuration
|
|
fileURL:fileURL
|
|
captureSessionID:captureSessionID
|
|
completionHandler:completionHandler];
|
|
}];
|
|
}
|
|
|
|
- (void)stopRecordingAsynchronouslyWithContext:(NSString *)context
|
|
{
|
|
SCTraceODPCompatibleStart(2);
|
|
[_captureResource.queuePerformer perform:^{
|
|
[SCCaptureWorker stopRecordingWithCaptureResource:_captureResource];
|
|
}];
|
|
}
|
|
|
|
- (void)cancelRecordingAsynchronouslyWithContext:(NSString *)context
|
|
{
|
|
SCTraceODPCompatibleStart(2);
|
|
[_captureResource.queuePerformer perform:^{
|
|
[SCCaptureWorker cancelRecordingWithCaptureResource:_captureResource];
|
|
}];
|
|
}
|
|
|
|
- (void)startScanAsynchronouslyWithScanConfiguration:(SCScanConfiguration *)configuration context:(NSString *)context
|
|
{
|
|
SCTraceODPCompatibleStart(2);
|
|
[_captureResource.queuePerformer perform:^{
|
|
SCTraceStart();
|
|
[SCCaptureWorker startScanWithScanConfiguration:configuration resource:_captureResource];
|
|
}];
|
|
}
|
|
|
|
- (void)stopScanAsynchronouslyWithCompletionHandler:(dispatch_block_t)completionHandler context:(NSString *)context
|
|
{
|
|
SCTraceODPCompatibleStart(2);
|
|
[_captureResource.queuePerformer perform:^{
|
|
SCTraceStart();
|
|
[SCCaptureWorker stopScanWithCompletionHandler:completionHandler resource:_captureResource];
|
|
}];
|
|
}
|
|
|
|
- (void)sampleFrameWithCompletionHandler:(void (^)(UIImage *frame, CMTime presentationTime))completionHandler
|
|
context:(NSString *)context
|
|
{
|
|
SCTraceODPCompatibleStart(2);
|
|
// Previously _captureResource.videoFrameSampler was conditionally created when setting up, but if this method is
|
|
// called it is a
|
|
// safe assumption the client wants it to run instead of failing silently, so always create
|
|
// _captureResource.videoFrameSampler
|
|
if (!_captureResource.videoFrameSampler) {
|
|
_captureResource.videoFrameSampler = [SCManagedVideoFrameSampler new];
|
|
[_captureResource.announcer addListener:_captureResource.videoFrameSampler];
|
|
}
|
|
SCLogCapturerInfo(@"Sampling next frame");
|
|
[_captureResource.videoFrameSampler sampleNextFrame:completionHandler];
|
|
}
|
|
|
|
- (void)addTimedTask:(SCTimedTask *)task context:(NSString *)context
|
|
{
|
|
SCTraceODPCompatibleStart(2);
|
|
SCLogCapturerInfo(@"Adding timed task:%@", task);
|
|
[_captureResource.queuePerformer perform:^{
|
|
[_captureResource.videoCapturer addTimedTask:task];
|
|
}];
|
|
}
|
|
|
|
- (void)clearTimedTasksWithContext:(NSString *)context
|
|
{
|
|
SCTraceODPCompatibleStart(2);
|
|
[_captureResource.queuePerformer perform:^{
|
|
[_captureResource.videoCapturer clearTimedTasks];
|
|
}];
|
|
}
|
|
|
|
#pragma mark - Utilities
|
|
|
|
- (void)convertViewCoordinates:(CGPoint)viewCoordinates
|
|
completionHandler:(sc_managed_capturer_convert_view_coordniates_completion_handler_t)completionHandler
|
|
context:(NSString *)context
|
|
{
|
|
SCTraceODPCompatibleStart(2);
|
|
SCAssert(completionHandler, @"completionHandler shouldn't be nil");
|
|
[_captureResource.queuePerformer perform:^{
|
|
SCTraceStart();
|
|
if (SCDeviceSupportsMetal()) {
|
|
CGSize viewSize = [UIScreen mainScreen].fixedCoordinateSpace.bounds.size;
|
|
CGPoint pointOfInterest =
|
|
[_captureResource.device convertViewCoordinates:viewCoordinates
|
|
viewSize:viewSize
|
|
videoGravity:AVLayerVideoGravityResizeAspectFill];
|
|
runOnMainThreadAsynchronously(^{
|
|
completionHandler(pointOfInterest);
|
|
});
|
|
} else {
|
|
CGSize viewSize = _captureResource.videoPreviewLayer.bounds.size;
|
|
CGPoint pointOfInterest =
|
|
[_captureResource.device convertViewCoordinates:viewCoordinates
|
|
viewSize:viewSize
|
|
videoGravity:_captureResource.videoPreviewLayer.videoGravity];
|
|
runOnMainThreadAsynchronously(^{
|
|
completionHandler(pointOfInterest);
|
|
});
|
|
}
|
|
}];
|
|
}
|
|
|
|
- (void)detectLensCategoryOnNextFrame:(CGPoint)point
|
|
lenses:(NSArray<SCLens *> *)lenses
|
|
completion:(sc_managed_lenses_processor_category_point_completion_handler_t)completion
|
|
context:(NSString *)context
|
|
|
|
{
|
|
SCTraceODPCompatibleStart(2);
|
|
SCAssert(completion, @"completionHandler shouldn't be nil");
|
|
SCAssertMainThread();
|
|
[_captureResource.queuePerformer perform:^{
|
|
SCTraceStart();
|
|
SCLogCapturerInfo(@"Detecting lens category on next frame. point:%@, lenses:%@", NSStringFromCGPoint(point),
|
|
[lenses valueForKey:NSStringFromSelector(@selector(lensId))]);
|
|
[_captureResource.lensProcessingCore
|
|
detectLensCategoryOnNextFrame:point
|
|
videoOrientation:_captureResource.videoDataSource.videoOrientation
|
|
lenses:lenses
|
|
completion:^(SCLensCategory *_Nullable category, NSInteger categoriesCount) {
|
|
runOnMainThreadAsynchronously(^{
|
|
if (completion) {
|
|
completion(category, categoriesCount);
|
|
}
|
|
});
|
|
}];
|
|
}];
|
|
}
|
|
|
|
#pragma mark - Configurations
|
|
|
|
- (void)setDevicePositionAsynchronously:(SCManagedCaptureDevicePosition)devicePosition
|
|
completionHandler:(dispatch_block_t)completionHandler
|
|
context:(NSString *)context
|
|
{
|
|
SCTraceODPCompatibleStart(2);
|
|
SCLogCapturerInfo(@"Setting device position asynchronously to: %lu", (unsigned long)devicePosition);
|
|
[_captureResource.queuePerformer perform:^{
|
|
SCTraceStart();
|
|
BOOL devicePositionChanged = NO;
|
|
BOOL nightModeChanged = NO;
|
|
BOOL portraitModeChanged = NO;
|
|
BOOL zoomFactorChanged = NO;
|
|
BOOL flashSupportedOrTorchSupportedChanged = NO;
|
|
SCManagedCapturerState *state = [_captureResource.state copy];
|
|
if (_captureResource.state.devicePosition != devicePosition) {
|
|
SCManagedCaptureDevice *device = [SCManagedCaptureDevice deviceWithPosition:devicePosition];
|
|
if (device) {
|
|
if (!device.delegate) {
|
|
device.delegate = _captureResource.captureDeviceHandler;
|
|
}
|
|
|
|
SCManagedCaptureDevice *prevDevice = _captureResource.device;
|
|
[SCCaptureWorker turnARSessionOff:_captureResource];
|
|
BOOL isStreaming = _captureResource.videoDataSource.isStreaming;
|
|
if (!SCDeviceSupportsMetal()) {
|
|
if (isStreaming) {
|
|
[_captureResource.videoDataSource stopStreaming];
|
|
}
|
|
}
|
|
SCLogCapturerInfo(@"Set device position beginConfiguration");
|
|
[_captureResource.videoDataSource beginConfiguration];
|
|
[_captureResource.managedSession beginConfiguration];
|
|
// Turn off flash for the current device in case it is active
|
|
[_captureResource.device setTorchActive:NO];
|
|
if (_captureResource.state.devicePosition == SCManagedCaptureDevicePositionFront) {
|
|
_captureResource.frontFlashController.torchActive = NO;
|
|
}
|
|
[_captureResource.deviceCapacityAnalyzer removeFocusListener];
|
|
[_captureResource.device removeDeviceAsInput:_captureResource.managedSession.avSession];
|
|
_captureResource.device = device;
|
|
BOOL deviceSet = [_captureResource.device setDeviceAsInput:_captureResource.managedSession.avSession];
|
|
// If we are toggling while recording, set the night mode back to not
|
|
// active
|
|
if (_captureResource.videoRecording) {
|
|
[self _setNightModeActive:NO];
|
|
}
|
|
// Sync night mode, torch and flash state with the current device
|
|
devicePositionChanged = (_captureResource.state.devicePosition != devicePosition);
|
|
nightModeChanged =
|
|
(_captureResource.state.isNightModeActive != _captureResource.device.isNightModeActive);
|
|
portraitModeChanged =
|
|
devicePositionChanged &&
|
|
(devicePosition == SCManagedCaptureDevicePositionBackDualCamera ||
|
|
_captureResource.state.devicePosition == SCManagedCaptureDevicePositionBackDualCamera);
|
|
zoomFactorChanged = (_captureResource.state.zoomFactor != _captureResource.device.zoomFactor);
|
|
if (zoomFactorChanged && _captureResource.device.softwareZoom) {
|
|
[SCCaptureWorker softwareZoomWithDevice:_captureResource.device resource:_captureResource];
|
|
}
|
|
if (_captureResource.state.flashActive != _captureResource.device.flashActive) {
|
|
// preserve flashActive across devices
|
|
_captureResource.device.flashActive = _captureResource.state.flashActive;
|
|
}
|
|
if (_captureResource.state.liveVideoStreaming != device.liveVideoStreamingActive) {
|
|
// preserve liveVideoStreaming state across devices
|
|
[_captureResource.device setLiveVideoStreaming:_captureResource.state.liveVideoStreaming
|
|
session:_captureResource.managedSession.avSession];
|
|
}
|
|
if (devicePosition == SCManagedCaptureDevicePositionBackDualCamera &&
|
|
_captureResource.state.isNightModeActive != _captureResource.device.isNightModeActive) {
|
|
// preserve nightMode when switching from back camera to back dual camera
|
|
[self _setNightModeActive:_captureResource.state.isNightModeActive];
|
|
}
|
|
|
|
flashSupportedOrTorchSupportedChanged =
|
|
(_captureResource.state.flashSupported != _captureResource.device.isFlashSupported ||
|
|
_captureResource.state.torchSupported != _captureResource.device.isTorchSupported);
|
|
SCLogCapturerInfo(@"Set device position: %lu -> %lu, night mode: %d -> %d, zoom "
|
|
@"factor: %f -> %f, flash supported: %d -> %d, torch supported: %d -> %d",
|
|
(unsigned long)_captureResource.state.devicePosition, (unsigned long)devicePosition,
|
|
_captureResource.state.isNightModeActive, _captureResource.device.isNightModeActive,
|
|
_captureResource.state.zoomFactor, _captureResource.device.zoomFactor,
|
|
_captureResource.state.flashSupported, _captureResource.device.isFlashSupported,
|
|
_captureResource.state.torchSupported, _captureResource.device.isTorchSupported);
|
|
_captureResource.state = [[[[[[[[SCManagedCapturerStateBuilder
|
|
withManagedCapturerState:_captureResource.state] setDevicePosition:devicePosition]
|
|
setIsNightModeActive:_captureResource.device.isNightModeActive]
|
|
setZoomFactor:_captureResource.device.zoomFactor]
|
|
setFlashSupported:_captureResource.device.isFlashSupported]
|
|
setTorchSupported:_captureResource.device.isTorchSupported]
|
|
setIsPortraitModeActive:devicePosition == SCManagedCaptureDevicePositionBackDualCamera] build];
|
|
[self _updateHRSIEnabled];
|
|
[self _updateStillImageStabilizationEnabled];
|
|
// This needs to be done after we have finished configure everything
|
|
// for session otherwise we
|
|
// may set it up without hooking up the video input yet, and will set
|
|
// wrong parameter for the
|
|
// output.
|
|
[_captureResource.videoDataSource setDevicePosition:devicePosition];
|
|
if (@available(ios 11.0, *)) {
|
|
if (portraitModeChanged) {
|
|
[_captureResource.videoDataSource
|
|
setDepthCaptureEnabled:_captureResource.state.isPortraitModeActive];
|
|
[_captureResource.device setCaptureDepthData:_captureResource.state.isPortraitModeActive
|
|
session:_captureResource.managedSession.avSession];
|
|
[_captureResource.stillImageCapturer
|
|
setPortraitModeCaptureEnabled:_captureResource.state.isPortraitModeActive];
|
|
if (_captureResource.state.isPortraitModeActive) {
|
|
SCProcessingPipelineBuilder *processingPipelineBuilder =
|
|
[[SCProcessingPipelineBuilder alloc] init];
|
|
processingPipelineBuilder.portraitModeEnabled = YES;
|
|
SCProcessingPipeline *pipeline = [processingPipelineBuilder build];
|
|
SCLogCapturerInfo(@"Adding processing pipeline:%@", pipeline);
|
|
[_captureResource.videoDataSource addProcessingPipeline:pipeline];
|
|
} else {
|
|
[_captureResource.videoDataSource removeProcessingPipeline];
|
|
}
|
|
}
|
|
}
|
|
[_captureResource.deviceCapacityAnalyzer setAsFocusListenerForDevice:_captureResource.device];
|
|
|
|
[SCCaptureWorker updateLensesFieldOfViewTracking:_captureResource];
|
|
[_captureResource.managedSession commitConfiguration];
|
|
[_captureResource.videoDataSource commitConfiguration];
|
|
|
|
// Checks if the flash is activated and if so switches the flash along
|
|
// with the camera view. Setting device's torch mode has to be called after -[AVCaptureSession
|
|
// commitConfiguration], otherwise flash may be not working, especially for iPhone 8/8 Plus.
|
|
if (_captureResource.state.torchActive ||
|
|
(_captureResource.state.flashActive && _captureResource.videoRecording)) {
|
|
[_captureResource.device setTorchActive:YES];
|
|
if (devicePosition == SCManagedCaptureDevicePositionFront) {
|
|
_captureResource.frontFlashController.torchActive = YES;
|
|
}
|
|
}
|
|
|
|
SCLogCapturerInfo(@"Set device position commitConfiguration");
|
|
[_captureResource.droppedFramesReporter didChangeCaptureDevicePosition];
|
|
if (!SCDeviceSupportsMetal()) {
|
|
if (isStreaming) {
|
|
[SCCaptureWorker startStreaming:_captureResource];
|
|
}
|
|
}
|
|
NSArray *inputs = _captureResource.managedSession.avSession.inputs;
|
|
if (!deviceSet) {
|
|
[self _logFailureSetDevicePositionFrom:_captureResource.state.devicePosition
|
|
to:devicePosition
|
|
reason:@"setDeviceForInput failed"];
|
|
} else if (inputs.count == 0) {
|
|
[self _logFailureSetDevicePositionFrom:_captureResource.state.devicePosition
|
|
to:devicePosition
|
|
reason:@"no input"];
|
|
} else if (inputs.count > 1) {
|
|
[self
|
|
_logFailureSetDevicePositionFrom:_captureResource.state.devicePosition
|
|
to:devicePosition
|
|
reason:[NSString sc_stringWithFormat:@"multiple inputs: %@", inputs]];
|
|
} else {
|
|
AVCaptureDeviceInput *input = [inputs firstObject];
|
|
AVCaptureDevice *resultDevice = input.device;
|
|
if (resultDevice == prevDevice.device) {
|
|
[self _logFailureSetDevicePositionFrom:_captureResource.state.devicePosition
|
|
to:devicePosition
|
|
reason:@"stayed on previous device"];
|
|
} else if (resultDevice != _captureResource.device.device) {
|
|
[self
|
|
_logFailureSetDevicePositionFrom:_captureResource.state.devicePosition
|
|
to:devicePosition
|
|
reason:[NSString sc_stringWithFormat:@"unknown input device: %@",
|
|
resultDevice]];
|
|
}
|
|
}
|
|
} else {
|
|
[self _logFailureSetDevicePositionFrom:_captureResource.state.devicePosition
|
|
to:devicePosition
|
|
reason:@"no device"];
|
|
}
|
|
} else {
|
|
SCLogCapturerInfo(@"Device position did not change");
|
|
if (_captureResource.device.position != _captureResource.state.devicePosition) {
|
|
[self _logFailureSetDevicePositionFrom:state.devicePosition
|
|
to:devicePosition
|
|
reason:@"state position set incorrectly"];
|
|
}
|
|
}
|
|
BOOL stateChanged = ![_captureResource.state isEqual:state];
|
|
state = [_captureResource.state copy];
|
|
runOnMainThreadAsynchronously(^{
|
|
if (stateChanged) {
|
|
[_captureResource.announcer managedCapturer:self didChangeState:state];
|
|
}
|
|
if (devicePositionChanged) {
|
|
[_captureResource.announcer managedCapturer:self didChangeCaptureDevicePosition:state];
|
|
}
|
|
if (nightModeChanged) {
|
|
[_captureResource.announcer managedCapturer:self didChangeNightModeActive:state];
|
|
}
|
|
if (portraitModeChanged) {
|
|
[_captureResource.announcer managedCapturer:self didChangePortraitModeActive:state];
|
|
}
|
|
if (zoomFactorChanged) {
|
|
[_captureResource.announcer managedCapturer:self didChangeZoomFactor:state];
|
|
}
|
|
if (flashSupportedOrTorchSupportedChanged) {
|
|
[_captureResource.announcer managedCapturer:self didChangeFlashSupportedAndTorchSupported:state];
|
|
}
|
|
if (completionHandler) {
|
|
completionHandler();
|
|
}
|
|
});
|
|
}];
|
|
}
|
|
|
|
- (void)_logFailureSetDevicePositionFrom:(SCManagedCaptureDevicePosition)start
|
|
to:(SCManagedCaptureDevicePosition)end
|
|
reason:(NSString *)reason
|
|
{
|
|
SCTraceODPCompatibleStart(2);
|
|
SCLogCapturerInfo(@"Device position change failed: %@", reason);
|
|
[[SCLogger sharedInstance] logEvent:kSCCameraMetricsCameraFlipFailure
|
|
parameters:@{
|
|
@"start" : @(start),
|
|
@"end" : @(end),
|
|
@"reason" : reason,
|
|
}];
|
|
}
|
|
|
|
- (void)setFlashActive:(BOOL)flashActive
|
|
completionHandler:(dispatch_block_t)completionHandler
|
|
context:(NSString *)context
|
|
{
|
|
SCTraceODPCompatibleStart(2);
|
|
[_captureResource.queuePerformer perform:^{
|
|
SCTraceStart();
|
|
BOOL flashActiveOrFrontFlashEnabledChanged = NO;
|
|
if (_captureResource.state.flashActive != flashActive) {
|
|
[_captureResource.device setFlashActive:flashActive];
|
|
SCLogCapturerInfo(@"Set flash active: %d -> %d", _captureResource.state.flashActive, flashActive);
|
|
_captureResource.state = [[[SCManagedCapturerStateBuilder withManagedCapturerState:_captureResource.state]
|
|
setFlashActive:flashActive] build];
|
|
flashActiveOrFrontFlashEnabledChanged = YES;
|
|
}
|
|
SCManagedCapturerState *state = [_captureResource.state copy];
|
|
runOnMainThreadAsynchronously(^{
|
|
if (flashActiveOrFrontFlashEnabledChanged) {
|
|
[_captureResource.announcer managedCapturer:self didChangeState:state];
|
|
[_captureResource.announcer managedCapturer:self didChangeFlashActive:state];
|
|
}
|
|
if (completionHandler) {
|
|
completionHandler();
|
|
}
|
|
});
|
|
}];
|
|
}
|
|
|
|
- (void)setLensesActive:(BOOL)lensesActive
|
|
completionHandler:(dispatch_block_t)completionHandler
|
|
context:(NSString *)context
|
|
{
|
|
SCTraceODPCompatibleStart(2);
|
|
[self _setLensesActive:lensesActive
|
|
liveVideoStreaming:NO
|
|
filterFactory:nil
|
|
completionHandler:completionHandler
|
|
context:context];
|
|
}
|
|
|
|
- (void)setLensesActive:(BOOL)lensesActive
|
|
filterFactory:(SCLookseryFilterFactory *)filterFactory
|
|
completionHandler:(dispatch_block_t)completionHandler
|
|
context:(NSString *)context
|
|
{
|
|
[self _setLensesActive:lensesActive
|
|
liveVideoStreaming:NO
|
|
filterFactory:filterFactory
|
|
completionHandler:completionHandler
|
|
context:context];
|
|
}
|
|
|
|
- (void)setLensesInTalkActive:(BOOL)lensesActive
|
|
completionHandler:(dispatch_block_t)completionHandler
|
|
context:(NSString *)context
|
|
{
|
|
// Talk requires liveVideoStreaming to be turned on
|
|
BOOL liveVideoStreaming = lensesActive;
|
|
|
|
dispatch_block_t activationBlock = ^{
|
|
[self _setLensesActive:lensesActive
|
|
liveVideoStreaming:liveVideoStreaming
|
|
filterFactory:nil
|
|
completionHandler:completionHandler
|
|
context:context];
|
|
};
|
|
|
|
@weakify(self);
|
|
[_captureResource.queuePerformer perform:^{
|
|
@strongify(self);
|
|
SC_GUARD_ELSE_RETURN(self);
|
|
// If lenses are enabled in TV3 and it was enabled not from TV3. We have to turn off lenses off at first.
|
|
BOOL shouldTurnOffBeforeActivation = liveVideoStreaming && !self->_captureResource.state.liveVideoStreaming &&
|
|
self->_captureResource.state.lensesActive;
|
|
if (shouldTurnOffBeforeActivation) {
|
|
[self _setLensesActive:NO
|
|
liveVideoStreaming:NO
|
|
filterFactory:nil
|
|
completionHandler:activationBlock
|
|
context:context];
|
|
} else {
|
|
activationBlock();
|
|
}
|
|
}];
|
|
}
|
|
|
|
- (void)_setLensesActive:(BOOL)lensesActive
|
|
liveVideoStreaming:(BOOL)liveVideoStreaming
|
|
filterFactory:(SCLookseryFilterFactory *)filterFactory
|
|
completionHandler:(dispatch_block_t)completionHandler
|
|
context:(NSString *)context
|
|
{
|
|
SCTraceODPCompatibleStart(2);
|
|
SCLogCapturerInfo(@"Setting lenses active to: %d", lensesActive);
|
|
[_captureResource.queuePerformer perform:^{
|
|
SCTraceStart();
|
|
BOOL lensesActiveChanged = NO;
|
|
if (_captureResource.state.lensesActive != lensesActive) {
|
|
SCLogCapturerInfo(@"Set lenses active: %d -> %d", _captureResource.state.lensesActive, lensesActive);
|
|
_captureResource.state = [[[SCManagedCapturerStateBuilder withManagedCapturerState:_captureResource.state]
|
|
setLensesActive:lensesActive] build];
|
|
|
|
// Update capturer settings(orientation and resolution) after changing state, because
|
|
// _setLiveVideoStreaming logic is depends on it
|
|
[self _setLiveVideoStreaming:liveVideoStreaming];
|
|
|
|
[SCCaptureWorker turnARSessionOff:_captureResource];
|
|
|
|
// Only enable sample buffer display when lenses is not active.
|
|
[_captureResource.videoDataSource setSampleBufferDisplayEnabled:!lensesActive];
|
|
[_captureResource.debugInfoDict setObject:!lensesActive ? @"True" : @"False"
|
|
forKey:@"sampleBufferDisplayEnabled"];
|
|
|
|
lensesActiveChanged = YES;
|
|
[_captureResource.lensProcessingCore setAspectRatio:_captureResource.state.liveVideoStreaming];
|
|
[_captureResource.lensProcessingCore setLensesActive:_captureResource.state.lensesActive
|
|
videoOrientation:_captureResource.videoDataSource.videoOrientation
|
|
filterFactory:filterFactory];
|
|
BOOL modifySource = _captureResource.state.liveVideoStreaming || _captureResource.videoRecording;
|
|
[_captureResource.lensProcessingCore setModifySource:modifySource];
|
|
[_captureResource.lensProcessingCore setShouldMuteAllSounds:_captureResource.state.liveVideoStreaming];
|
|
if (_captureResource.fileInputDecider.shouldProcessFileInput) {
|
|
[_captureResource.lensProcessingCore setLensesActive:YES
|
|
videoOrientation:_captureResource.videoDataSource.videoOrientation
|
|
filterFactory:filterFactory];
|
|
}
|
|
[_captureResource.videoDataSource
|
|
setVideoStabilizationEnabledIfSupported:!_captureResource.state.lensesActive];
|
|
|
|
if (SCIsMasterBuild()) {
|
|
// Check that connection configuration is correct
|
|
if (_captureResource.state.lensesActive &&
|
|
_captureResource.state.devicePosition == SCManagedCaptureDevicePositionFront) {
|
|
for (AVCaptureOutput *output in _captureResource.managedSession.avSession.outputs) {
|
|
if ([output isKindOfClass:[AVCaptureVideoDataOutput class]]) {
|
|
AVCaptureConnection *connection = [output connectionWithMediaType:AVMediaTypeVideo];
|
|
SCAssert(connection.videoMirrored &&
|
|
connection.videoOrientation == !_captureResource.state.liveVideoStreaming
|
|
? AVCaptureVideoOrientationLandscapeRight
|
|
: AVCaptureVideoOrientationPortrait,
|
|
@"Connection configuration is not correct");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
dispatch_block_t viewChangeHandler = ^{
|
|
SCManagedCapturerState *state = [_captureResource.state copy]; // update to latest state always
|
|
runOnMainThreadAsynchronously(^{
|
|
[_captureResource.announcer managedCapturer:self didChangeState:state];
|
|
[_captureResource.announcer managedCapturer:self didChangeLensesActive:state];
|
|
[_captureResource.videoPreviewGLViewManager setLensesActive:state.lensesActive];
|
|
if (completionHandler) {
|
|
completionHandler();
|
|
}
|
|
});
|
|
};
|
|
if (lensesActiveChanged && !lensesActive && SCDeviceSupportsMetal()) {
|
|
// If we are turning off lenses and have sample buffer display on.
|
|
// We need to wait until new frame presented in sample buffer before
|
|
// dismiss the Lenses' OpenGL view.
|
|
[_captureResource.videoDataSource waitUntilSampleBufferDisplayed:_captureResource.queuePerformer.queue
|
|
completionHandler:viewChangeHandler];
|
|
} else {
|
|
viewChangeHandler();
|
|
}
|
|
}];
|
|
}
|
|
|
|
- (void)_setLiveVideoStreaming:(BOOL)liveVideoStreaming
|
|
{
|
|
SCAssertPerformer(_captureResource.queuePerformer);
|
|
BOOL enableLiveVideoStreaming = liveVideoStreaming;
|
|
if (!_captureResource.state.lensesActive && liveVideoStreaming) {
|
|
SCLogLensesError(@"LiveVideoStreaming is not allowed when lenses are turned off");
|
|
enableLiveVideoStreaming = NO;
|
|
}
|
|
SC_GUARD_ELSE_RETURN(enableLiveVideoStreaming != _captureResource.state.liveVideoStreaming);
|
|
|
|
// We will disable blackCameraNoOutputDetector if in live video streaming
|
|
// In case there is some black camera when doing video call, will consider re-enable it
|
|
[self _setBlackCameraNoOutputDetectorEnabled:!liveVideoStreaming];
|
|
|
|
if (!_captureResource.device.isConnected) {
|
|
SCLogCapturerError(@"Can't perform configuration for live video streaming");
|
|
}
|
|
SCLogCapturerInfo(@"Set live video streaming: %d -> %d", _captureResource.state.liveVideoStreaming,
|
|
enableLiveVideoStreaming);
|
|
_captureResource.state = [[[SCManagedCapturerStateBuilder withManagedCapturerState:_captureResource.state]
|
|
setLiveVideoStreaming:enableLiveVideoStreaming] build];
|
|
|
|
BOOL isStreaming = _captureResource.videoDataSource.isStreaming;
|
|
if (isStreaming) {
|
|
[_captureResource.videoDataSource stopStreaming];
|
|
}
|
|
|
|
SCLogCapturerInfo(@"Set live video streaming beginConfiguration");
|
|
[_captureResource.managedSession performConfiguration:^{
|
|
[_captureResource.videoDataSource beginConfiguration];
|
|
|
|
// If video chat is active we should use portrait orientation, otherwise landscape right
|
|
[_captureResource.videoDataSource setVideoOrientation:_captureResource.state.liveVideoStreaming
|
|
? AVCaptureVideoOrientationPortrait
|
|
: AVCaptureVideoOrientationLandscapeRight];
|
|
|
|
[_captureResource.device setLiveVideoStreaming:_captureResource.state.liveVideoStreaming
|
|
session:_captureResource.managedSession.avSession];
|
|
|
|
[_captureResource.videoDataSource commitConfiguration];
|
|
}];
|
|
|
|
SCLogCapturerInfo(@"Set live video streaming commitConfiguration");
|
|
|
|
if (isStreaming) {
|
|
[_captureResource.videoDataSource startStreaming];
|
|
}
|
|
}
|
|
|
|
- (void)_setBlackCameraNoOutputDetectorEnabled:(BOOL)enabled
|
|
{
|
|
if (enabled) {
|
|
[self addListener:_captureResource.blackCameraDetector.blackCameraNoOutputDetector];
|
|
[_captureResource.videoDataSource addListener:_captureResource.blackCameraDetector.blackCameraNoOutputDetector];
|
|
} else {
|
|
[self removeListener:_captureResource.blackCameraDetector.blackCameraNoOutputDetector];
|
|
[_captureResource.videoDataSource
|
|
removeListener:_captureResource.blackCameraDetector.blackCameraNoOutputDetector];
|
|
}
|
|
}
|
|
|
|
- (void)setTorchActiveAsynchronously:(BOOL)torchActive
|
|
completionHandler:(dispatch_block_t)completionHandler
|
|
context:(NSString *)context
|
|
{
|
|
SCTraceODPCompatibleStart(2);
|
|
SCLogCapturerInfo(@"Setting torch active asynchronously to: %d", torchActive);
|
|
[_captureResource.queuePerformer perform:^{
|
|
SCTraceStart();
|
|
BOOL torchActiveChanged = NO;
|
|
if (_captureResource.state.torchActive != torchActive) {
|
|
[_captureResource.device setTorchActive:torchActive];
|
|
if (_captureResource.state.devicePosition == SCManagedCaptureDevicePositionFront) {
|
|
_captureResource.frontFlashController.torchActive = torchActive;
|
|
}
|
|
SCLogCapturerInfo(@"Set torch active: %d -> %d", _captureResource.state.torchActive, torchActive);
|
|
_captureResource.state = [[[SCManagedCapturerStateBuilder withManagedCapturerState:_captureResource.state]
|
|
setTorchActive:torchActive] build];
|
|
torchActiveChanged = YES;
|
|
}
|
|
SCManagedCapturerState *state = [_captureResource.state copy];
|
|
runOnMainThreadAsynchronously(^{
|
|
if (torchActiveChanged) {
|
|
[_captureResource.announcer managedCapturer:self didChangeState:state];
|
|
}
|
|
if (completionHandler) {
|
|
completionHandler();
|
|
}
|
|
});
|
|
}];
|
|
}
|
|
|
|
- (void)setNightModeActiveAsynchronously:(BOOL)active
|
|
completionHandler:(dispatch_block_t)completionHandler
|
|
context:(NSString *)context
|
|
{
|
|
SCTraceODPCompatibleStart(2);
|
|
[_captureResource.queuePerformer perform:^{
|
|
SCTraceStart();
|
|
// Only do the configuration if current device is connected
|
|
if (_captureResource.device.isConnected) {
|
|
SCLogCapturerInfo(@"Set night mode beginConfiguration");
|
|
[_captureResource.managedSession performConfiguration:^{
|
|
[self _setNightModeActive:active];
|
|
[self _updateHRSIEnabled];
|
|
[self _updateStillImageStabilizationEnabled];
|
|
}];
|
|
SCLogCapturerInfo(@"Set night mode commitConfiguration");
|
|
}
|
|
BOOL nightModeChanged = (_captureResource.state.isNightModeActive != active);
|
|
if (nightModeChanged) {
|
|
SCLogCapturerInfo(@"Set night mode active: %d -> %d", _captureResource.state.isNightModeActive, active);
|
|
_captureResource.state = [[[SCManagedCapturerStateBuilder withManagedCapturerState:_captureResource.state]
|
|
setIsNightModeActive:active] build];
|
|
}
|
|
SCManagedCapturerState *state = [_captureResource.state copy];
|
|
runOnMainThreadAsynchronously(^{
|
|
if (nightModeChanged) {
|
|
[_captureResource.announcer managedCapturer:self didChangeState:state];
|
|
[_captureResource.announcer managedCapturer:self didChangeNightModeActive:state];
|
|
}
|
|
if (completionHandler) {
|
|
completionHandler();
|
|
}
|
|
});
|
|
}];
|
|
}
|
|
|
|
- (void)_setNightModeActive:(BOOL)active
|
|
{
|
|
SCTraceODPCompatibleStart(2);
|
|
[_captureResource.device setNightModeActive:active session:_captureResource.managedSession.avSession];
|
|
if ([SCManagedCaptureDevice isEnhancedNightModeSupported]) {
|
|
[self _toggleSoftwareNightmode:active];
|
|
}
|
|
}
|
|
|
|
- (void)_toggleSoftwareNightmode:(BOOL)active
|
|
{
|
|
SCTraceODPCompatibleStart(2);
|
|
if (active) {
|
|
SCLogCapturerInfo(@"Set enhanced night mode active");
|
|
SCProcessingPipelineBuilder *processingPipelineBuilder = [[SCProcessingPipelineBuilder alloc] init];
|
|
processingPipelineBuilder.enhancedNightMode = YES;
|
|
SCProcessingPipeline *pipeline = [processingPipelineBuilder build];
|
|
SCLogCapturerInfo(@"Adding processing pipeline:%@", pipeline);
|
|
[_captureResource.videoDataSource addProcessingPipeline:pipeline];
|
|
} else {
|
|
SCLogCapturerInfo(@"Removing processing pipeline");
|
|
[_captureResource.videoDataSource removeProcessingPipeline];
|
|
}
|
|
}
|
|
|
|
- (BOOL)_shouldCaptureImageFromVideo
|
|
{
|
|
SCTraceODPCompatibleStart(2);
|
|
BOOL isIphone5Series = [SCDeviceName isSimilarToIphone5orNewer] && ![SCDeviceName isSimilarToIphone6orNewer];
|
|
return isIphone5Series && !_captureResource.state.flashActive && ![self isLensApplied];
|
|
}
|
|
|
|
- (void)lockZoomWithContext:(NSString *)context
|
|
{
|
|
SCTraceODPCompatibleStart(2);
|
|
SCAssertMainThread();
|
|
SCLogCapturerInfo(@"Lock zoom");
|
|
_captureResource.allowsZoom = NO;
|
|
}
|
|
|
|
- (void)unlockZoomWithContext:(NSString *)context
|
|
{
|
|
SCTraceODPCompatibleStart(2);
|
|
SCAssertMainThread();
|
|
SCLogCapturerInfo(@"Unlock zoom");
|
|
// Don't let anyone unlock the zoom while ARKit is active. When ARKit shuts down, it'll unlock it.
|
|
SC_GUARD_ELSE_RETURN(!_captureResource.state.arSessionActive);
|
|
_captureResource.allowsZoom = YES;
|
|
}
|
|
|
|
- (void)setZoomFactorAsynchronously:(CGFloat)zoomFactor context:(NSString *)context
|
|
{
|
|
SCTraceODPCompatibleStart(2);
|
|
SCAssertMainThread();
|
|
SC_GUARD_ELSE_RETURN(_captureResource.allowsZoom);
|
|
SCLogCapturerInfo(@"Setting zoom factor to: %f", zoomFactor);
|
|
[_captureResource.deviceZoomHandler setZoomFactor:zoomFactor forDevice:_captureResource.device immediately:NO];
|
|
}
|
|
|
|
- (void)resetZoomFactorAsynchronously:(CGFloat)zoomFactor
|
|
devicePosition:(SCManagedCaptureDevicePosition)devicePosition
|
|
context:(NSString *)context
|
|
{
|
|
SCTraceODPCompatibleStart(2);
|
|
SCAssertMainThread();
|
|
SC_GUARD_ELSE_RETURN(_captureResource.allowsZoom);
|
|
SCLogCapturerInfo(@"Setting zoom factor to: %f devicePosition:%lu", zoomFactor, (unsigned long)devicePosition);
|
|
SCManagedCaptureDevice *device = [SCManagedCaptureDevice deviceWithPosition:devicePosition];
|
|
[_captureResource.deviceZoomHandler setZoomFactor:zoomFactor forDevice:device immediately:YES];
|
|
}
|
|
|
|
- (void)setExposurePointOfInterestAsynchronously:(CGPoint)pointOfInterest
|
|
fromUser:(BOOL)fromUser
|
|
completionHandler:(dispatch_block_t)completionHandler
|
|
context:(NSString *)context
|
|
{
|
|
SCTraceODPCompatibleStart(2);
|
|
[_captureResource.queuePerformer perform:^{
|
|
SCTraceStart();
|
|
if (_captureResource.device.isConnected) {
|
|
CGPoint exposurePoint;
|
|
if ([self isVideoMirrored]) {
|
|
exposurePoint = CGPointMake(pointOfInterest.x, 1 - pointOfInterest.y);
|
|
} else {
|
|
exposurePoint = pointOfInterest;
|
|
}
|
|
if (_captureResource.device.softwareZoom) {
|
|
// Fix for the zooming factor
|
|
[_captureResource.device
|
|
setExposurePointOfInterest:CGPointMake(
|
|
(exposurePoint.x - 0.5) / _captureResource.device.softwareZoom + 0.5,
|
|
(exposurePoint.y - 0.5) / _captureResource.device.softwareZoom + 0.5)
|
|
fromUser:fromUser];
|
|
} else {
|
|
[_captureResource.device setExposurePointOfInterest:exposurePoint fromUser:fromUser];
|
|
}
|
|
}
|
|
if (completionHandler) {
|
|
runOnMainThreadAsynchronously(completionHandler);
|
|
}
|
|
}];
|
|
}
|
|
|
|
- (void)setAutofocusPointOfInterestAsynchronously:(CGPoint)pointOfInterest
|
|
completionHandler:(dispatch_block_t)completionHandler
|
|
context:(NSString *)context
|
|
{
|
|
SCTraceODPCompatibleStart(2);
|
|
[_captureResource.queuePerformer perform:^{
|
|
SCTraceStart();
|
|
if (_captureResource.device.isConnected) {
|
|
CGPoint focusPoint;
|
|
if ([self isVideoMirrored]) {
|
|
focusPoint = CGPointMake(pointOfInterest.x, 1 - pointOfInterest.y);
|
|
} else {
|
|
focusPoint = pointOfInterest;
|
|
}
|
|
if (_captureResource.device.softwareZoom) {
|
|
// Fix for the zooming factor
|
|
[_captureResource.device
|
|
setAutofocusPointOfInterest:CGPointMake(
|
|
(focusPoint.x - 0.5) / _captureResource.device.softwareZoom + 0.5,
|
|
(focusPoint.y - 0.5) / _captureResource.device.softwareZoom + 0.5)];
|
|
} else {
|
|
[_captureResource.device setAutofocusPointOfInterest:focusPoint];
|
|
}
|
|
}
|
|
if (completionHandler) {
|
|
runOnMainThreadAsynchronously(completionHandler);
|
|
}
|
|
}];
|
|
}
|
|
|
|
- (void)setPortraitModePointOfInterestAsynchronously:(CGPoint)pointOfInterest
|
|
completionHandler:(dispatch_block_t)completionHandler
|
|
context:(NSString *)context
|
|
{
|
|
SCTraceODPCompatibleStart(2);
|
|
[SCCaptureWorker setPortraitModePointOfInterestAsynchronously:pointOfInterest
|
|
completionHandler:completionHandler
|
|
resource:_captureResource];
|
|
}
|
|
|
|
- (void)continuousAutofocusAndExposureAsynchronouslyWithCompletionHandler:(dispatch_block_t)completionHandler
|
|
context:(NSString *)context
|
|
{
|
|
SCTraceODPCompatibleStart(2);
|
|
[_captureResource.queuePerformer perform:^{
|
|
SCTraceStart();
|
|
if (_captureResource.device.isConnected) {
|
|
[_captureResource.device continuousAutofocus];
|
|
[_captureResource.device setExposurePointOfInterest:CGPointMake(0.5, 0.5) fromUser:NO];
|
|
if (SCCameraTweaksEnablePortraitModeAutofocus()) {
|
|
[self setPortraitModePointOfInterestAsynchronously:CGPointMake(0.5, 0.5)
|
|
completionHandler:nil
|
|
context:context];
|
|
}
|
|
}
|
|
if (completionHandler) {
|
|
runOnMainThreadAsynchronously(completionHandler);
|
|
}
|
|
}];
|
|
}
|
|
|
|
#pragma mark - Add / Remove Listener
|
|
|
|
- (void)addListener:(id<SCManagedCapturerListener>)listener
|
|
{
|
|
SCTraceODPCompatibleStart(2);
|
|
// Only do the make sure thing if I added it to announcer fresh.
|
|
SC_GUARD_ELSE_RETURN([_captureResource.announcer addListener:listener]);
|
|
// After added the listener, make sure we called all these methods with its
|
|
// initial values
|
|
[_captureResource.queuePerformer perform:^{
|
|
SCManagedCapturerState *state = [_captureResource.state copy];
|
|
AVCaptureVideoPreviewLayer *videoPreviewLayer = _captureResource.videoPreviewLayer;
|
|
LSAGLView *videoPreviewGLView = _captureResource.videoPreviewGLViewManager.view;
|
|
runOnMainThreadAsynchronously(^{
|
|
SCTraceStart();
|
|
if ([listener respondsToSelector:@selector(managedCapturer:didChangeState:)]) {
|
|
[listener managedCapturer:self didChangeState:state];
|
|
}
|
|
if ([listener respondsToSelector:@selector(managedCapturer:didChangeCaptureDevicePosition:)]) {
|
|
[listener managedCapturer:self didChangeCaptureDevicePosition:state];
|
|
}
|
|
if ([listener respondsToSelector:@selector(managedCapturer:didChangeNightModeActive:)]) {
|
|
[listener managedCapturer:self didChangeNightModeActive:state];
|
|
}
|
|
if ([listener respondsToSelector:@selector(managedCapturer:didChangeFlashActive:)]) {
|
|
[listener managedCapturer:self didChangeFlashActive:state];
|
|
}
|
|
if ([listener respondsToSelector:@selector(managedCapturer:didChangeFlashSupportedAndTorchSupported:)]) {
|
|
[listener managedCapturer:self didChangeFlashSupportedAndTorchSupported:state];
|
|
}
|
|
if ([listener respondsToSelector:@selector(managedCapturer:didChangeZoomFactor:)]) {
|
|
[listener managedCapturer:self didChangeZoomFactor:state];
|
|
}
|
|
if ([listener respondsToSelector:@selector(managedCapturer:didChangeLowLightCondition:)]) {
|
|
[listener managedCapturer:self didChangeLowLightCondition:state];
|
|
}
|
|
if ([listener respondsToSelector:@selector(managedCapturer:didChangeAdjustingExposure:)]) {
|
|
[listener managedCapturer:self didChangeAdjustingExposure:state];
|
|
}
|
|
if (!SCDeviceSupportsMetal()) {
|
|
if ([listener respondsToSelector:@selector(managedCapturer:didChangeVideoPreviewLayer:)]) {
|
|
[listener managedCapturer:self didChangeVideoPreviewLayer:videoPreviewLayer];
|
|
}
|
|
}
|
|
if (videoPreviewGLView &&
|
|
[listener respondsToSelector:@selector(managedCapturer:didChangeVideoPreviewGLView:)]) {
|
|
[listener managedCapturer:self didChangeVideoPreviewGLView:videoPreviewGLView];
|
|
}
|
|
if ([listener respondsToSelector:@selector(managedCapturer:didChangeLensesActive:)]) {
|
|
[listener managedCapturer:self didChangeLensesActive:state];
|
|
}
|
|
});
|
|
}];
|
|
}
|
|
|
|
- (void)removeListener:(id<SCManagedCapturerListener>)listener
|
|
{
|
|
SCTraceODPCompatibleStart(2);
|
|
[_captureResource.announcer removeListener:listener];
|
|
}
|
|
|
|
- (void)addVideoDataSourceListener:(id<SCManagedVideoDataSourceListener>)listener
|
|
{
|
|
SCTraceODPCompatibleStart(2);
|
|
[_captureResource.videoDataSource addListener:listener];
|
|
}
|
|
|
|
- (void)removeVideoDataSourceListener:(id<SCManagedVideoDataSourceListener>)listener
|
|
{
|
|
SCTraceODPCompatibleStart(2);
|
|
[_captureResource.videoDataSource removeListener:listener];
|
|
}
|
|
|
|
- (void)addDeviceCapacityAnalyzerListener:(id<SCManagedDeviceCapacityAnalyzerListener>)listener
|
|
{
|
|
SCTraceODPCompatibleStart(2);
|
|
[_captureResource.deviceCapacityAnalyzer addListener:listener];
|
|
}
|
|
|
|
- (void)removeDeviceCapacityAnalyzerListener:(id<SCManagedDeviceCapacityAnalyzerListener>)listener
|
|
{
|
|
SCTraceODPCompatibleStart(2);
|
|
[_captureResource.deviceCapacityAnalyzer removeListener:listener];
|
|
}
|
|
|
|
#pragma mark - Debug
|
|
|
|
- (NSString *)debugInfo
|
|
{
|
|
SCTraceODPCompatibleStart(2);
|
|
NSMutableString *info = [NSMutableString new];
|
|
[info appendString:@"==== SCManagedCapturer tokens ====\n"];
|
|
[_captureResource.tokenSet enumerateObjectsUsingBlock:^(SCCapturerToken *_Nonnull token, BOOL *_Nonnull stop) {
|
|
[info appendFormat:@"%@\n", token.debugDescription];
|
|
}];
|
|
return info.copy;
|
|
}
|
|
|
|
- (NSString *)description
|
|
{
|
|
return [self debugDescription];
|
|
}
|
|
|
|
- (NSString *)debugDescription
|
|
{
|
|
return [NSString sc_stringWithFormat:@"SCManagedCapturer state:\n%@\nVideo streamer info:\n%@",
|
|
_captureResource.state.debugDescription,
|
|
_captureResource.videoDataSource.description];
|
|
}
|
|
|
|
- (CMTime)firstWrittenAudioBufferDelay
|
|
{
|
|
SCTraceODPCompatibleStart(2);
|
|
return [SCCaptureWorker firstWrittenAudioBufferDelay:_captureResource];
|
|
}
|
|
|
|
- (BOOL)audioQueueStarted
|
|
{
|
|
SCTraceODPCompatibleStart(2);
|
|
return [SCCaptureWorker audioQueueStarted:_captureResource];
|
|
}
|
|
|
|
#pragma mark - SCTimeProfilable
|
|
|
|
+ (SCTimeProfilerContext)context
|
|
{
|
|
return SCTimeProfilerContextCamera;
|
|
}
|
|
|
|
// We disable and re-enable liveness timer when enter background and foreground
|
|
|
|
- (void)applicationDidEnterBackground
|
|
{
|
|
SCTraceODPCompatibleStart(2);
|
|
[SCCaptureWorker destroyLivenessConsistencyTimer:_captureResource];
|
|
// Hide the view when in background.
|
|
if (!SCDeviceSupportsMetal()) {
|
|
[_captureResource.queuePerformer perform:^{
|
|
_captureResource.appInBackground = YES;
|
|
[CATransaction begin];
|
|
[CATransaction setDisableActions:YES];
|
|
_captureResource.videoPreviewLayer.hidden = YES;
|
|
[CATransaction commit];
|
|
}];
|
|
} else {
|
|
[_captureResource.queuePerformer perform:^{
|
|
_captureResource.appInBackground = YES;
|
|
// If it is running, stop the streaming.
|
|
if (_captureResource.status == SCManagedCapturerStatusRunning) {
|
|
[_captureResource.videoDataSource stopStreaming];
|
|
}
|
|
}];
|
|
}
|
|
[[SCManagedCapturePreviewLayerController sharedInstance] applicationDidEnterBackground];
|
|
}
|
|
|
|
- (void)applicationWillEnterForeground
|
|
{
|
|
SCTraceODPCompatibleStart(2);
|
|
if (!SCDeviceSupportsMetal()) {
|
|
[_captureResource.queuePerformer perform:^{
|
|
SCTraceStart();
|
|
_captureResource.appInBackground = NO;
|
|
|
|
if (!SCDeviceSupportsMetal()) {
|
|
[self _fixNonMetalSessionPreviewInconsistency];
|
|
}
|
|
|
|
// Doing this right now on iOS 10. It will probably work on iOS 9 as well, but need to verify.
|
|
if (SC_AT_LEAST_IOS_10) {
|
|
[self _runningConsistencyCheckAndFix];
|
|
// For OS version >= iOS 10, try to fix AVCaptureSession when app is entering foreground.
|
|
_captureResource.numRetriesFixAVCaptureSessionWithCurrentSession = 0;
|
|
[self _fixAVSessionIfNecessary];
|
|
}
|
|
}];
|
|
} else {
|
|
[_captureResource.queuePerformer perform:^{
|
|
SCTraceStart();
|
|
_captureResource.appInBackground = NO;
|
|
if (_captureResource.status == SCManagedCapturerStatusRunning) {
|
|
[_captureResource.videoDataSource startStreaming];
|
|
}
|
|
// Doing this right now on iOS 10. It will probably work on iOS 9 as well, but need to verify.
|
|
if (SC_AT_LEAST_IOS_10) {
|
|
[self _runningConsistencyCheckAndFix];
|
|
// For OS version >= iOS 10, try to fix AVCaptureSession when app is entering foreground.
|
|
_captureResource.numRetriesFixAVCaptureSessionWithCurrentSession = 0;
|
|
[self _fixAVSessionIfNecessary];
|
|
}
|
|
}];
|
|
}
|
|
[[SCManagedCapturePreviewLayerController sharedInstance] applicationWillEnterForeground];
|
|
}
|
|
|
|
- (void)applicationWillResignActive
|
|
{
|
|
SCTraceODPCompatibleStart(2);
|
|
[[SCManagedCapturePreviewLayerController sharedInstance] applicationWillResignActive];
|
|
[_captureResource.queuePerformer perform:^{
|
|
[self _pauseCaptureSessionKVOCheck];
|
|
}];
|
|
}
|
|
|
|
- (void)applicationDidBecomeActive
|
|
{
|
|
SCTraceODPCompatibleStart(2);
|
|
[[SCManagedCapturePreviewLayerController sharedInstance] applicationDidBecomeActive];
|
|
[_captureResource.queuePerformer perform:^{
|
|
SCTraceStart();
|
|
// Since we foreground it, do the running consistency check immediately.
|
|
// Reset number of retries for fixing status inconsistency
|
|
_captureResource.numRetriesFixInconsistencyWithCurrentSession = 0;
|
|
[self _runningConsistencyCheckAndFix];
|
|
if (!SC_AT_LEAST_IOS_10) {
|
|
// For OS version < iOS 10, try to fix AVCaptureSession after app becomes active.
|
|
_captureResource.numRetriesFixAVCaptureSessionWithCurrentSession = 0;
|
|
[self _fixAVSessionIfNecessary];
|
|
}
|
|
[self _resumeCaptureSessionKVOCheck];
|
|
if (_captureResource.status == SCManagedCapturerStatusRunning) {
|
|
// Reschedule the timer if we don't have it already
|
|
runOnMainThreadAsynchronously(^{
|
|
SCTraceStart();
|
|
[SCCaptureWorker setupLivenessConsistencyTimerIfForeground:_captureResource];
|
|
});
|
|
}
|
|
}];
|
|
}
|
|
|
|
- (void)_runningConsistencyCheckAndFix
|
|
{
|
|
SCTraceODPCompatibleStart(2);
|
|
// Don't enforce consistency on simulator, as it'll constantly false-positive and restart session.
|
|
SC_GUARD_ELSE_RETURN(![SCDeviceName isSimulator]);
|
|
if (_captureResource.state.arSessionActive) {
|
|
[self _runningARSessionConsistencyCheckAndFix];
|
|
} else {
|
|
[self _runningAVCaptureSessionConsistencyCheckAndFix];
|
|
}
|
|
}
|
|
|
|
- (void)_runningARSessionConsistencyCheckAndFix
|
|
{
|
|
SCTraceODPCompatibleStart(2);
|
|
SCAssert([_captureResource.queuePerformer isCurrentPerformer], @"");
|
|
SCAssert(_captureResource.state.arSessionActive, @"");
|
|
if (@available(iOS 11.0, *)) {
|
|
// Occassionally the capture session will get into a weird "stuck" state.
|
|
// If this happens, we'll see that the timestamp for the most recent frame is behind the current time.
|
|
// Pausinging the session for a moment and restarting to attempt to jog it loose.
|
|
NSTimeInterval timeSinceLastFrame = CACurrentMediaTime() - _captureResource.arSession.currentFrame.timestamp;
|
|
BOOL reset = NO;
|
|
if (_captureResource.arSession.currentFrame.camera.trackingStateReason == ARTrackingStateReasonInitializing) {
|
|
if (timeSinceLastFrame > kSCManagedCapturerFixInconsistencyARSessionHungInitThreshold) {
|
|
SCLogCapturerInfo(@"*** Found inconsistency for ARSession timestamp (possible hung init), fix now ***");
|
|
reset = YES;
|
|
}
|
|
} else if (timeSinceLastFrame > kSCManagedCapturerFixInconsistencyARSessionDelayThreshold) {
|
|
SCLogCapturerInfo(@"*** Found inconsistency for ARSession timestamp (init complete), fix now ***");
|
|
reset = YES;
|
|
}
|
|
if (reset) {
|
|
[SCCaptureWorker turnARSessionOff:_captureResource];
|
|
[SCCaptureWorker turnARSessionOn:_captureResource];
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void)_runningAVCaptureSessionConsistencyCheckAndFix
|
|
{
|
|
SCTraceODPCompatibleStart(2);
|
|
SCAssert([_captureResource.queuePerformer isCurrentPerformer], @"");
|
|
SCAssert(!_captureResource.state.arSessionActive, @"");
|
|
[[SCLogger sharedInstance] logStepToEvent:@"CAMERA_OPEN_WITH_FIX_INCONSISTENCY"
|
|
uniqueId:@""
|
|
stepName:@"startConsistencyCheckAndFix"];
|
|
// If the video preview layer's hidden status is out of sync with the
|
|
// session's running status,
|
|
// fix that now. Also, we don't care that much if the status is not running.
|
|
if (!SCDeviceSupportsMetal()) {
|
|
[self _fixNonMetalSessionPreviewInconsistency];
|
|
}
|
|
// Skip the liveness consistency check if we are in background
|
|
if (_captureResource.appInBackground) {
|
|
SCLogCapturerInfo(@"*** Skipped liveness consistency check, as we are in the background ***");
|
|
return;
|
|
}
|
|
if (_captureResource.status == SCManagedCapturerStatusRunning && !_captureResource.managedSession.isRunning) {
|
|
SCGhostToSnappableSignalCameraFixInconsistency();
|
|
SCLogCapturerInfo(@"*** Found status inconsistency for running, fix now ***");
|
|
_captureResource.numRetriesFixInconsistencyWithCurrentSession++;
|
|
if (_captureResource.numRetriesFixInconsistencyWithCurrentSession <=
|
|
kSCManagedCapturerFixInconsistencyMaxRetriesWithCurrentSession) {
|
|
SCTraceStartSection("Fix non-running session")
|
|
{
|
|
if (!SCDeviceSupportsMetal()) {
|
|
[CATransaction begin];
|
|
[CATransaction setDisableActions:YES];
|
|
[_captureResource.managedSession startRunning];
|
|
[SCCaptureWorker setupVideoPreviewLayer:_captureResource];
|
|
[CATransaction commit];
|
|
} else {
|
|
[_captureResource.managedSession startRunning];
|
|
}
|
|
}
|
|
SCTraceEndSection();
|
|
} else {
|
|
SCTraceStartSection("Create new capturer session")
|
|
{
|
|
// start running with new capture session if the inconsistency fixing not succeeds
|
|
// after kSCManagedCapturerFixInconsistencyMaxRetriesWithCurrentSession retries
|
|
SCLogCapturerInfo(@"*** Recreate and run new capture session to fix the inconsistency ***");
|
|
[self _startRunningWithNewCaptureSession];
|
|
}
|
|
SCTraceEndSection();
|
|
}
|
|
BOOL sessionIsRunning = _captureResource.managedSession.isRunning;
|
|
if (sessionIsRunning && !SCDeviceSupportsMetal()) {
|
|
// If it is fixed, we signal received the first frame.
|
|
SCGhostToSnappableSignalDidReceiveFirstPreviewFrame();
|
|
runOnMainThreadAsynchronously(^{
|
|
// To approximate this did render timer, it is not accurate.
|
|
SCGhostToSnappableSignalDidRenderFirstPreviewFrame(CACurrentMediaTime());
|
|
});
|
|
}
|
|
SCLogCapturerInfo(@"*** Applied inconsistency fix, running state : %@ ***", sessionIsRunning ? @"YES" : @"NO");
|
|
if (_captureResource.managedSession.isRunning) {
|
|
[[SCLogger sharedInstance] logStepToEvent:@"CAMERA_OPEN_WITH_FIX_INCONSISTENCY"
|
|
uniqueId:@""
|
|
stepName:@"finishConsistencyCheckAndFix"];
|
|
[[SCLogger sharedInstance]
|
|
logTimedEventEnd:@"CAMERA_OPEN_WITH_FIX_INCONSISTENCY"
|
|
uniqueId:@""
|
|
parameters:@{
|
|
@"count" : @(_captureResource.numRetriesFixInconsistencyWithCurrentSession)
|
|
}];
|
|
}
|
|
} else {
|
|
[[SCLogger sharedInstance] cancelLogTimedEvent:@"CAMERA_OPEN_WITH_FIX_INCONSISTENCY" uniqueId:@""];
|
|
// Reset number of retries for fixing status inconsistency
|
|
_captureResource.numRetriesFixInconsistencyWithCurrentSession = 0;
|
|
}
|
|
|
|
[_captureResource.blackCameraDetector sessionDidChangeIsRunning:_captureResource.managedSession.isRunning];
|
|
}
|
|
|
|
- (void)mediaServicesWereReset
|
|
{
|
|
SCTraceODPCompatibleStart(2);
|
|
[self mediaServicesWereLost];
|
|
[_captureResource.queuePerformer perform:^{
|
|
/* If the current state requires the ARSession, restart it.
|
|
Explicitly flip the arSessionActive flag so that `turnSessionOn` thinks it can reset itself.
|
|
*/
|
|
if (_captureResource.state.arSessionActive) {
|
|
_captureResource.state = [[[SCManagedCapturerStateBuilder withManagedCapturerState:_captureResource.state]
|
|
setArSessionActive:NO] build];
|
|
[SCCaptureWorker turnARSessionOn:_captureResource];
|
|
}
|
|
}];
|
|
}
|
|
|
|
- (void)mediaServicesWereLost
|
|
{
|
|
SCTraceODPCompatibleStart(2);
|
|
[_captureResource.queuePerformer perform:^{
|
|
if (!_captureResource.state.arSessionActive && !_captureResource.managedSession.isRunning) {
|
|
/*
|
|
If the session is running we will trigger
|
|
_sessionRuntimeError: so nothing else is
|
|
needed here.
|
|
*/
|
|
[_captureResource.videoCapturer.outputURL reloadAssetKeys];
|
|
}
|
|
}];
|
|
}
|
|
|
|
- (void)_livenessConsistency:(NSTimer *)timer
|
|
{
|
|
SCTraceODPCompatibleStart(2);
|
|
SCAssertMainThread();
|
|
// We can directly check the application state because this timer is scheduled
|
|
// on the main thread.
|
|
if ([UIApplication sharedApplication].applicationState == UIApplicationStateActive) {
|
|
[_captureResource.queuePerformer perform:^{
|
|
[self _runningConsistencyCheckAndFix];
|
|
}];
|
|
}
|
|
}
|
|
|
|
- (void)_sessionRuntimeError:(NSNotification *)notification
|
|
{
|
|
SCTraceODPCompatibleStart(2);
|
|
NSError *sessionError = notification.userInfo[AVCaptureSessionErrorKey];
|
|
SCLogCapturerError(@"Encountered runtime error for capture session %@", sessionError);
|
|
|
|
NSString *errorString =
|
|
[sessionError.description stringByReplacingOccurrencesOfString:@" " withString:@"_"].uppercaseString
|
|
?: @"UNKNOWN_ERROR";
|
|
[[SCUserTraceLogger shared]
|
|
logUserTraceEvent:[NSString sc_stringWithFormat:@"AVCAPTURESESSION_RUNTIME_ERROR_%@", errorString]];
|
|
|
|
if (sessionError.code == AVErrorMediaServicesWereReset) {
|
|
// If it is a AVErrorMediaServicesWereReset error, we can just call startRunning, it is much light weighted
|
|
[_captureResource.queuePerformer perform:^{
|
|
if (!SCDeviceSupportsMetal()) {
|
|
[CATransaction begin];
|
|
[CATransaction setDisableActions:YES];
|
|
[_captureResource.managedSession startRunning];
|
|
[SCCaptureWorker setupVideoPreviewLayer:_captureResource];
|
|
[CATransaction commit];
|
|
} else {
|
|
[_captureResource.managedSession startRunning];
|
|
}
|
|
}];
|
|
} else {
|
|
if (_captureResource.isRecreateSessionFixScheduled) {
|
|
SCLogCoreCameraInfo(@"Fixing session runtime error is scheduled, skip");
|
|
return;
|
|
}
|
|
|
|
_captureResource.isRecreateSessionFixScheduled = YES;
|
|
NSTimeInterval delay = 0;
|
|
NSTimeInterval timeNow = [NSDate timeIntervalSinceReferenceDate];
|
|
if (timeNow - _captureResource.lastSessionRuntimeErrorTime < kMinFixSessionRuntimeErrorInterval) {
|
|
SCLogCoreCameraInfo(@"Fixing runtime error session in less than %f, delay",
|
|
kMinFixSessionRuntimeErrorInterval);
|
|
delay = kMinFixSessionRuntimeErrorInterval;
|
|
}
|
|
_captureResource.lastSessionRuntimeErrorTime = timeNow;
|
|
[_captureResource.queuePerformer perform:^{
|
|
SCTraceStart();
|
|
// Occasionaly _captureResource.avSession will throw out an error when shutting down. If this happens while
|
|
// ARKit is starting up,
|
|
// _startRunningWithNewCaptureSession will throw a wrench in ARSession startup and freeze the image.
|
|
SC_GUARD_ELSE_RETURN(!_captureResource.state.arSessionActive);
|
|
// Need to reset the flag before _startRunningWithNewCaptureSession
|
|
_captureResource.isRecreateSessionFixScheduled = NO;
|
|
[self _startRunningWithNewCaptureSession];
|
|
[self _fixAVSessionIfNecessary];
|
|
}
|
|
after:delay];
|
|
}
|
|
|
|
[[SCLogger sharedInstance] logUnsampledEvent:kSCCameraMetricsRuntimeError
|
|
parameters:@{
|
|
@"error" : sessionError == nil ? @"Unknown error" : sessionError.description,
|
|
}
|
|
secretParameters:nil
|
|
metrics:nil];
|
|
}
|
|
|
|
- (void)_startRunningWithNewCaptureSessionIfNecessary
|
|
{
|
|
SCTraceODPCompatibleStart(2);
|
|
if (_captureResource.isRecreateSessionFixScheduled) {
|
|
SCLogCapturerInfo(@"Session recreation is scheduled, return");
|
|
return;
|
|
}
|
|
_captureResource.isRecreateSessionFixScheduled = YES;
|
|
[_captureResource.queuePerformer perform:^{
|
|
// Need to reset the flag before _startRunningWithNewCaptureSession
|
|
_captureResource.isRecreateSessionFixScheduled = NO;
|
|
[self _startRunningWithNewCaptureSession];
|
|
}];
|
|
}
|
|
|
|
- (void)_startRunningWithNewCaptureSession
|
|
{
|
|
SCTraceODPCompatibleStart(2);
|
|
SCAssert([_captureResource.queuePerformer isCurrentPerformer], @"");
|
|
SCLogCapturerInfo(@"Start running with new capture session. isRecording:%d isStreaming:%d status:%lu",
|
|
_captureResource.videoRecording, _captureResource.videoDataSource.isStreaming,
|
|
(unsigned long)_captureResource.status);
|
|
|
|
// Mark the start of recreating session
|
|
[_captureResource.blackCameraDetector sessionWillRecreate];
|
|
|
|
// Light weight fix gating
|
|
BOOL lightWeightFix = SCCameraTweaksSessionLightWeightFixEnabled() || SCCameraTweaksBlackCameraRecoveryEnabled();
|
|
|
|
if (!lightWeightFix) {
|
|
[_captureResource.deviceCapacityAnalyzer removeListener:_captureResource.stillImageCapturer];
|
|
[self removeListener:_captureResource.stillImageCapturer];
|
|
[_captureResource.videoDataSource removeListener:_captureResource.lensProcessingCore.capturerListener];
|
|
|
|
[_captureResource.videoDataSource removeListener:_captureResource.deviceCapacityAnalyzer];
|
|
[_captureResource.videoDataSource removeListener:_captureResource.stillImageCapturer];
|
|
|
|
if (SCIsMasterBuild()) {
|
|
[_captureResource.videoDataSource removeListener:_captureResource.videoStreamReporter];
|
|
}
|
|
[_captureResource.videoDataSource removeListener:_captureResource.videoScanner];
|
|
[_captureResource.videoDataSource removeListener:_captureResource.videoCapturer];
|
|
[_captureResource.videoDataSource
|
|
removeListener:_captureResource.blackCameraDetector.blackCameraNoOutputDetector];
|
|
}
|
|
|
|
[_captureResource.videoCapturer.outputURL reloadAssetKeys];
|
|
|
|
BOOL isStreaming = _captureResource.videoDataSource.isStreaming;
|
|
if (_captureResource.videoRecording) {
|
|
// Stop video recording prematurely
|
|
[self stopRecordingAsynchronouslyWithContext:SCCapturerContext];
|
|
NSError *error = [NSError
|
|
errorWithDomain:kSCManagedCapturerErrorDomain
|
|
description:
|
|
[NSString
|
|
sc_stringWithFormat:@"Interrupt video recording to start new session. %@",
|
|
@{
|
|
@"isAVSessionRunning" : @(_captureResource.managedSession.isRunning),
|
|
@"numRetriesFixInconsistency" :
|
|
@(_captureResource.numRetriesFixInconsistencyWithCurrentSession),
|
|
@"numRetriesFixAVCaptureSession" :
|
|
@(_captureResource.numRetriesFixAVCaptureSessionWithCurrentSession),
|
|
@"lastSessionRuntimeErrorTime" :
|
|
@(_captureResource.lastSessionRuntimeErrorTime),
|
|
}]
|
|
code:-1];
|
|
[[SCLogger sharedInstance] logUnsampledEvent:kSCCameraMetricsVideoRecordingInterrupted
|
|
parameters:@{
|
|
@"error" : error.description
|
|
}
|
|
secretParameters:nil
|
|
metrics:nil];
|
|
}
|
|
@try {
|
|
if (@available(iOS 11.0, *)) {
|
|
[_captureResource.arSession pause];
|
|
if (!lightWeightFix) {
|
|
[_captureResource.videoDataSource removeListener:_captureResource.arImageCapturer];
|
|
}
|
|
}
|
|
[_captureResource.managedSession stopRunning];
|
|
[_captureResource.device removeDeviceAsInput:_captureResource.managedSession.avSession];
|
|
} @catch (NSException *exception) {
|
|
SCLogCapturerError(@"Encountered Exception %@", exception);
|
|
} @finally {
|
|
// Nil out device inputs from both devices
|
|
[[SCManagedCaptureDevice front] resetDeviceAsInput];
|
|
[[SCManagedCaptureDevice back] resetDeviceAsInput];
|
|
}
|
|
|
|
if (!SCDeviceSupportsMetal()) {
|
|
// Redo the video preview to mitigate https://ph.sc-corp.net/T42584
|
|
[SCCaptureWorker redoVideoPreviewLayer:_captureResource];
|
|
}
|
|
|
|
#if !TARGET_IPHONE_SIMULATOR
|
|
if (@available(iOS 11.0, *)) {
|
|
_captureResource.arSession = [[ARSession alloc] init];
|
|
_captureResource.arImageCapturer =
|
|
[_captureResource.arImageCaptureProvider arImageCapturerWith:_captureResource.queuePerformer
|
|
lensProcessingCore:_captureResource.lensProcessingCore];
|
|
}
|
|
[self _resetAVCaptureSession];
|
|
#endif
|
|
[_captureResource.managedSession.avSession setAutomaticallyConfiguresApplicationAudioSession:NO];
|
|
[_captureResource.device setDeviceAsInput:_captureResource.managedSession.avSession];
|
|
|
|
if (_captureResource.fileInputDecider.shouldProcessFileInput) {
|
|
// Keep the same logic, always create new VideoDataSource
|
|
[self _setupNewVideoFileDataSource];
|
|
} else {
|
|
if (!lightWeightFix) {
|
|
[self _setupNewVideoDataSource];
|
|
} else {
|
|
[self _setupVideoDataSourceWithNewSession];
|
|
}
|
|
}
|
|
|
|
if (_captureResource.status == SCManagedCapturerStatusRunning) {
|
|
if (!SCDeviceSupportsMetal()) {
|
|
[CATransaction begin];
|
|
[CATransaction setDisableActions:YES];
|
|
// Set the session to be the new session before start running.
|
|
_captureResource.videoPreviewLayer.session = _captureResource.managedSession.avSession;
|
|
if (!_captureResource.appInBackground) {
|
|
[_captureResource.managedSession startRunning];
|
|
}
|
|
[SCCaptureWorker setupVideoPreviewLayer:_captureResource];
|
|
[CATransaction commit];
|
|
} else {
|
|
if (!_captureResource.appInBackground) {
|
|
[_captureResource.managedSession startRunning];
|
|
}
|
|
}
|
|
}
|
|
// Since this start and stop happens in one block, we don't have to worry
|
|
// about streamingSequence issues
|
|
if (isStreaming) {
|
|
[_captureResource.videoDataSource startStreaming];
|
|
}
|
|
SCManagedCapturerState *state = [_captureResource.state copy];
|
|
AVCaptureVideoPreviewLayer *videoPreviewLayer = _captureResource.videoPreviewLayer;
|
|
runOnMainThreadAsynchronously(^{
|
|
[_captureResource.announcer managedCapturer:self didResetFromRuntimeError:state];
|
|
if (!SCDeviceSupportsMetal()) {
|
|
[_captureResource.announcer managedCapturer:self didChangeVideoPreviewLayer:videoPreviewLayer];
|
|
}
|
|
});
|
|
|
|
// Mark the end of recreating session
|
|
[_captureResource.blackCameraDetector sessionDidRecreate];
|
|
}
|
|
|
|
/**
|
|
* Heavy-weight session fixing approach: recreating everything
|
|
*/
|
|
- (void)_setupNewVideoDataSource
|
|
{
|
|
if (@available(iOS 11.0, *)) {
|
|
_captureResource.videoDataSource =
|
|
[[SCManagedVideoStreamer alloc] initWithSession:_captureResource.managedSession.avSession
|
|
arSession:_captureResource.arSession
|
|
devicePosition:_captureResource.state.devicePosition];
|
|
[_captureResource.videoDataSource addListener:_captureResource.arImageCapturer];
|
|
if (_captureResource.state.isPortraitModeActive) {
|
|
[_captureResource.videoDataSource setDepthCaptureEnabled:YES];
|
|
|
|
SCProcessingPipelineBuilder *processingPipelineBuilder = [[SCProcessingPipelineBuilder alloc] init];
|
|
processingPipelineBuilder.portraitModeEnabled = YES;
|
|
SCProcessingPipeline *pipeline = [processingPipelineBuilder build];
|
|
[_captureResource.videoDataSource addProcessingPipeline:pipeline];
|
|
}
|
|
} else {
|
|
_captureResource.videoDataSource =
|
|
[[SCManagedVideoStreamer alloc] initWithSession:_captureResource.managedSession.avSession
|
|
devicePosition:_captureResource.state.devicePosition];
|
|
}
|
|
|
|
[self _setupVideoDataSourceListeners];
|
|
}
|
|
|
|
- (void)_setupNewVideoFileDataSource
|
|
{
|
|
_captureResource.videoDataSource =
|
|
[[SCManagedVideoFileStreamer alloc] initWithPlaybackForURL:_captureResource.fileInputDecider.fileURL];
|
|
[_captureResource.lensProcessingCore setLensesActive:YES
|
|
videoOrientation:_captureResource.videoDataSource.videoOrientation
|
|
filterFactory:nil];
|
|
runOnMainThreadAsynchronously(^{
|
|
[_captureResource.videoPreviewGLViewManager prepareViewIfNecessary];
|
|
});
|
|
[self _setupVideoDataSourceListeners];
|
|
}
|
|
|
|
/**
|
|
* Light-weight session fixing approach: recreating AVCaptureSession / AVCaptureOutput, and bind it to the new session
|
|
*/
|
|
- (void)_setupVideoDataSourceWithNewSession
|
|
{
|
|
if (@available(iOS 11.0, *)) {
|
|
SCManagedVideoStreamer *streamer = (SCManagedVideoStreamer *)_captureResource.videoDataSource;
|
|
[streamer setupWithSession:_captureResource.managedSession.avSession
|
|
devicePosition:_captureResource.state.devicePosition];
|
|
[streamer setupWithARSession:_captureResource.arSession];
|
|
} else {
|
|
SCManagedVideoStreamer *streamer = (SCManagedVideoStreamer *)_captureResource.videoDataSource;
|
|
[streamer setupWithSession:_captureResource.managedSession.avSession
|
|
devicePosition:_captureResource.state.devicePosition];
|
|
}
|
|
[_captureResource.stillImageCapturer setupWithSession:_captureResource.managedSession.avSession];
|
|
}
|
|
|
|
- (void)_setupVideoDataSourceListeners
|
|
{
|
|
if (_captureResource.videoFrameSampler) {
|
|
[_captureResource.announcer addListener:_captureResource.videoFrameSampler];
|
|
}
|
|
|
|
[_captureResource.videoDataSource addSampleBufferDisplayController:_captureResource.sampleBufferDisplayController];
|
|
[_captureResource.videoDataSource addListener:_captureResource.lensProcessingCore.capturerListener];
|
|
[_captureResource.videoDataSource addListener:_captureResource.deviceCapacityAnalyzer];
|
|
if (SCIsMasterBuild()) {
|
|
[_captureResource.videoDataSource addListener:_captureResource.videoStreamReporter];
|
|
}
|
|
[_captureResource.videoDataSource addListener:_captureResource.videoScanner];
|
|
[_captureResource.videoDataSource addListener:_captureResource.blackCameraDetector.blackCameraNoOutputDetector];
|
|
_captureResource.stillImageCapturer = [SCManagedStillImageCapturer capturerWithCaptureResource:_captureResource];
|
|
[_captureResource.deviceCapacityAnalyzer addListener:_captureResource.stillImageCapturer];
|
|
[_captureResource.videoDataSource addListener:_captureResource.stillImageCapturer];
|
|
|
|
[self addListener:_captureResource.stillImageCapturer];
|
|
}
|
|
|
|
- (void)_resetAVCaptureSession
|
|
{
|
|
SCTraceODPCompatibleStart(2);
|
|
SCAssert([_captureResource.queuePerformer isCurrentPerformer], @"");
|
|
_captureResource.numRetriesFixAVCaptureSessionWithCurrentSession = 0;
|
|
// lazily initialize _captureResource.kvoController on background thread
|
|
if (!_captureResource.kvoController) {
|
|
_captureResource.kvoController = [[FBKVOController alloc] initWithObserver:self];
|
|
}
|
|
[_captureResource.kvoController unobserve:_captureResource.managedSession.avSession];
|
|
_captureResource.managedSession =
|
|
[[SCManagedCaptureSession alloc] initWithBlackCameraDetector:_captureResource.blackCameraDetector];
|
|
[_captureResource.kvoController observe:_captureResource.managedSession.avSession
|
|
keyPath:@keypath(_captureResource.managedSession.avSession, running)
|
|
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
|
|
action:_captureResource.handleAVSessionStatusChange];
|
|
}
|
|
|
|
- (void)_pauseCaptureSessionKVOCheck
|
|
{
|
|
SCTraceODPCompatibleStart(2);
|
|
SCAssert([_captureResource.queuePerformer isCurrentPerformer], @"");
|
|
[_captureResource.kvoController unobserve:_captureResource.managedSession.avSession];
|
|
}
|
|
|
|
- (void)_resumeCaptureSessionKVOCheck
|
|
{
|
|
SCTraceODPCompatibleStart(2);
|
|
SCAssert([_captureResource.queuePerformer isCurrentPerformer], @"");
|
|
[_captureResource.kvoController observe:_captureResource.managedSession.avSession
|
|
keyPath:@keypath(_captureResource.managedSession.avSession, running)
|
|
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
|
|
action:_captureResource.handleAVSessionStatusChange];
|
|
}
|
|
|
|
- (id<SCManagedVideoDataSource>)currentVideoDataSource
|
|
{
|
|
SCTraceODPCompatibleStart(2);
|
|
return _captureResource.videoDataSource;
|
|
}
|
|
|
|
- (void)checkRestrictedCamera:(void (^)(BOOL, BOOL, AVAuthorizationStatus))callback
|
|
{
|
|
SCTraceODPCompatibleStart(2);
|
|
[_captureResource.queuePerformer perform:^{
|
|
// Front and back should be available if user has no restriction on camera.
|
|
BOOL front = [[SCManagedCaptureDevice front] isAvailable];
|
|
BOOL back = [[SCManagedCaptureDevice back] isAvailable];
|
|
AVAuthorizationStatus status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];
|
|
runOnMainThreadAsynchronously(^{
|
|
callback(front, back, status);
|
|
});
|
|
}];
|
|
}
|
|
|
|
- (SCSnapCreationTriggers *)snapCreationTriggers
|
|
{
|
|
return _captureResource.snapCreationTriggers;
|
|
}
|
|
|
|
- (void)setBlackCameraDetector:(SCBlackCameraDetector *)blackCameraDetector
|
|
deviceMotionProvider:(id<SCDeviceMotionProvider>)deviceMotionProvider
|
|
fileInputDecider:(id<SCFileInputDecider>)fileInputDecider
|
|
arImageCaptureProvider:(id<SCManagedCapturerARImageCaptureProvider>)arImageCaptureProvider
|
|
glviewManager:(id<SCManagedCapturerGLViewManagerAPI>)glViewManager
|
|
lensAPIProvider:(id<SCManagedCapturerLensAPIProvider>)lensAPIProvider
|
|
lsaComponentTracker:(id<SCManagedCapturerLSAComponentTrackerAPI>)lsaComponentTracker
|
|
managedCapturerPreviewLayerControllerDelegate:
|
|
(id<SCManagedCapturePreviewLayerControllerDelegate>)previewLayerControllerDelegate
|
|
{
|
|
_captureResource.blackCameraDetector = blackCameraDetector;
|
|
_captureResource.deviceMotionProvider = deviceMotionProvider;
|
|
_captureResource.fileInputDecider = fileInputDecider;
|
|
_captureResource.arImageCaptureProvider = arImageCaptureProvider;
|
|
_captureResource.videoPreviewGLViewManager = glViewManager;
|
|
[_captureResource.videoPreviewGLViewManager configureWithCaptureResource:_captureResource];
|
|
_captureResource.lensAPIProvider = lensAPIProvider;
|
|
_captureResource.lsaTrackingComponentHandler = lsaComponentTracker;
|
|
[_captureResource.lsaTrackingComponentHandler configureWithCaptureResource:_captureResource];
|
|
_captureResource.previewLayerControllerDelegate = previewLayerControllerDelegate;
|
|
[SCManagedCapturePreviewLayerController sharedInstance].delegate = previewLayerControllerDelegate;
|
|
}
|
|
|
|
@end
|