Jonny Banana
6 years ago
committed by
GitHub
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
84 changed files with 7048 additions and 0 deletions
-
17ManagedCapturer/ARConfiguration+SCConfiguration.h
-
36ManagedCapturer/ARConfiguration+SCConfiguration.m
-
15ManagedCapturer/AVCaptureConnection+InputDevice.h
-
25ManagedCapturer/AVCaptureConnection+InputDevice.m
-
34ManagedCapturer/AVCaptureDevice+ConfigurationLock.h
-
47ManagedCapturer/AVCaptureDevice+ConfigurationLock.m
-
21ManagedCapturer/NSURL+Asset.h
-
23ManagedCapturer/NSURL+Asset.m
-
10ManagedCapturer/OWNERS
-
39ManagedCapturer/SCAudioCaptureSession.h
-
289ManagedCapturer/SCAudioCaptureSession.m
-
23ManagedCapturer/SCCameraSettingUtils.h
-
79ManagedCapturer/SCCameraSettingUtils.m
-
74ManagedCapturer/SCCaptureCommon.h
-
31ManagedCapturer/SCCaptureCommon.m
-
22ManagedCapturer/SCCaptureCoreImageFaceDetector.h
-
205ManagedCapturer/SCCaptureCoreImageFaceDetector.m
-
24ManagedCapturer/SCCaptureDeviceAuthorization.h
-
71ManagedCapturer/SCCaptureDeviceAuthorization.m
-
31ManagedCapturer/SCCaptureDeviceAuthorizationChecker.h
-
71ManagedCapturer/SCCaptureDeviceAuthorizationChecker.m
-
31ManagedCapturer/SCCaptureDeviceResolver.h
-
147ManagedCapturer/SCCaptureDeviceResolver.m
-
43ManagedCapturer/SCCaptureFaceDetectionParser.h
-
94ManagedCapturer/SCCaptureFaceDetectionParser.m
-
31ManagedCapturer/SCCaptureFaceDetector.h
-
22ManagedCapturer/SCCaptureFaceDetectorTrigger.h
-
97ManagedCapturer/SCCaptureFaceDetectorTrigger.m
-
23ManagedCapturer/SCCaptureMetadataObjectParser.h
-
38ManagedCapturer/SCCaptureMetadataObjectParser.m
-
19ManagedCapturer/SCCaptureMetadataOutputDetector.h
-
175ManagedCapturer/SCCaptureMetadataOutputDetector.m
-
225ManagedCapturer/SCCapturer.h
-
44ManagedCapturer/SCCapturerBufferedVideoWriter.h
-
430ManagedCapturer/SCCapturerBufferedVideoWriter.m
-
20ManagedCapturer/SCCapturerDefines.h
-
18ManagedCapturer/SCCapturerToken.h
-
30ManagedCapturer/SCCapturerToken.m
-
20ManagedCapturer/SCCapturerTokenProvider.h
-
42ManagedCapturer/SCCapturerTokenProvider.m
-
18ManagedCapturer/SCExposureState.h
-
47ManagedCapturer/SCExposureState.m
-
19ManagedCapturer/SCFileAudioCaptureSession.h
-
243ManagedCapturer/SCFileAudioCaptureSession.m
-
20ManagedCapturer/SCManagedAudioStreamer.h
-
115ManagedCapturer/SCManagedAudioStreamer.m
-
71ManagedCapturer/SCManagedCaptureDevice+SCManagedCapturer.h
-
17ManagedCapturer/SCManagedCaptureDevice+SCManagedDeviceCapacityAnalyzer.h
-
60ManagedCapturer/SCManagedCaptureDevice.h
-
821ManagedCapturer/SCManagedCaptureDevice.m
-
17ManagedCapturer/SCManagedCaptureDeviceAutoExposureHandler.h
-
63ManagedCapturer/SCManagedCaptureDeviceAutoExposureHandler.m
-
18ManagedCapturer/SCManagedCaptureDeviceAutoFocusHandler.h
-
131ManagedCapturer/SCManagedCaptureDeviceAutoFocusHandler.m
-
25ManagedCapturer/SCManagedCaptureDeviceDefaultZoomHandler.h
-
93ManagedCapturer/SCManagedCaptureDeviceDefaultZoomHandler.m
-
17ManagedCapturer/SCManagedCaptureDeviceDefaultZoomHandler_Private.h
-
22ManagedCapturer/SCManagedCaptureDeviceExposureHandler.h
-
28ManagedCapturer/SCManagedCaptureDeviceFaceDetectionAutoExposureHandler.h
-
121ManagedCapturer/SCManagedCaptureDeviceFaceDetectionAutoExposureHandler.m
-
28ManagedCapturer/SCManagedCaptureDeviceFaceDetectionAutoFocusHandler.h
-
153ManagedCapturer/SCManagedCaptureDeviceFaceDetectionAutoFocusHandler.m
-
28ManagedCapturer/SCManagedCaptureDeviceFocusHandler.h
-
23ManagedCapturer/SCManagedCaptureDeviceHandler.h
-
77ManagedCapturer/SCManagedCaptureDeviceHandler.m
-
12ManagedCapturer/SCManagedCaptureDeviceLinearInterpolationZoomHandler.h
-
190ManagedCapturer/SCManagedCaptureDeviceLinearInterpolationZoomHandler.m
-
20ManagedCapturer/SCManagedCaptureDeviceLockOnRecordExposureHandler.h
-
90ManagedCapturer/SCManagedCaptureDeviceLockOnRecordExposureHandler.m
-
13ManagedCapturer/SCManagedCaptureDeviceSavitzkyGolayZoomHandler.h
-
95ManagedCapturer/SCManagedCaptureDeviceSavitzkyGolayZoomHandler.m
-
23ManagedCapturer/SCManagedCaptureDeviceSubjectAreaHandler.h
-
67ManagedCapturer/SCManagedCaptureDeviceSubjectAreaHandler.m
-
19ManagedCapturer/SCManagedCaptureDeviceThresholdExposureHandler.h
-
133ManagedCapturer/SCManagedCaptureDeviceThresholdExposureHandler.m
-
61ManagedCapturer/SCManagedCaptureFaceDetectionAdjustingPOIResource.h
-
232ManagedCapturer/SCManagedCaptureFaceDetectionAdjustingPOIResource.m
-
80ManagedCapturer/SCManagedCapturePreviewLayerController.h
-
563ManagedCapturer/SCManagedCapturePreviewLayerController.m
-
25ManagedCapturer/SCManagedCapturePreviewView.h
-
173ManagedCapturer/SCManagedCapturePreviewView.m
-
14ManagedCapturer/SCManagedCapturePreviewViewDebugView.h
-
204ManagedCapturer/SCManagedCapturePreviewViewDebugView.m
-
23ManagedCapturer/SCManagedCapturer.h
@ -0,0 +1,17 @@ |
|||||
|
// |
||||
|
// ARConfiguration+SCConfiguration.h |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by Max Goedjen on 11/7/17. |
||||
|
// |
||||
|
|
||||
|
#import "SCManagedCaptureDevice.h" |
||||
|
|
||||
|
#import <ARKit/ARKit.h> |
||||
|
|
||||
|
@interface ARConfiguration (SCConfiguration) |
||||
|
|
||||
|
+ (BOOL)sc_supportedForDevicePosition:(SCManagedCaptureDevicePosition)position; |
||||
|
+ (ARConfiguration *_Nullable)sc_configurationForDevicePosition:(SCManagedCaptureDevicePosition)position; |
||||
|
|
||||
|
@end |
@ -0,0 +1,36 @@ |
|||||
|
// |
||||
|
// ARConfiguration+SCConfiguration.m |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by Max Goedjen on 11/7/17. |
||||
|
// |
||||
|
|
||||
|
#import "ARConfiguration+SCConfiguration.h" |
||||
|
|
||||
|
#import "SCCapturerDefines.h" |
||||
|
|
||||
|
@implementation ARConfiguration (SCConfiguration) |
||||
|
|
||||
|
+ (BOOL)sc_supportedForDevicePosition:(SCManagedCaptureDevicePosition)position |
||||
|
{ |
||||
|
return [[[self sc_configurationForDevicePosition:position] class] isSupported]; |
||||
|
} |
||||
|
|
||||
|
+ (ARConfiguration *)sc_configurationForDevicePosition:(SCManagedCaptureDevicePosition)position |
||||
|
{ |
||||
|
if (@available(iOS 11.0, *)) { |
||||
|
if (position == SCManagedCaptureDevicePositionBack) { |
||||
|
ARWorldTrackingConfiguration *config = [[ARWorldTrackingConfiguration alloc] init]; |
||||
|
config.planeDetection = ARPlaneDetectionHorizontal; |
||||
|
config.lightEstimationEnabled = NO; |
||||
|
return config; |
||||
|
} else { |
||||
|
#ifdef SC_USE_ARKIT_FACE |
||||
|
return [[ARFaceTrackingConfiguration alloc] init]; |
||||
|
#endif |
||||
|
} |
||||
|
} |
||||
|
return nil; |
||||
|
} |
||||
|
|
||||
|
@end |
@ -0,0 +1,15 @@ |
|||||
|
// |
||||
|
// AVCaptureConnection+InputDevice.h |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by William Morriss on 1/20/15 |
||||
|
// Copyright (c) 2015 Snapchat, Inc. All rights reserved. |
||||
|
// |
||||
|
|
||||
|
#import <AVFoundation/AVFoundation.h> |
||||
|
|
||||
|
@interface AVCaptureConnection (InputDevice) |
||||
|
|
||||
|
- (AVCaptureDevice *)inputDevice; |
||||
|
|
||||
|
@end |
@ -0,0 +1,25 @@ |
|||||
|
// |
||||
|
// AVCaptureConnection+InputDevice.m |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by William Morriss on 1/20/15 |
||||
|
// Copyright (c) 2015 Snapchat, Inc. All rights reserved. |
||||
|
// |
||||
|
|
||||
|
#import "AVCaptureConnection+InputDevice.h" |
||||
|
|
||||
|
#import <SCFoundation/SCAssertWrapper.h> |
||||
|
|
||||
|
@implementation AVCaptureConnection (InputDevice) |
||||
|
|
||||
|
- (AVCaptureDevice *)inputDevice |
||||
|
{ |
||||
|
NSArray *inputPorts = self.inputPorts; |
||||
|
AVCaptureInputPort *port = [inputPorts firstObject]; |
||||
|
SCAssert([port.input isKindOfClass:[AVCaptureDeviceInput class]], @"unexpected port"); |
||||
|
AVCaptureDeviceInput *deviceInput = (AVCaptureDeviceInput *)port.input; |
||||
|
AVCaptureDevice *device = deviceInput.device; |
||||
|
return device; |
||||
|
} |
||||
|
|
||||
|
@end |
@ -0,0 +1,34 @@ |
|||||
|
// |
||||
|
// AVCaptureDevice+ConfigurationLock.h |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by Derek Peirce on 4/19/17. |
||||
|
// Copyright © 2017 Snapchat, Inc. All rights reserved. |
||||
|
// |
||||
|
|
||||
|
#import <AVFoundation/AVFoundation.h> |
||||
|
#import <Foundation/Foundation.h> |
||||
|
|
||||
|
@interface AVCaptureDevice (ConfigurationLock) |
||||
|
|
||||
|
/* |
||||
|
The following method will lock this AVCaptureDevice, run the task, then unlock the device. |
||||
|
The task is usually related to set AVCaptureDevice. |
||||
|
It will return a boolean telling you whether or not your task ran successfully. You can use the boolean to adjust your |
||||
|
strategy to handle this failure. For some cases, we don't have a good mechanism to handle the failure. E.g. if we want |
||||
|
to re-focus, but failed to do so. What is next step? Pop up a alert view to user? If yes, it is intrusive, if not, user |
||||
|
will get confused. Just because the error handling is difficulty, we would like to notify you if the task fails. |
||||
|
If the task does not run successfully. We will log an event using SCLogger for better visibility. |
||||
|
*/ |
||||
|
- (BOOL)runTask:(NSString *)taskName withLockedConfiguration:(void (^)(void))task; |
||||
|
|
||||
|
/* |
||||
|
The following method has the same function as the above one. |
||||
|
The difference is that it retries the operation for certain times. Please give a number below or equal 2. |
||||
|
When retry equals 0, we will only try to lock for once. |
||||
|
When retry equals 1, we will retry once if the 1st try fails. |
||||
|
.... |
||||
|
*/ |
||||
|
- (BOOL)runTask:(NSString *)taskName withLockedConfiguration:(void (^)(void))task retry:(NSUInteger)retryTimes; |
||||
|
|
||||
|
@end |
@ -0,0 +1,47 @@ |
|||||
|
// |
||||
|
// AVCaptureDevice+ConfigurationLock.m |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by Derek Peirce on 4/19/17. |
||||
|
// Copyright © 2017 Snapchat, Inc. All rights reserved. |
||||
|
// |
||||
|
|
||||
|
#import "AVCaptureDevice+ConfigurationLock.h" |
||||
|
|
||||
|
#import "SCLogger+Camera.h" |
||||
|
|
||||
|
#import <SCFoundation/SCAssertWrapper.h> |
||||
|
#import <SCFoundation/SCLog.h> |
||||
|
#import <SCLogger/SCLogger.h> |
||||
|
|
||||
|
@implementation AVCaptureDevice (ConfigurationLock) |
||||
|
|
||||
|
- (BOOL)runTask:(NSString *)taskName withLockedConfiguration:(void (^)(void))task |
||||
|
{ |
||||
|
return [self runTask:taskName withLockedConfiguration:task retry:0]; |
||||
|
} |
||||
|
|
||||
|
- (BOOL)runTask:(NSString *)taskName withLockedConfiguration:(void (^)(void))task retry:(NSUInteger)retryTimes |
||||
|
{ |
||||
|
SCAssert(taskName, @"camera logger taskString should not be empty"); |
||||
|
SCAssert(retryTimes <= 2 && retryTimes >= 0, @"retry times should be equal to or below 2."); |
||||
|
NSError *error = nil; |
||||
|
BOOL deviceLockSuccess = NO; |
||||
|
NSUInteger retryCounter = 0; |
||||
|
while (retryCounter <= retryTimes && !deviceLockSuccess) { |
||||
|
deviceLockSuccess = [self lockForConfiguration:&error]; |
||||
|
retryCounter++; |
||||
|
} |
||||
|
if (deviceLockSuccess) { |
||||
|
task(); |
||||
|
[self unlockForConfiguration]; |
||||
|
SCLogCoreCameraInfo(@"AVCapture Device setting success, task:%@ tryCount:%zu", taskName, |
||||
|
(unsigned long)retryCounter); |
||||
|
} else { |
||||
|
SCLogCoreCameraError(@"AVCapture Device Encountered error when %@ %@", taskName, error); |
||||
|
[[SCLogger sharedInstance] logManagedCapturerSettingFailure:taskName error:error]; |
||||
|
} |
||||
|
return deviceLockSuccess; |
||||
|
} |
||||
|
|
||||
|
@end |
@ -0,0 +1,21 @@ |
|||||
|
// |
||||
|
// NSURL+NSURL_Asset.h |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by Michel Loenngren on 4/30/17. |
||||
|
// Copyright © 2017 Snapchat, Inc. All rights reserved. |
||||
|
// |
||||
|
|
||||
|
#import <Foundation/Foundation.h> |
||||
|
|
||||
|
@interface NSURL (Asset) |
||||
|
|
||||
|
/** |
||||
|
In case the media server is reset while recording AVFoundation |
||||
|
gets in a weird state. Even though we reload our AVFoundation |
||||
|
object we still need to reload the assetkeys on the |
||||
|
outputfile. If we don't the AVAssetWriter will fail when started. |
||||
|
*/ |
||||
|
- (void)reloadAssetKeys; |
||||
|
|
||||
|
@end |
@ -0,0 +1,23 @@ |
|||||
|
// |
||||
|
// NSURL+NSURL_Asset.m |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by Michel Loenngren on 4/30/17. |
||||
|
// Copyright © 2017 Snapchat, Inc. All rights reserved. |
||||
|
// |
||||
|
|
||||
|
#import "NSURL+Asset.h" |
||||
|
|
||||
|
#import <SCBase/SCMacros.h> |
||||
|
|
||||
|
@import AVFoundation; |
||||
|
|
||||
|
@implementation NSURL (Asset) |
||||
|
|
||||
|
- (void)reloadAssetKeys |
||||
|
{ |
||||
|
AVAsset *videoAsset = [AVAsset assetWithURL:self]; |
||||
|
[videoAsset loadValuesAsynchronouslyForKeys:@[ @keypath(videoAsset.duration) ] completionHandler:nil]; |
||||
|
} |
||||
|
|
||||
|
@end |
@ -0,0 +1,10 @@ |
|||||
|
--- !OWNERS |
||||
|
|
||||
|
version: 2 |
||||
|
|
||||
|
default: |
||||
|
jira_project: CCAM |
||||
|
owners: |
||||
|
num_required_reviewers: 0 |
||||
|
teams: |
||||
|
- Snapchat/core-camera-ios |
@ -0,0 +1,39 @@ |
|||||
|
// |
||||
|
// SCAudioCaptureSession.h |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by Liu Liu on 3/5/15. |
||||
|
// Copyright (c) 2015 Snapchat, Inc. All rights reserved. |
||||
|
// |
||||
|
|
||||
|
#import <CoreMedia/CoreMedia.h> |
||||
|
#import <Foundation/Foundation.h> |
||||
|
|
||||
|
extern double const kSCAudioCaptureSessionDefaultSampleRate; |
||||
|
|
||||
|
typedef void (^audio_capture_session_block)(NSError *error); |
||||
|
|
||||
|
@protocol SCAudioCaptureSession; |
||||
|
|
||||
|
@protocol SCAudioCaptureSessionDelegate <NSObject> |
||||
|
|
||||
|
- (void)audioCaptureSession:(id<SCAudioCaptureSession>)audioCaptureSession |
||||
|
didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer; |
||||
|
|
||||
|
@end |
||||
|
|
||||
|
@protocol SCAudioCaptureSession <NSObject> |
||||
|
|
||||
|
@property (nonatomic, weak) id<SCAudioCaptureSessionDelegate> delegate; |
||||
|
|
||||
|
// Return detail informantions dictionary if error occured, else return nil |
||||
|
- (void)beginAudioRecordingAsynchronouslyWithSampleRate:(double)sampleRate |
||||
|
completionHandler:(audio_capture_session_block)completionHandler; |
||||
|
|
||||
|
- (void)disposeAudioRecordingSynchronouslyWithCompletionHandler:(dispatch_block_t)completionHandler; |
||||
|
|
||||
|
@end |
||||
|
|
||||
|
@interface SCAudioCaptureSession : NSObject <SCAudioCaptureSession> |
||||
|
|
||||
|
@end |
@ -0,0 +1,289 @@ |
|||||
|
// |
||||
|
// SCAudioCaptureSession.m |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by Liu Liu on 3/5/15. |
||||
|
// Copyright (c) 2015 Snapchat, Inc. All rights reserved. |
||||
|
// |
||||
|
|
||||
|
#import "SCAudioCaptureSession.h" |
||||
|
|
||||
|
#import <SCAudio/SCAudioSession.h> |
||||
|
#import <SCFoundation/SCLog.h> |
||||
|
#import <SCFoundation/SCQueuePerformer.h> |
||||
|
#import <SCFoundation/SCTrace.h> |
||||
|
|
||||
|
#import <mach/mach.h> |
||||
|
#import <mach/mach_time.h> |
||||
|
|
||||
|
@import AVFoundation; |
||||
|
|
||||
|
double const kSCAudioCaptureSessionDefaultSampleRate = 44100; |
||||
|
NSString *const SCAudioCaptureSessionErrorDomain = @"SCAudioCaptureSessionErrorDomain"; |
||||
|
|
||||
|
static NSInteger const kNumberOfAudioBuffersInQueue = 15; |
||||
|
static float const kAudioBufferDurationInSeconds = 0.2; |
||||
|
|
||||
|
static char *const kSCAudioCaptureSessionQueueLabel = "com.snapchat.audio-capture-session"; |
||||
|
|
||||
|
@implementation SCAudioCaptureSession { |
||||
|
SCQueuePerformer *_performer; |
||||
|
|
||||
|
AudioQueueRef _audioQueue; |
||||
|
AudioQueueBufferRef _audioQueueBuffers[kNumberOfAudioBuffersInQueue]; |
||||
|
CMAudioFormatDescriptionRef _audioFormatDescription; |
||||
|
} |
||||
|
|
||||
|
@synthesize delegate = _delegate; |
||||
|
|
||||
|
- (instancetype)init |
||||
|
{ |
||||
|
SCTraceStart(); |
||||
|
self = [super init]; |
||||
|
if (self) { |
||||
|
_performer = [[SCQueuePerformer alloc] initWithLabel:kSCAudioCaptureSessionQueueLabel |
||||
|
qualityOfService:QOS_CLASS_USER_INTERACTIVE |
||||
|
queueType:DISPATCH_QUEUE_SERIAL |
||||
|
context:SCQueuePerformerContextCamera]; |
||||
|
} |
||||
|
return self; |
||||
|
} |
||||
|
|
||||
|
- (void)dealloc |
||||
|
{ |
||||
|
[self disposeAudioRecordingSynchronouslyWithCompletionHandler:NULL]; |
||||
|
} |
||||
|
|
||||
|
static AudioStreamBasicDescription setupAudioFormat(UInt32 inFormatID, Float64 sampleRate) |
||||
|
{ |
||||
|
SCTraceStart(); |
||||
|
AudioStreamBasicDescription recordFormat = {0}; |
||||
|
|
||||
|
recordFormat.mSampleRate = sampleRate; |
||||
|
recordFormat.mChannelsPerFrame = (UInt32)[SCAudioSession sharedInstance].inputNumberOfChannels; |
||||
|
|
||||
|
recordFormat.mFormatID = inFormatID; |
||||
|
if (inFormatID == kAudioFormatLinearPCM) { |
||||
|
// if we want pcm, default to signed 16-bit little-endian |
||||
|
recordFormat.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger | kLinearPCMFormatFlagIsPacked; |
||||
|
recordFormat.mBitsPerChannel = 16; |
||||
|
recordFormat.mBytesPerPacket = recordFormat.mBytesPerFrame = |
||||
|
(recordFormat.mBitsPerChannel / 8) * recordFormat.mChannelsPerFrame; |
||||
|
recordFormat.mFramesPerPacket = 1; |
||||
|
} |
||||
|
return recordFormat; |
||||
|
} |
||||
|
|
||||
|
static int computeRecordBufferSize(const AudioStreamBasicDescription *format, const AudioQueueRef audioQueue, |
||||
|
float seconds) |
||||
|
{ |
||||
|
SCTraceStart(); |
||||
|
int packets, frames, bytes = 0; |
||||
|
frames = (int)ceil(seconds * format->mSampleRate); |
||||
|
|
||||
|
if (format->mBytesPerFrame > 0) { |
||||
|
bytes = frames * format->mBytesPerFrame; |
||||
|
} else { |
||||
|
UInt32 maxPacketSize; |
||||
|
if (format->mBytesPerPacket > 0) |
||||
|
maxPacketSize = format->mBytesPerPacket; // constant packet size |
||||
|
else { |
||||
|
UInt32 propertySize = sizeof(maxPacketSize); |
||||
|
AudioQueueGetProperty(audioQueue, kAudioQueueProperty_MaximumOutputPacketSize, &maxPacketSize, |
||||
|
&propertySize); |
||||
|
} |
||||
|
if (format->mFramesPerPacket > 0) |
||||
|
packets = frames / format->mFramesPerPacket; |
||||
|
else |
||||
|
packets = frames; // worst-case scenario: 1 frame in a packet |
||||
|
if (packets == 0) // sanity check |
||||
|
packets = 1; |
||||
|
bytes = packets * maxPacketSize; |
||||
|
} |
||||
|
return bytes; |
||||
|
} |
||||
|
|
||||
|
static NSTimeInterval machHostTimeToSeconds(UInt64 mHostTime) |
||||
|
{ |
||||
|
static dispatch_once_t onceToken; |
||||
|
static mach_timebase_info_data_t timebase_info; |
||||
|
dispatch_once(&onceToken, ^{ |
||||
|
(void)mach_timebase_info(&timebase_info); |
||||
|
}); |
||||
|
return (double)mHostTime * timebase_info.numer / timebase_info.denom / NSEC_PER_SEC; |
||||
|
} |
||||
|
|
||||
|
static void audioQueueBufferHandler(void *inUserData, AudioQueueRef inAQ, AudioQueueBufferRef inBuffer, |
||||
|
const AudioTimeStamp *nStartTime, UInt32 inNumPackets, |
||||
|
const AudioStreamPacketDescription *inPacketDesc) |
||||
|
{ |
||||
|
SCTraceStart(); |
||||
|
SCAudioCaptureSession *audioCaptureSession = (__bridge SCAudioCaptureSession *)inUserData; |
||||
|
if (inNumPackets > 0) { |
||||
|
CMTime PTS = CMTimeMakeWithSeconds(machHostTimeToSeconds(nStartTime->mHostTime), 600); |
||||
|
[audioCaptureSession appendAudioQueueBuffer:inBuffer |
||||
|
numPackets:inNumPackets |
||||
|
PTS:PTS |
||||
|
packetDescriptions:inPacketDesc]; |
||||
|
} |
||||
|
|
||||
|
AudioQueueEnqueueBuffer(inAQ, inBuffer, 0, NULL); |
||||
|
} |
||||
|
|
||||
|
- (void)appendAudioQueueBuffer:(AudioQueueBufferRef)audioQueueBuffer |
||||
|
numPackets:(UInt32)numPackets |
||||
|
PTS:(CMTime)PTS |
||||
|
packetDescriptions:(const AudioStreamPacketDescription *)packetDescriptions |
||||
|
{ |
||||
|
SCTraceStart(); |
||||
|
CMBlockBufferRef dataBuffer = NULL; |
||||
|
CMBlockBufferCreateWithMemoryBlock(NULL, NULL, audioQueueBuffer->mAudioDataByteSize, NULL, NULL, 0, |
||||
|
audioQueueBuffer->mAudioDataByteSize, 0, &dataBuffer); |
||||
|
if (dataBuffer) { |
||||
|
CMBlockBufferReplaceDataBytes(audioQueueBuffer->mAudioData, dataBuffer, 0, |
||||
|
audioQueueBuffer->mAudioDataByteSize); |
||||
|
CMSampleBufferRef sampleBuffer = NULL; |
||||
|
CMAudioSampleBufferCreateWithPacketDescriptions(NULL, dataBuffer, true, NULL, NULL, _audioFormatDescription, |
||||
|
numPackets, PTS, packetDescriptions, &sampleBuffer); |
||||
|
if (sampleBuffer) { |
||||
|
[self processAudioSampleBuffer:sampleBuffer]; |
||||
|
CFRelease(sampleBuffer); |
||||
|
} |
||||
|
CFRelease(dataBuffer); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
- (void)processAudioSampleBuffer:(CMSampleBufferRef)sampleBuffer |
||||
|
{ |
||||
|
SCTraceStart(); |
||||
|
[_delegate audioCaptureSession:self didOutputSampleBuffer:sampleBuffer]; |
||||
|
} |
||||
|
|
||||
|
- (NSError *)_generateErrorForType:(NSString *)errorType |
||||
|
errorCode:(int)errorCode |
||||
|
format:(AudioStreamBasicDescription)format |
||||
|
{ |
||||
|
NSDictionary *errorInfo = @{ |
||||
|
@"error_type" : errorType, |
||||
|
@"error_code" : @(errorCode), |
||||
|
@"record_format" : @{ |
||||
|
@"format_id" : @(format.mFormatID), |
||||
|
@"format_flags" : @(format.mFormatFlags), |
||||
|
@"sample_rate" : @(format.mSampleRate), |
||||
|
@"bytes_per_packet" : @(format.mBytesPerPacket), |
||||
|
@"frames_per_packet" : @(format.mFramesPerPacket), |
||||
|
@"bytes_per_frame" : @(format.mBytesPerFrame), |
||||
|
@"channels_per_frame" : @(format.mChannelsPerFrame), |
||||
|
@"bits_per_channel" : @(format.mBitsPerChannel) |
||||
|
} |
||||
|
}; |
||||
|
SCLogGeneralInfo(@"Audio queue error occured. ErrorInfo: %@", errorInfo); |
||||
|
return [NSError errorWithDomain:SCAudioCaptureSessionErrorDomain code:errorCode userInfo:errorInfo]; |
||||
|
} |
||||
|
|
||||
|
- (NSError *)beginAudioRecordingWithSampleRate:(Float64)sampleRate |
||||
|
{ |
||||
|
SCTraceStart(); |
||||
|
if ([SCAudioSession sharedInstance].inputAvailable) { |
||||
|
// SCAudioSession should be activated already |
||||
|
SCTraceSignal(@"Set audio session to be active"); |
||||
|
AudioStreamBasicDescription recordFormat = setupAudioFormat(kAudioFormatLinearPCM, sampleRate); |
||||
|
OSStatus audioQueueCreationStatus = AudioQueueNewInput(&recordFormat, audioQueueBufferHandler, |
||||
|
(__bridge void *)self, NULL, NULL, 0, &_audioQueue); |
||||
|
if (audioQueueCreationStatus != 0) { |
||||
|
NSError *error = [self _generateErrorForType:@"audio_queue_create_error" |
||||
|
errorCode:audioQueueCreationStatus |
||||
|
format:recordFormat]; |
||||
|
return error; |
||||
|
} |
||||
|
SCTraceSignal(@"Initialize audio queue with new input"); |
||||
|
UInt32 bufferByteSize = computeRecordBufferSize( |
||||
|
&recordFormat, _audioQueue, kAudioBufferDurationInSeconds); // Enough bytes for half a second |
||||
|
for (int i = 0; i < kNumberOfAudioBuffersInQueue; i++) { |
||||
|
AudioQueueAllocateBuffer(_audioQueue, bufferByteSize, &_audioQueueBuffers[i]); |
||||
|
AudioQueueEnqueueBuffer(_audioQueue, _audioQueueBuffers[i], 0, NULL); |
||||
|
} |
||||
|
SCTraceSignal(@"Allocate audio buffer"); |
||||
|
UInt32 size = sizeof(recordFormat); |
||||
|
audioQueueCreationStatus = |
||||
|
AudioQueueGetProperty(_audioQueue, kAudioQueueProperty_StreamDescription, &recordFormat, &size); |
||||
|
if (0 != audioQueueCreationStatus) { |
||||
|
NSError *error = [self _generateErrorForType:@"audio_queue_get_property_error" |
||||
|
errorCode:audioQueueCreationStatus |
||||
|
format:recordFormat]; |
||||
|
[self disposeAudioRecording]; |
||||
|
return error; |
||||
|
} |
||||
|
SCTraceSignal(@"Audio queue sample rate %lf", recordFormat.mSampleRate); |
||||
|
AudioChannelLayout acl; |
||||
|
bzero(&acl, sizeof(acl)); |
||||
|
acl.mChannelLayoutTag = kAudioChannelLayoutTag_Mono; |
||||
|
audioQueueCreationStatus = CMAudioFormatDescriptionCreate(NULL, &recordFormat, sizeof(acl), &acl, 0, NULL, NULL, |
||||
|
&_audioFormatDescription); |
||||
|
if (0 != audioQueueCreationStatus) { |
||||
|
NSError *error = [self _generateErrorForType:@"audio_queue_audio_format_error" |
||||
|
errorCode:audioQueueCreationStatus |
||||
|
format:recordFormat]; |
||||
|
[self disposeAudioRecording]; |
||||
|
return error; |
||||
|
} |
||||
|
SCTraceSignal(@"Start audio queue"); |
||||
|
audioQueueCreationStatus = AudioQueueStart(_audioQueue, NULL); |
||||
|
if (0 != audioQueueCreationStatus) { |
||||
|
NSError *error = [self _generateErrorForType:@"audio_queue_start_error" |
||||
|
errorCode:audioQueueCreationStatus |
||||
|
format:recordFormat]; |
||||
|
[self disposeAudioRecording]; |
||||
|
return error; |
||||
|
} |
||||
|
} |
||||
|
return nil; |
||||
|
} |
||||
|
|
||||
|
- (void)disposeAudioRecording |
||||
|
{ |
||||
|
SCTraceStart(); |
||||
|
SCLogGeneralInfo(@"dispose audio recording"); |
||||
|
if (_audioQueue) { |
||||
|
AudioQueueStop(_audioQueue, true); |
||||
|
AudioQueueDispose(_audioQueue, true); |
||||
|
for (int i = 0; i < kNumberOfAudioBuffersInQueue; i++) { |
||||
|
_audioQueueBuffers[i] = NULL; |
||||
|
} |
||||
|
_audioQueue = NULL; |
||||
|
} |
||||
|
if (_audioFormatDescription) { |
||||
|
CFRelease(_audioFormatDescription); |
||||
|
_audioFormatDescription = NULL; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
#pragma mark - Public methods |
||||
|
|
||||
|
- (void)beginAudioRecordingAsynchronouslyWithSampleRate:(double)sampleRate |
||||
|
completionHandler:(audio_capture_session_block)completionHandler |
||||
|
{ |
||||
|
SCTraceStart(); |
||||
|
// Request audio session change for recording mode. |
||||
|
[_performer perform:^{ |
||||
|
SCTraceStart(); |
||||
|
NSError *error = [self beginAudioRecordingWithSampleRate:sampleRate]; |
||||
|
if (completionHandler) { |
||||
|
completionHandler(error); |
||||
|
} |
||||
|
}]; |
||||
|
} |
||||
|
|
||||
|
- (void)disposeAudioRecordingSynchronouslyWithCompletionHandler:(dispatch_block_t)completionHandler |
||||
|
{ |
||||
|
SCTraceStart(); |
||||
|
[_performer performAndWait:^{ |
||||
|
SCTraceStart(); |
||||
|
[self disposeAudioRecording]; |
||||
|
if (completionHandler) { |
||||
|
completionHandler(); |
||||
|
} |
||||
|
}]; |
||||
|
} |
||||
|
|
||||
|
@end |
@ -0,0 +1,23 @@ |
|||||
|
// |
||||
|
// SCCameraSettingUtils.h |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by Pinlin Chen on 12/09/2017. |
||||
|
// |
||||
|
|
||||
|
#import <SCBase/SCMacros.h> |
||||
|
|
||||
|
#import <SCCapturerDefines.h> |
||||
|
|
||||
|
#import <CoreMedia/CoreMedia.h> |
||||
|
#import <Foundation/Foundation.h> |
||||
|
|
||||
|
SC_EXTERN_C_BEGIN |
||||
|
|
||||
|
// Return the value if metadata attribute is found; otherwise, return nil |
||||
|
extern NSNumber *retrieveExposureTimeFromEXIFAttachments(CFDictionaryRef exifAttachments); |
||||
|
extern NSNumber *retrieveBrightnessFromEXIFAttachments(CFDictionaryRef exifAttachments); |
||||
|
extern NSNumber *retrieveISOSpeedRatingFromEXIFAttachments(CFDictionaryRef exifAttachments); |
||||
|
extern void retrieveSampleBufferMetadata(CMSampleBufferRef sampleBuffer, SampleBufferMetadata *metadata); |
||||
|
|
||||
|
SC_EXTERN_C_END |
@ -0,0 +1,79 @@ |
|||||
|
// |
||||
|
// SCCameraSettingUtils.m |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by Pinlin Chen on 12/09/2017. |
||||
|
// |
||||
|
|
||||
|
#import "SCCameraSettingUtils.h" |
||||
|
|
||||
|
#import <SCFoundation/SCLog.h> |
||||
|
|
||||
|
#import <ImageIO/CGImageProperties.h> |
||||
|
|
||||
|
NSNumber *retrieveExposureTimeFromEXIFAttachments(CFDictionaryRef exifAttachments) |
||||
|
{ |
||||
|
if (!exifAttachments) { |
||||
|
return nil; |
||||
|
} |
||||
|
id value = CFDictionaryGetValue(exifAttachments, kCGImagePropertyExifExposureTime); |
||||
|
// Fetching exposure time from the sample buffer |
||||
|
if ([value isKindOfClass:[NSNumber class]]) { |
||||
|
return (NSNumber *)value; |
||||
|
} |
||||
|
return nil; |
||||
|
} |
||||
|
|
||||
|
NSNumber *retrieveBrightnessFromEXIFAttachments(CFDictionaryRef exifAttachments) |
||||
|
{ |
||||
|
if (!exifAttachments) { |
||||
|
return nil; |
||||
|
} |
||||
|
id value = CFDictionaryGetValue(exifAttachments, kCGImagePropertyExifBrightnessValue); |
||||
|
if ([value isKindOfClass:[NSNumber class]]) { |
||||
|
return (NSNumber *)value; |
||||
|
} |
||||
|
return nil; |
||||
|
} |
||||
|
|
||||
|
NSNumber *retrieveISOSpeedRatingFromEXIFAttachments(CFDictionaryRef exifAttachments) |
||||
|
{ |
||||
|
if (!exifAttachments) { |
||||
|
return nil; |
||||
|
} |
||||
|
NSArray *ISOSpeedRatings = CFDictionaryGetValue(exifAttachments, kCGImagePropertyExifISOSpeedRatings); |
||||
|
if ([ISOSpeedRatings respondsToSelector:@selector(count)] && |
||||
|
[ISOSpeedRatings respondsToSelector:@selector(firstObject)] && ISOSpeedRatings.count > 0) { |
||||
|
id value = [ISOSpeedRatings firstObject]; |
||||
|
if ([value isKindOfClass:[NSNumber class]]) { |
||||
|
return (NSNumber *)value; |
||||
|
} |
||||
|
} |
||||
|
return nil; |
||||
|
} |
||||
|
|
||||
|
void retrieveSampleBufferMetadata(CMSampleBufferRef sampleBuffer, SampleBufferMetadata *metadata) |
||||
|
{ |
||||
|
CFDictionaryRef exifAttachments = CMGetAttachment(sampleBuffer, kCGImagePropertyExifDictionary, NULL); |
||||
|
if (exifAttachments == nil) { |
||||
|
SCLogCoreCameraWarning(@"SampleBuffer exifAttachment is nil"); |
||||
|
} |
||||
|
// Fetching exposure time from the sample buffer |
||||
|
NSNumber *currentExposureTimeNum = retrieveExposureTimeFromEXIFAttachments(exifAttachments); |
||||
|
if (currentExposureTimeNum) { |
||||
|
metadata->exposureTime = [currentExposureTimeNum floatValue]; |
||||
|
} |
||||
|
NSNumber *currentISOSpeedRatingNum = retrieveISOSpeedRatingFromEXIFAttachments(exifAttachments); |
||||
|
if (currentISOSpeedRatingNum) { |
||||
|
metadata->isoSpeedRating = (int)[currentISOSpeedRatingNum integerValue]; |
||||
|
} |
||||
|
NSNumber *currentBrightnessNum = retrieveBrightnessFromEXIFAttachments(exifAttachments); |
||||
|
if (currentBrightnessNum) { |
||||
|
float currentBrightness = [currentBrightnessNum floatValue]; |
||||
|
if (isfinite(currentBrightness)) { |
||||
|
metadata->brightness = currentBrightness; |
||||
|
} else { |
||||
|
metadata->brightness = 0; |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,74 @@ |
|||||
|
// |
||||
|
// SCCaptureCommon.h |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by Lin Jia on 9/29/17. |
||||
|
// |
||||
|
// |
||||
|
|
||||
|
#import "SCManagedCaptureDevice.h" |
||||
|
#import "SCManagedDeviceCapacityAnalyzerListener.h" |
||||
|
#import "SCVideoCaptureSessionInfo.h" |
||||
|
|
||||
|
#import <SCCameraFoundation/SCManagedVideoDataSourceListener.h> |
||||
|
|
||||
|
#import <AVFoundation/AVFoundation.h> |
||||
|
#import <Foundation/Foundation.h> |
||||
|
|
||||
|
@class SCManagedCapturerState; |
||||
|
@class SCManagedLensesProcessor; |
||||
|
@class SCManagedVideoDataSource; |
||||
|
@class SCManagedVideoCapturerOutputSettings; |
||||
|
@class SCLens; |
||||
|
@class SCLensCategory; |
||||
|
@class SCLookseryFilterFactory; |
||||
|
@class SCSnapScannedData; |
||||
|
@class SCCraftResourceManager; |
||||
|
@class SCScanConfiguration; |
||||
|
@class SCCapturerToken; |
||||
|
@class SCProcessingPipeline; |
||||
|
@class SCTimedTask; |
||||
|
@protocol SCManagedSampleBufferDisplayController; |
||||
|
|
||||
|
typedef void (^sc_managed_capturer_capture_still_image_completion_handler_t)(UIImage *fullScreenImage, |
||||
|
NSDictionary *metadata, NSError *error, |
||||
|
SCManagedCapturerState *state); |
||||
|
|
||||
|
typedef void (^sc_managed_capturer_capture_video_frame_completion_handler_t)(UIImage *image); |
||||
|
|
||||
|
typedef void (^sc_managed_capturer_start_recording_completion_handler_t)(SCVideoCaptureSessionInfo session, |
||||
|
NSError *error); |
||||
|
|
||||
|
typedef void (^sc_managed_capturer_convert_view_coordniates_completion_handler_t)(CGPoint pointOfInterest); |
||||
|
|
||||
|
typedef void (^sc_managed_capturer_unsafe_changes_t)(AVCaptureSession *session, AVCaptureDevice *front, |
||||
|
AVCaptureDeviceInput *frontInput, AVCaptureDevice *back, |
||||
|
AVCaptureDeviceInput *backInput, SCManagedCapturerState *state); |
||||
|
|
||||
|
typedef void (^sc_managed_capturer_stop_running_completion_handler_t)(BOOL succeed); |
||||
|
|
||||
|
typedef void (^sc_managed_capturer_scan_results_handler_t)(NSObject *resultObject); |
||||
|
|
||||
|
typedef void (^sc_managed_lenses_processor_category_point_completion_handler_t)(SCLensCategory *category, |
||||
|
NSInteger categoriesCount); |
||||
|
extern CGFloat const kSCManagedCapturerAspectRatioUnspecified; |
||||
|
|
||||
|
extern CGFloat const kSCManagedCapturerDefaultVideoActiveFormatWidth; |
||||
|
|
||||
|
extern CGFloat const kSCManagedCapturerDefaultVideoActiveFormatHeight; |
||||
|
|
||||
|
extern CGFloat const kSCManagedCapturerVideoActiveFormatWidth1080p; |
||||
|
|
||||
|
extern CGFloat const kSCManagedCapturerVideoActiveFormatHeight1080p; |
||||
|
|
||||
|
extern CGFloat const kSCManagedCapturerNightVideoHighResActiveFormatWidth; |
||||
|
|
||||
|
extern CGFloat const kSCManagedCapturerNightVideoHighResActiveFormatHeight; |
||||
|
|
||||
|
extern CGFloat const kSCManagedCapturerNightVideoDefaultResActiveFormatWidth; |
||||
|
|
||||
|
extern CGFloat const kSCManagedCapturerNightVideoDefaultResActiveFormatHeight; |
||||
|
|
||||
|
extern CGFloat const kSCManagedCapturerLiveStreamingVideoActiveFormatWidth; |
||||
|
|
||||
|
extern CGFloat const kSCManagedCapturerLiveStreamingVideoActiveFormatHeight; |
@ -0,0 +1,31 @@ |
|||||
|
// |
||||
|
// SCCaptureCommon.m |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by Lin Jia on 9/29/17. |
||||
|
// |
||||
|
// |
||||
|
|
||||
|
#import "SCCaptureCommon.h" |
||||
|
|
||||
|
CGFloat const kSCManagedCapturerAspectRatioUnspecified = -1.0; |
||||
|
|
||||
|
CGFloat const kSCManagedCapturerDefaultVideoActiveFormatWidth = 1280; |
||||
|
|
||||
|
CGFloat const kSCManagedCapturerDefaultVideoActiveFormatHeight = 720; |
||||
|
|
||||
|
CGFloat const kSCManagedCapturerVideoActiveFormatWidth1080p = 1920; |
||||
|
|
||||
|
CGFloat const kSCManagedCapturerVideoActiveFormatHeight1080p = 1080; |
||||
|
|
||||
|
CGFloat const kSCManagedCapturerNightVideoHighResActiveFormatWidth = 2592; |
||||
|
|
||||
|
CGFloat const kSCManagedCapturerNightVideoHighResActiveFormatHeight = 1936; |
||||
|
|
||||
|
CGFloat const kSCManagedCapturerNightVideoDefaultResActiveFormatWidth = 640; |
||||
|
|
||||
|
CGFloat const kSCManagedCapturerNightVideoDefaultResActiveFormatHeight = 480; |
||||
|
|
||||
|
CGFloat const kSCManagedCapturerLiveStreamingVideoActiveFormatWidth = 1280; |
||||
|
|
||||
|
CGFloat const kSCManagedCapturerLiveStreamingVideoActiveFormatHeight = 720; |
@ -0,0 +1,22 @@ |
|||||
|
// |
||||
|
// SCCaptureCoreImageFaceDetector.h |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by Jiyang Zhu on 3/27/18. |
||||
|
// Copyright © 2018 Snapchat, Inc. All rights reserved. |
||||
|
// |
||||
|
// This class is intended to detect faces in Camera. It receives CMSampleBuffer, process the face detection using |
||||
|
// CIDetector, and announce the bounds and faceIDs. |
||||
|
|
||||
|
#import "SCCaptureFaceDetector.h" |
||||
|
|
||||
|
#import <SCBase/SCMacros.h> |
||||
|
#import <SCCameraFoundation/SCManagedVideoDataSourceListener.h> |
||||
|
|
||||
|
#import <Foundation/Foundation.h> |
||||
|
|
||||
|
@interface SCCaptureCoreImageFaceDetector : NSObject <SCCaptureFaceDetector, SCManagedVideoDataSourceListener> |
||||
|
|
||||
|
SC_INIT_AND_NEW_UNAVAILABLE; |
||||
|
|
||||
|
@end |
@ -0,0 +1,205 @@ |
|||||
|
// |
||||
|
// SCCaptureCoreImageFaceDetector.m |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by Jiyang Zhu on 3/27/18. |
||||
|
// Copyright © 2018 Snapchat, Inc. All rights reserved. |
||||
|
// |
||||
|
|
||||
|
#import "SCCaptureCoreImageFaceDetector.h" |
||||
|
|
||||
|
#import "SCCameraTweaks.h" |
||||
|
#import "SCCaptureFaceDetectionParser.h" |
||||
|
#import "SCCaptureFaceDetectorTrigger.h" |
||||
|
#import "SCCaptureResource.h" |
||||
|
#import "SCManagedCapturer.h" |
||||
|
|
||||
|
#import <SCFoundation/NSArray+Helpers.h> |
||||
|
#import <SCFoundation/SCAssertWrapper.h> |
||||
|
#import <SCFoundation/SCLog.h> |
||||
|
#import <SCFoundation/SCQueuePerformer.h> |
||||
|
#import <SCFoundation/SCTrace.h> |
||||
|
#import <SCFoundation/SCTraceODPCompatible.h> |
||||
|
#import <SCFoundation/SCZeroDependencyExperiments.h> |
||||
|
#import <SCFoundation/UIImage+CVPixelBufferRef.h> |
||||
|
|
||||
|
@import ImageIO; |
||||
|
|
||||
|
static const NSTimeInterval kSCCaptureCoreImageFaceDetectorMaxAllowedLatency = |
||||
|
1; // Drop the face detection result if it is 1 second late. |
||||
|
static const NSInteger kDefaultNumberOfSequentialOutputSampleBuffer = -1; // -1 means no sequential sample buffers. |
||||
|
|
||||
|
static char *const kSCCaptureCoreImageFaceDetectorProcessQueue = |
||||
|
"com.snapchat.capture-core-image-face-detector-process"; |
||||
|
|
||||
|
@implementation SCCaptureCoreImageFaceDetector { |
||||
|
CIDetector *_detector; |
||||
|
SCCaptureResource *_captureResource; |
||||
|
|
||||
|
BOOL _isDetecting; |
||||
|
BOOL _hasDetectedFaces; |
||||
|
NSInteger _numberOfSequentialOutputSampleBuffer; |
||||
|
NSUInteger _detectionFrequency; |
||||
|
NSDictionary *_detectorOptions; |
||||
|
SCManagedCaptureDevicePosition _devicePosition; |
||||
|
CIContext *_context; |
||||
|
|
||||
|
SCQueuePerformer *_callbackPerformer; |
||||
|
SCQueuePerformer *_processPerformer; |
||||
|
|
||||
|
SCCaptureFaceDetectionParser *_parser; |
||||
|
SCCaptureFaceDetectorTrigger *_trigger; |
||||
|
} |
||||
|
|
||||
|
@synthesize trigger = _trigger; |
||||
|
@synthesize parser = _parser; |
||||
|
|
||||
|
- (instancetype)initWithCaptureResource:(SCCaptureResource *)captureResource |
||||
|
{ |
||||
|
SCTraceODPCompatibleStart(2); |
||||
|
self = [super init]; |
||||
|
if (self) { |
||||
|
SCAssert(captureResource, @"SCCaptureResource should not be nil"); |
||||
|
SCAssert(captureResource.queuePerformer, @"SCQueuePerformer should not be nil"); |
||||
|
_callbackPerformer = captureResource.queuePerformer; |
||||
|
_captureResource = captureResource; |
||||
|
_parser = [[SCCaptureFaceDetectionParser alloc] |
||||
|
initWithFaceBoundsAreaThreshold:pow(SCCameraFaceFocusMinFaceSize(), 2)]; |
||||
|
_processPerformer = [[SCQueuePerformer alloc] initWithLabel:kSCCaptureCoreImageFaceDetectorProcessQueue |
||||
|
qualityOfService:QOS_CLASS_USER_INITIATED |
||||
|
queueType:DISPATCH_QUEUE_SERIAL |
||||
|
context:SCQueuePerformerContextCamera]; |
||||
|
_detectionFrequency = SCExperimentWithFaceDetectionFrequency(); |
||||
|
_devicePosition = captureResource.device.position; |
||||
|
_trigger = [[SCCaptureFaceDetectorTrigger alloc] initWithDetector:self]; |
||||
|
} |
||||
|
return self; |
||||
|
} |
||||
|
|
||||
|
- (void)_setupDetectionIfNeeded |
||||
|
{ |
||||
|
SCTraceODPCompatibleStart(2); |
||||
|
SC_GUARD_ELSE_RETURN(!_detector); |
||||
|
if (!_context) { |
||||
|
_context = [CIContext context]; |
||||
|
} |
||||
|
// For CIDetectorMinFeatureSize, the valid range is [0.0100, 0.5000], otherwise, it will cause a crash. |
||||
|
if (!_detectorOptions) { |
||||
|
_detectorOptions = @{ |
||||
|
CIDetectorAccuracy : CIDetectorAccuracyLow, |
||||
|
CIDetectorTracking : @(YES), |
||||
|
CIDetectorMaxFeatureCount : @(2), |
||||
|
CIDetectorMinFeatureSize : @(SCCameraFaceFocusMinFaceSize()), |
||||
|
CIDetectorNumberOfAngles : @(3) |
||||
|
}; |
||||
|
} |
||||
|
@try { |
||||
|
_detector = [CIDetector detectorOfType:CIDetectorTypeFace context:_context options:_detectorOptions]; |
||||
|
} @catch (NSException *exception) { |
||||
|
SCLogCoreCameraError(@"Failed to create CIDetector with exception:%@", exception); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
- (void)_resetDetection |
||||
|
{ |
||||
|
SCTraceODPCompatibleStart(2); |
||||
|
_detector = nil; |
||||
|
[self _setupDetectionIfNeeded]; |
||||
|
} |
||||
|
|
||||
|
- (SCQueuePerformer *)detectionPerformer |
||||
|
{ |
||||
|
return _processPerformer; |
||||
|
} |
||||
|
|
||||
|
- (void)startDetection |
||||
|
{ |
||||
|
SCTraceODPCompatibleStart(2); |
||||
|
SCAssert([[self detectionPerformer] isCurrentPerformer], @"Calling -startDetection in an invalid queue."); |
||||
|
[self _setupDetectionIfNeeded]; |
||||
|
_isDetecting = YES; |
||||
|
_hasDetectedFaces = NO; |
||||
|
_numberOfSequentialOutputSampleBuffer = kDefaultNumberOfSequentialOutputSampleBuffer; |
||||
|
} |
||||
|
|
||||
|
- (void)stopDetection |
||||
|
{ |
||||
|
SCTraceODPCompatibleStart(2); |
||||
|
SCAssert([[self detectionPerformer] isCurrentPerformer], @"Calling -stopDetection in an invalid queue."); |
||||
|
_isDetecting = NO; |
||||
|
} |
||||
|
|
||||
|
- (NSDictionary<NSNumber *, NSValue *> *)_detectFaceFeaturesInImage:(CIImage *)image |
||||
|
withOrientation:(CGImagePropertyOrientation)orientation |
||||
|
{ |
||||
|
SCTraceODPCompatibleStart(2); |
||||
|
NSDictionary *opts = |
||||
|
@{ CIDetectorImageOrientation : @(orientation), |
||||
|
CIDetectorEyeBlink : @(NO), |
||||
|
CIDetectorSmile : @(NO) }; |
||||
|
NSArray<CIFeature *> *features = [_detector featuresInImage:image options:opts]; |
||||
|
return [_parser parseFaceBoundsByFaceIDFromCIFeatures:features |
||||
|
withImageSize:image.extent.size |
||||
|
imageOrientation:orientation]; |
||||
|
} |
||||
|
|
||||
|
#pragma mark - SCManagedVideoDataSourceListener |
||||
|
|
||||
|
- (void)managedVideoDataSource:(id<SCManagedVideoDataSource>)managedVideoDataSource |
||||
|
didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer |
||||
|
devicePosition:(SCManagedCaptureDevicePosition)devicePosition |
||||
|
{ |
||||
|
SCTraceODPCompatibleStart(2); |
||||
|
SC_GUARD_ELSE_RETURN(_isDetecting); |
||||
|
|
||||
|
// Reset detection if the device position changes. Resetting detection should execute in _processPerformer, so we |
||||
|
// just set a flag here, and then do it later in the perform block. |
||||
|
BOOL shouldForceResetDetection = NO; |
||||
|
if (devicePosition != _devicePosition) { |
||||
|
_devicePosition = devicePosition; |
||||
|
shouldForceResetDetection = YES; |
||||
|
_numberOfSequentialOutputSampleBuffer = kDefaultNumberOfSequentialOutputSampleBuffer; |
||||
|
} |
||||
|
|
||||
|
_numberOfSequentialOutputSampleBuffer++; |
||||
|
SC_GUARD_ELSE_RETURN(_numberOfSequentialOutputSampleBuffer % _detectionFrequency == 0); |
||||
|
@weakify(self); |
||||
|
CFRetain(sampleBuffer); |
||||
|
[_processPerformer perform:^{ |
||||
|
SCTraceStart(); |
||||
|
@strongify(self); |
||||
|
SC_GUARD_ELSE_RETURN(self); |
||||
|
|
||||
|
if (shouldForceResetDetection) { |
||||
|
// Resetting detection usually costs no more than 1ms. |
||||
|
[self _resetDetection]; |
||||
|
} |
||||
|
|
||||
|
CGImagePropertyOrientation orientation = |
||||
|
(devicePosition == SCManagedCaptureDevicePositionBack ? kCGImagePropertyOrientationRight |
||||
|
: kCGImagePropertyOrientationLeftMirrored); |
||||
|
CIImage *image = [CIImage imageWithCVPixelBuffer:CMSampleBufferGetImageBuffer(sampleBuffer)]; |
||||
|
NSDictionary<NSNumber *, NSValue *> *faceBoundsByFaceID = |
||||
|
[self _detectFaceFeaturesInImage:image withOrientation:orientation]; |
||||
|
|
||||
|
// Calculate the latency for face detection, if it is too long, discard the face detection results. |
||||
|
NSTimeInterval latency = |
||||
|
CACurrentMediaTime() - CMTimeGetSeconds(CMSampleBufferGetPresentationTimeStamp(sampleBuffer)); |
||||
|
CFRelease(sampleBuffer); |
||||
|
if (latency >= kSCCaptureCoreImageFaceDetectorMaxAllowedLatency) { |
||||
|
faceBoundsByFaceID = nil; |
||||
|
} |
||||
|
|
||||
|
// Only announce face detection result if faceBoundsByFaceID is not empty, or faceBoundsByFaceID was not empty |
||||
|
// last time. |
||||
|
if (faceBoundsByFaceID.count > 0 || self->_hasDetectedFaces) { |
||||
|
self->_hasDetectedFaces = faceBoundsByFaceID.count > 0; |
||||
|
[self->_callbackPerformer perform:^{ |
||||
|
[self->_captureResource.announcer managedCapturer:[SCManagedCapturer sharedInstance] |
||||
|
didDetectFaceBounds:faceBoundsByFaceID]; |
||||
|
}]; |
||||
|
} |
||||
|
}]; |
||||
|
} |
||||
|
|
||||
|
@end |
@ -0,0 +1,24 @@ |
|||||
|
// |
||||
|
// SCCaptureDeviceAuthorization.h |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by Xiaomu Wu on 8/19/14. |
||||
|
// Copyright (c) 2014 Snapchat, Inc. All rights reserved. |
||||
|
// |
||||
|
|
||||
|
#import <Foundation/Foundation.h> |
||||
|
|
||||
|
@interface SCCaptureDeviceAuthorization : NSObject |
||||
|
|
||||
|
// Methods for checking / requesting authorization to use media capture devices of a given type. |
||||
|
+ (BOOL)notDeterminedForMediaType:(NSString *)mediaType; |
||||
|
+ (BOOL)deniedForMediaType:(NSString *)mediaType; |
||||
|
+ (BOOL)restrictedForMediaType:(NSString *)mediaType; |
||||
|
+ (void)requestAccessForMediaType:(NSString *)mediaType completionHandler:(void (^)(BOOL granted))handler; |
||||
|
|
||||
|
// Convenience methods for media type == AVMediaTypeVideo |
||||
|
+ (BOOL)notDeterminedForVideoCapture; |
||||
|
+ (BOOL)deniedForVideoCapture; |
||||
|
+ (void)requestAccessForVideoCaptureWithCompletionHandler:(void (^)(BOOL granted))handler; |
||||
|
|
||||
|
@end |
@ -0,0 +1,71 @@ |
|||||
|
// |
||||
|
// SCCaptureDeviceAuthorization.m |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by Xiaomu Wu on 8/19/14. |
||||
|
// Copyright (c) 2014 Snapchat, Inc. All rights reserved. |
||||
|
// |
||||
|
|
||||
|
#import "SCCaptureDeviceAuthorization.h" |
||||
|
|
||||
|
#import <BlizzardSchema/SCAEvents.h> |
||||
|
#import <SCFoundation/SCTrace.h> |
||||
|
#import <SCLogger/SCLogger.h> |
||||
|
|
||||
|
@import AVFoundation; |
||||
|
|
||||
|
@implementation SCCaptureDeviceAuthorization |
||||
|
|
||||
|
#pragma mark - Public |
||||
|
|
||||
|
+ (BOOL)notDeterminedForMediaType:(NSString *)mediaType |
||||
|
{ |
||||
|
return [AVCaptureDevice authorizationStatusForMediaType:mediaType] == AVAuthorizationStatusNotDetermined; |
||||
|
} |
||||
|
|
||||
|
+ (BOOL)deniedForMediaType:(NSString *)mediaType |
||||
|
{ |
||||
|
return [AVCaptureDevice authorizationStatusForMediaType:mediaType] == AVAuthorizationStatusDenied; |
||||
|
} |
||||
|
|
||||
|
+ (BOOL)restrictedForMediaType:(NSString *)mediaType |
||||
|
{ |
||||
|
return [AVCaptureDevice authorizationStatusForMediaType:mediaType] == AVAuthorizationStatusRestricted; |
||||
|
} |
||||
|
|
||||
|
+ (void)requestAccessForMediaType:(NSString *)mediaType completionHandler:(void (^)(BOOL granted))handler |
||||
|
{ |
||||
|
[AVCaptureDevice requestAccessForMediaType:mediaType completionHandler:handler]; |
||||
|
} |
||||
|
|
||||
|
#pragma mark - Convenience methods for AVMediaTypeVideo |
||||
|
|
||||
|
+ (BOOL)notDeterminedForVideoCapture |
||||
|
{ |
||||
|
return [self notDeterminedForMediaType:AVMediaTypeVideo]; |
||||
|
} |
||||
|
|
||||
|
+ (BOOL)deniedForVideoCapture |
||||
|
{ |
||||
|
return [self deniedForMediaType:AVMediaTypeVideo]; |
||||
|
} |
||||
|
|
||||
|
+ (void)requestAccessForVideoCaptureWithCompletionHandler:(void (^)(BOOL granted))handler |
||||
|
{ |
||||
|
BOOL firstTimeAsking = |
||||
|
[AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo] == AVAuthorizationStatusNotDetermined; |
||||
|
[self requestAccessForMediaType:AVMediaTypeVideo |
||||
|
completionHandler:^(BOOL granted) { |
||||
|
if (firstTimeAsking) { |
||||
|
SCAPermissionPromptResponse *responseEvent = [[SCAPermissionPromptResponse alloc] init]; |
||||
|
[responseEvent setPermissionPromptType:SCAPermissionPromptType_OS_CAMERA]; |
||||
|
[responseEvent setAccepted:granted]; |
||||
|
[[SCLogger sharedInstance] logUserTrackedEvent:responseEvent]; |
||||
|
} |
||||
|
if (handler) { |
||||
|
handler(granted); |
||||
|
} |
||||
|
}]; |
||||
|
} |
||||
|
|
||||
|
@end |
@ -0,0 +1,31 @@ |
|||||
|
// |
||||
|
// SCCaptureDeviceAuthorizationChecker.h |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by Sun Lei on 15/03/2018. |
||||
|
// |
||||
|
|
||||
|
@class SCQueuePerformer; |
||||
|
|
||||
|
#import <SCBase/SCMacros.h> |
||||
|
|
||||
|
#import <Foundation/Foundation.h> |
||||
|
|
||||
|
/* |
||||
|
In general, the function of SCCaptureDeviceAuthorizationChecker is to speed up the checking of AVMediaTypeVideo |
||||
|
authorization. It would cache the authorization value. 'preloadVideoCaptureAuthorization' would be called very early |
||||
|
after the app is launched to populate the cached value. 'authorizedForVideoCapture' could be called to get the value |
||||
|
synchronously. |
||||
|
|
||||
|
*/ |
||||
|
|
||||
|
@interface SCCaptureDeviceAuthorizationChecker : NSObject |
||||
|
|
||||
|
SC_INIT_AND_NEW_UNAVAILABLE |
||||
|
- (instancetype)initWithPerformer:(SCQueuePerformer *)performer NS_DESIGNATED_INITIALIZER; |
||||
|
|
||||
|
- (BOOL)authorizedForVideoCapture; |
||||
|
|
||||
|
- (void)preloadVideoCaptureAuthorization; |
||||
|
|
||||
|
@end |
@ -0,0 +1,71 @@ |
|||||
|
// |
||||
|
// SCCaptureDeviceAuthorizationChecker.m |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by Sun Lei on 15/03/2018. |
||||
|
// |
||||
|
|
||||
|
#import "SCCaptureDeviceAuthorizationChecker.h" |
||||
|
|
||||
|
#import <SCFoundation/SCQueuePerformer.h> |
||||
|
#import <SCFoundation/SCTraceODPCompatible.h> |
||||
|
|
||||
|
@import AVFoundation; |
||||
|
|
||||
|
@interface SCCaptureDeviceAuthorizationChecker () { |
||||
|
SCQueuePerformer *_performer; |
||||
|
BOOL _videoCaptureAuthorizationCachedValue; |
||||
|
} |
||||
|
@end |
||||
|
|
||||
|
@implementation SCCaptureDeviceAuthorizationChecker |
||||
|
|
||||
|
- (instancetype)initWithPerformer:(SCQueuePerformer *)performer |
||||
|
{ |
||||
|
SCTraceODPCompatibleStart(2); |
||||
|
self = [super init]; |
||||
|
if (self) { |
||||
|
_performer = performer; |
||||
|
_videoCaptureAuthorizationCachedValue = NO; |
||||
|
} |
||||
|
return self; |
||||
|
} |
||||
|
|
||||
|
- (void)preloadVideoCaptureAuthorization |
||||
|
{ |
||||
|
SCTraceODPCompatibleStart(2); |
||||
|
[_performer perform:^{ |
||||
|
SCTraceODPCompatibleStart(2); |
||||
|
_videoCaptureAuthorizationCachedValue = [self authorizedForMediaType:AVMediaTypeVideo]; |
||||
|
}]; |
||||
|
} |
||||
|
|
||||
|
- (BOOL)authorizedForVideoCapture |
||||
|
{ |
||||
|
SCTraceODPCompatibleStart(2); |
||||
|
// Cache authorizedForVideoCapture for low devices if it's YES |
||||
|
// [AVCaptureDevice authorizationStatusForMediaType:] is expensive on low devices like iPhone4 |
||||
|
if (_videoCaptureAuthorizationCachedValue) { |
||||
|
// If the user authorizes and then unauthorizes, iOS would SIGKILL the app. |
||||
|
// When the user opens the app, a pop-up tells the user to allow camera access in settings. |
||||
|
// So 'return YES' makes sense here. |
||||
|
return YES; |
||||
|
} else { |
||||
|
@weakify(self); |
||||
|
[_performer performAndWait:^{ |
||||
|
@strongify(self); |
||||
|
SC_GUARD_ELSE_RETURN(self); |
||||
|
if (!_videoCaptureAuthorizationCachedValue) { |
||||
|
_videoCaptureAuthorizationCachedValue = [self authorizedForMediaType:AVMediaTypeVideo]; |
||||
|
} |
||||
|
}]; |
||||
|
return _videoCaptureAuthorizationCachedValue; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
- (BOOL)authorizedForMediaType:(NSString *)mediaType |
||||
|
{ |
||||
|
return [AVCaptureDevice authorizationStatusForMediaType:mediaType] == AVAuthorizationStatusAuthorized; |
||||
|
} |
||||
|
|
||||
|
@end |
@ -0,0 +1,31 @@ |
|||||
|
// |
||||
|
// SCCaptureDeviceResolver.h |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by Lin Jia on 11/8/17. |
||||
|
// |
||||
|
// |
||||
|
|
||||
|
#import <AVFoundation/AVFoundation.h> |
||||
|
|
||||
|
/* |
||||
|
See https://jira.sc-corp.net/browse/CCAM-5843 |
||||
|
|
||||
|
Retrieving AVCaptureDevice is a flaky operation. Thus create capture device resolver to make our code more robust. |
||||
|
|
||||
|
Resolver is used to retrieve AVCaptureDevice. We are going to do our best to find the camera for you. |
||||
|
|
||||
|
Resolver is only going to be used by SCManagedCaptureDevice. |
||||
|
|
||||
|
All APIs are thread safe. |
||||
|
*/ |
||||
|
|
||||
|
@interface SCCaptureDeviceResolver : NSObject |
||||
|
|
||||
|
+ (instancetype)sharedInstance; |
||||
|
|
||||
|
- (AVCaptureDevice *)findAVCaptureDevice:(AVCaptureDevicePosition)position; |
||||
|
|
||||
|
- (AVCaptureDevice *)findDualCamera; |
||||
|
|
||||
|
@end |
@ -0,0 +1,147 @@ |
|||||
|
// |
||||
|
// SCCaptureDeviceResolver.m |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by Lin Jia on 11/8/17. |
||||
|
// |
||||
|
// |
||||
|
|
||||
|
#import "SCCaptureDeviceResolver.h" |
||||
|
|
||||
|
#import "SCCameraTweaks.h" |
||||
|
|
||||
|
#import <SCBase/SCAvailability.h> |
||||
|
#import <SCFoundation/SCAssertWrapper.h> |
||||
|
|
||||
|
@interface SCCaptureDeviceResolver () { |
||||
|
AVCaptureDeviceDiscoverySession *_discoverySession; |
||||
|
} |
||||
|
|
||||
|
@end |
||||
|
|
||||
|
@implementation SCCaptureDeviceResolver |
||||
|
|
||||
|
+ (instancetype)sharedInstance |
||||
|
{ |
||||
|
static SCCaptureDeviceResolver *resolver; |
||||
|
static dispatch_once_t onceToken; |
||||
|
dispatch_once(&onceToken, ^{ |
||||
|
resolver = [[SCCaptureDeviceResolver alloc] init]; |
||||
|
}); |
||||
|
return resolver; |
||||
|
} |
||||
|
|
||||
|
- (instancetype)init |
||||
|
{ |
||||
|
self = [super init]; |
||||
|
if (self) { |
||||
|
NSMutableArray *deviceTypes = [[NSMutableArray alloc] init]; |
||||
|
[deviceTypes addObject:AVCaptureDeviceTypeBuiltInWideAngleCamera]; |
||||
|
if (SC_AT_LEAST_IOS_10_2) { |
||||
|
[deviceTypes addObject:AVCaptureDeviceTypeBuiltInDualCamera]; |
||||
|
} |
||||
|
// TODO: we should KVO _discoverySession.devices. |
||||
|
_discoverySession = |
||||
|
[AVCaptureDeviceDiscoverySession discoverySessionWithDeviceTypes:deviceTypes |
||||
|
mediaType:AVMediaTypeVideo |
||||
|
position:AVCaptureDevicePositionUnspecified]; |
||||
|
} |
||||
|
return self; |
||||
|
} |
||||
|
|
||||
|
- (AVCaptureDevice *)findAVCaptureDevice:(AVCaptureDevicePosition)position |
||||
|
{ |
||||
|
SCAssert(position == AVCaptureDevicePositionFront || position == AVCaptureDevicePositionBack, @""); |
||||
|
AVCaptureDevice *captureDevice; |
||||
|
if (position == AVCaptureDevicePositionFront) { |
||||
|
captureDevice = [self _pickBestFrontCamera:[_discoverySession.devices copy]]; |
||||
|
} else if (position == AVCaptureDevicePositionBack) { |
||||
|
captureDevice = [self _pickBestBackCamera:[_discoverySession.devices copy]]; |
||||
|
} |
||||
|
if (captureDevice) { |
||||
|
return captureDevice; |
||||
|
} |
||||
|
|
||||
|
if (SC_AT_LEAST_IOS_10_2 && SCCameraTweaksEnableDualCamera()) { |
||||
|
captureDevice = [AVCaptureDevice defaultDeviceWithDeviceType:AVCaptureDeviceTypeBuiltInDualCamera |
||||
|
mediaType:AVMediaTypeVideo |
||||
|
position:position]; |
||||
|
if (captureDevice) { |
||||
|
return captureDevice; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// if code still execute, discoverSession failed, then we keep searching. |
||||
|
captureDevice = [AVCaptureDevice defaultDeviceWithDeviceType:AVCaptureDeviceTypeBuiltInWideAngleCamera |
||||
|
mediaType:AVMediaTypeVideo |
||||
|
position:position]; |
||||
|
if (captureDevice) { |
||||
|
return captureDevice; |
||||
|
} |
||||
|
|
||||
|
#if !TARGET_IPHONE_SIMULATOR |
||||
|
// We do not return nil at the beginning of the function for simulator, because simulators of different IOS |
||||
|
// versions can check whether or not our camera device API access is correct. |
||||
|
SCAssertFail(@"No camera is found."); |
||||
|
#endif |
||||
|
return nil; |
||||
|
} |
||||
|
|
||||
|
- (AVCaptureDevice *)_pickBestFrontCamera:(NSArray<AVCaptureDevice *> *)devices |
||||
|
{ |
||||
|
for (AVCaptureDevice *device in devices) { |
||||
|
if (device.position == AVCaptureDevicePositionFront) { |
||||
|
return device; |
||||
|
} |
||||
|
} |
||||
|
return nil; |
||||
|
} |
||||
|
|
||||
|
- (AVCaptureDevice *)_pickBestBackCamera:(NSArray<AVCaptureDevice *> *)devices |
||||
|
{ |
||||
|
// Look for dual camera first if needed. If dual camera not found, continue to look for wide angle camera. |
||||
|
if (SC_AT_LEAST_IOS_10_2 && SCCameraTweaksEnableDualCamera()) { |
||||
|
for (AVCaptureDevice *device in devices) { |
||||
|
if (device.position == AVCaptureDevicePositionBack && |
||||
|
device.deviceType == AVCaptureDeviceTypeBuiltInDualCamera) { |
||||
|
return device; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
for (AVCaptureDevice *device in devices) { |
||||
|
if (device.position == AVCaptureDevicePositionBack && |
||||
|
device.deviceType == AVCaptureDeviceTypeBuiltInWideAngleCamera) { |
||||
|
return device; |
||||
|
} |
||||
|
} |
||||
|
return nil; |
||||
|
} |
||||
|
|
||||
|
- (AVCaptureDevice *)findDualCamera |
||||
|
{ |
||||
|
if (SC_AT_LEAST_IOS_10_2) { |
||||
|
for (AVCaptureDevice *device in [_discoverySession.devices copy]) { |
||||
|
if (device.position == AVCaptureDevicePositionBack && |
||||
|
device.deviceType == AVCaptureDeviceTypeBuiltInDualCamera) { |
||||
|
return device; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
AVCaptureDevice *captureDevice = [AVCaptureDevice defaultDeviceWithDeviceType:AVCaptureDeviceTypeBuiltInDualCamera |
||||
|
mediaType:AVMediaTypeVideo |
||||
|
position:AVCaptureDevicePositionBack]; |
||||
|
if (captureDevice) { |
||||
|
return captureDevice; |
||||
|
} |
||||
|
|
||||
|
#if !TARGET_IPHONE_SIMULATOR |
||||
|
// We do not return nil at the beginning of the function for simulator, because simulators of different IOS |
||||
|
// versions can check whether or not our camera device API access is correct. |
||||
|
SCAssertFail(@"No camera is found."); |
||||
|
#endif |
||||
|
return nil; |
||||
|
} |
||||
|
|
||||
|
@end |
@ -0,0 +1,43 @@ |
|||||
|
// |
||||
|
// SCCaptureFaceDetectionParser.h |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by Jiyang Zhu on 3/13/18. |
||||
|
// Copyright © 2018 Snapchat, Inc. All rights reserved. |
||||
|
// |
||||
|
// This class offers methods to parse face bounds from raw data, e.g., AVMetadataObject, CIFeature. |
||||
|
|
||||
|
#import <SCBase/SCMacros.h> |
||||
|
|
||||
|
#import <AVFoundation/AVFoundation.h> |
||||
|
#import <CoreImage/CoreImage.h> |
||||
|
|
||||
|
@interface SCCaptureFaceDetectionParser : NSObject |
||||
|
|
||||
|
SC_INIT_AND_NEW_UNAVAILABLE; |
||||
|
|
||||
|
- (instancetype)initWithFaceBoundsAreaThreshold:(CGFloat)minimumArea; |
||||
|
|
||||
|
/** |
||||
|
Parse face bounds from AVMetadataObject. |
||||
|
|
||||
|
@param metadataObjects An array of AVMetadataObject. |
||||
|
@return A dictionary, value is faceBounds: CGRect, key is faceID: NSString. |
||||
|
*/ |
||||
|
- (NSDictionary<NSNumber *, NSValue *> *)parseFaceBoundsByFaceIDFromMetadataObjects: |
||||
|
(NSArray<__kindof AVMetadataObject *> *)metadataObjects; |
||||
|
|
||||
|
/** |
||||
|
Parse face bounds from CIFeature. |
||||
|
|
||||
|
@param features An array of CIFeature. |
||||
|
@param imageSize Size of the image, where the feature are detected from. |
||||
|
@param imageOrientation Orientation of the image. |
||||
|
@return A dictionary, value is faceBounds: CGRect, key is faceID: NSString. |
||||
|
*/ |
||||
|
- (NSDictionary<NSNumber *, NSValue *> *)parseFaceBoundsByFaceIDFromCIFeatures:(NSArray<__kindof CIFeature *> *)features |
||||
|
withImageSize:(CGSize)imageSize |
||||
|
imageOrientation: |
||||
|
(CGImagePropertyOrientation)imageOrientation; |
||||
|
|
||||
|
@end |
@ -0,0 +1,94 @@ |
|||||
|
// |
||||
|
// SCCaptureFaceDetectionParser.m |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by Jiyang Zhu on 3/13/18. |
||||
|
// Copyright © 2018 Snapchat, Inc. All rights reserved. |
||||
|
// |
||||
|
|
||||
|
#import "SCCaptureFaceDetectionParser.h" |
||||
|
|
||||
|
#import <SCFoundation/NSArray+Helpers.h> |
||||
|
#import <SCFoundation/SCLog.h> |
||||
|
#import <SCFoundation/SCTraceODPCompatible.h> |
||||
|
|
||||
|
@implementation SCCaptureFaceDetectionParser { |
||||
|
CGFloat _minimumArea; |
||||
|
} |
||||
|
|
||||
|
- (instancetype)initWithFaceBoundsAreaThreshold:(CGFloat)minimumArea |
||||
|
{ |
||||
|
self = [super init]; |
||||
|
if (self) { |
||||
|
_minimumArea = minimumArea; |
||||
|
} |
||||
|
return self; |
||||
|
} |
||||
|
|
||||
|
- (NSDictionary<NSNumber *, NSValue *> *)parseFaceBoundsByFaceIDFromMetadataObjects: |
||||
|
(NSArray<__kindof AVMetadataObject *> *)metadataObjects |
||||
|
{ |
||||
|
SCTraceODPCompatibleStart(2); |
||||
|
NSMutableArray *faceObjects = [NSMutableArray array]; |
||||
|
[metadataObjects |
||||
|
enumerateObjectsUsingBlock:^(__kindof AVMetadataObject *_Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) { |
||||
|
if ([obj isKindOfClass:[AVMetadataFaceObject class]]) { |
||||
|
[faceObjects addObject:obj]; |
||||
|
} |
||||
|
}]; |
||||
|
|
||||
|
SC_GUARD_ELSE_RETURN_VALUE(faceObjects.count > 0, nil); |
||||
|
|
||||
|
NSMutableDictionary<NSNumber *, NSValue *> *faceBoundsByFaceID = |
||||
|
[NSMutableDictionary dictionaryWithCapacity:faceObjects.count]; |
||||
|
for (AVMetadataFaceObject *faceObject in faceObjects) { |
||||
|
CGRect bounds = faceObject.bounds; |
||||
|
if (CGRectGetWidth(bounds) * CGRectGetHeight(bounds) >= _minimumArea) { |
||||
|
[faceBoundsByFaceID setObject:[NSValue valueWithCGRect:bounds] forKey:@(faceObject.faceID)]; |
||||
|
} |
||||
|
} |
||||
|
return faceBoundsByFaceID; |
||||
|
} |
||||
|
|
||||
|
- (NSDictionary<NSNumber *, NSValue *> *)parseFaceBoundsByFaceIDFromCIFeatures:(NSArray<__kindof CIFeature *> *)features |
||||
|
withImageSize:(CGSize)imageSize |
||||
|
imageOrientation: |
||||
|
(CGImagePropertyOrientation)imageOrientation |
||||
|
{ |
||||
|
SCTraceODPCompatibleStart(2); |
||||
|
NSArray<CIFaceFeature *> *faceFeatures = [features filteredArrayUsingBlock:^BOOL(id _Nonnull evaluatedObject) { |
||||
|
return [evaluatedObject isKindOfClass:[CIFaceFeature class]]; |
||||
|
}]; |
||||
|
|
||||
|
SC_GUARD_ELSE_RETURN_VALUE(faceFeatures.count > 0, nil); |
||||
|
|
||||
|
NSMutableDictionary<NSNumber *, NSValue *> *faceBoundsByFaceID = |
||||
|
[NSMutableDictionary dictionaryWithCapacity:faceFeatures.count]; |
||||
|
CGFloat width = imageSize.width; |
||||
|
CGFloat height = imageSize.height; |
||||
|
SCLogGeneralInfo(@"Face feature count:%d", faceFeatures.count); |
||||
|
for (CIFaceFeature *faceFeature in faceFeatures) { |
||||
|
SCLogGeneralInfo(@"Face feature: hasTrackingID:%d, bounds:%@", faceFeature.hasTrackingID, |
||||
|
NSStringFromCGRect(faceFeature.bounds)); |
||||
|
if (faceFeature.hasTrackingID) { |
||||
|
CGRect transferredBounds; |
||||
|
// Somehow the detected bounds for back camera is mirrored. |
||||
|
if (imageOrientation == kCGImagePropertyOrientationRight) { |
||||
|
transferredBounds = CGRectMake( |
||||
|
CGRectGetMinX(faceFeature.bounds) / width, 1 - CGRectGetMaxY(faceFeature.bounds) / height, |
||||
|
CGRectGetWidth(faceFeature.bounds) / width, CGRectGetHeight(faceFeature.bounds) / height); |
||||
|
} else { |
||||
|
transferredBounds = CGRectMake( |
||||
|
CGRectGetMinX(faceFeature.bounds) / width, CGRectGetMinY(faceFeature.bounds) / height, |
||||
|
CGRectGetWidth(faceFeature.bounds) / width, CGRectGetHeight(faceFeature.bounds) / height); |
||||
|
} |
||||
|
if (CGRectGetWidth(transferredBounds) * CGRectGetHeight(transferredBounds) >= _minimumArea) { |
||||
|
[faceBoundsByFaceID setObject:[NSValue valueWithCGRect:transferredBounds] |
||||
|
forKey:@(faceFeature.trackingID)]; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
return faceBoundsByFaceID; |
||||
|
} |
||||
|
|
||||
|
@end |
@ -0,0 +1,31 @@ |
|||||
|
// |
||||
|
// SCCaptureFaceDetector.h |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by Jiyang Zhu on 3/27/18. |
||||
|
// Copyright © 2018 Snapchat, Inc. All rights reserved. |
||||
|
// |
||||
|
// This protocol declares properties and methods that are used for face detectors. |
||||
|
|
||||
|
#import <Foundation/Foundation.h> |
||||
|
|
||||
|
@class SCCaptureResource; |
||||
|
@class SCQueuePerformer; |
||||
|
@class SCCaptureFaceDetectorTrigger; |
||||
|
@class SCCaptureFaceDetectionParser; |
||||
|
|
||||
|
@protocol SCCaptureFaceDetector <NSObject> |
||||
|
|
||||
|
@property (nonatomic, strong, readonly) SCCaptureFaceDetectorTrigger *trigger; |
||||
|
|
||||
|
@property (nonatomic, strong, readonly) SCCaptureFaceDetectionParser *parser; |
||||
|
|
||||
|
- (instancetype)initWithCaptureResource:(SCCaptureResource *)captureResource; |
||||
|
|
||||
|
- (SCQueuePerformer *)detectionPerformer; |
||||
|
|
||||
|
- (void)startDetection; |
||||
|
|
||||
|
- (void)stopDetection; |
||||
|
|
||||
|
@end |
@ -0,0 +1,22 @@ |
|||||
|
// |
||||
|
// SCCaptureFaceDetectorTrigger.h |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by Jiyang Zhu on 3/22/18. |
||||
|
// Copyright © 2018 Snapchat, Inc. All rights reserved. |
||||
|
// |
||||
|
// This class is used to control when should SCCaptureFaceDetector starts and stops. |
||||
|
|
||||
|
#import <SCBase/SCMacros.h> |
||||
|
|
||||
|
#import <Foundation/Foundation.h> |
||||
|
|
||||
|
@protocol SCCaptureFaceDetector; |
||||
|
|
||||
|
@interface SCCaptureFaceDetectorTrigger : NSObject |
||||
|
|
||||
|
SC_INIT_AND_NEW_UNAVAILABLE; |
||||
|
|
||||
|
- (instancetype)initWithDetector:(id<SCCaptureFaceDetector>)detector; |
||||
|
|
||||
|
@end |
@ -0,0 +1,97 @@ |
|||||
|
// |
||||
|
// SCCaptureFaceDetectorTrigger.m |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by Jiyang Zhu on 3/22/18. |
||||
|
// Copyright © 2018 Snapchat, Inc. All rights reserved. |
||||
|
// |
||||
|
|
||||
|
#import "SCCaptureFaceDetectorTrigger.h" |
||||
|
|
||||
|
#import "SCCaptureFaceDetector.h" |
||||
|
|
||||
|
#import <SCFoundation/SCAppLifecycle.h> |
||||
|
#import <SCFoundation/SCIdleMonitor.h> |
||||
|
#import <SCFoundation/SCQueuePerformer.h> |
||||
|
#import <SCFoundation/SCTaskManager.h> |
||||
|
#import <SCFoundation/SCTraceODPCompatible.h> |
||||
|
|
||||
|
@interface SCCaptureFaceDetectorTrigger () { |
||||
|
id<SCCaptureFaceDetector> __weak _detector; |
||||
|
} |
||||
|
@end |
||||
|
|
||||
|
@implementation SCCaptureFaceDetectorTrigger |
||||
|
|
||||
|
- (instancetype)initWithDetector:(id<SCCaptureFaceDetector>)detector |
||||
|
{ |
||||
|
self = [super init]; |
||||
|
if (self) { |
||||
|
_detector = detector; |
||||
|
[[NSNotificationCenter defaultCenter] addObserver:self |
||||
|
selector:@selector(_applicationDidBecomeActive) |
||||
|
name:kSCPostponedUIApplicationDidBecomeActiveNotification |
||||
|
object:nil]; |
||||
|
[[NSNotificationCenter defaultCenter] addObserver:self |
||||
|
selector:@selector(_applicationWillResignActive) |
||||
|
name:UIApplicationWillResignActiveNotification |
||||
|
object:nil]; |
||||
|
} |
||||
|
return self; |
||||
|
} |
||||
|
|
||||
|
#pragma mark - Internal Methods |
||||
|
- (void)_applicationWillResignActive |
||||
|
{ |
||||
|
SCTraceODPCompatibleStart(2); |
||||
|
[self _stopDetection]; |
||||
|
} |
||||
|
|
||||
|
- (void)_applicationDidBecomeActive |
||||
|
{ |
||||
|
SCTraceODPCompatibleStart(2); |
||||
|
[self _waitUntilAppStartCompleteToStartDetection]; |
||||
|
} |
||||
|
|
||||
|
- (void)_waitUntilAppStartCompleteToStartDetection |
||||
|
{ |
||||
|
SCTraceODPCompatibleStart(2); |
||||
|
@weakify(self); |
||||
|
|
||||
|
if (SCExperimentWithWaitUntilIdleReplacement()) { |
||||
|
[[SCTaskManager sharedManager] addTaskToRunWhenAppIdle:"SCCaptureFaceDetectorTrigger.startDetection" |
||||
|
performer:[_detector detectionPerformer] |
||||
|
block:^{ |
||||
|
@strongify(self); |
||||
|
SC_GUARD_ELSE_RETURN(self); |
||||
|
|
||||
|
[self _startDetection]; |
||||
|
}]; |
||||
|
} else { |
||||
|
[[SCIdleMonitor sharedInstance] waitUntilIdleForTag:"SCCaptureFaceDetectorTrigger.startDetection" |
||||
|
callbackQueue:[_detector detectionPerformer].queue |
||||
|
block:^{ |
||||
|
@strongify(self); |
||||
|
SC_GUARD_ELSE_RETURN(self); |
||||
|
[self _startDetection]; |
||||
|
}]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
- (void)_startDetection |
||||
|
{ |
||||
|
SCTraceODPCompatibleStart(2); |
||||
|
[[_detector detectionPerformer] performImmediatelyIfCurrentPerformer:^{ |
||||
|
[_detector startDetection]; |
||||
|
}]; |
||||
|
} |
||||
|
|
||||
|
- (void)_stopDetection |
||||
|
{ |
||||
|
SCTraceODPCompatibleStart(2); |
||||
|
[[_detector detectionPerformer] performImmediatelyIfCurrentPerformer:^{ |
||||
|
[_detector stopDetection]; |
||||
|
}]; |
||||
|
} |
||||
|
|
||||
|
@end |
@ -0,0 +1,23 @@ |
|||||
|
// |
||||
|
// SCCaptureMetadataObjectParser.h |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by Jiyang Zhu on 3/13/18. |
||||
|
// Copyright © 2018 Snapchat, Inc. All rights reserved. |
||||
|
// |
||||
|
// This class offers class methods to parse AVMetadataObject. |
||||
|
|
||||
|
#import <AVFoundation/AVFoundation.h> |
||||
|
|
||||
|
@interface SCCaptureMetadataObjectParser : NSObject |
||||
|
|
||||
|
/** |
||||
|
Parse face bounds from AVMetadataObject. |
||||
|
|
||||
|
@param metadataObjects An array of AVMetadataObject. |
||||
|
@return A dictionary, value is faceBounds: CGRect, key is faceID: NSString. |
||||
|
*/ |
||||
|
- (NSDictionary<NSNumber *, NSValue *> *)parseFaceBoundsByFaceIDFromMetadataObjects: |
||||
|
(NSArray<__kindof AVMetadataObject *> *)metadataObjects; |
||||
|
|
||||
|
@end |
@ -0,0 +1,38 @@ |
|||||
|
// |
||||
|
// SCCaptureMetadataObjectParser.m |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by Jiyang Zhu on 3/13/18. |
||||
|
// Copyright © 2018 Snapchat, Inc. All rights reserved. |
||||
|
// |
||||
|
|
||||
|
#import "SCCaptureMetadataObjectParser.h" |
||||
|
|
||||
|
#import <SCBase/SCMacros.h> |
||||
|
|
||||
|
@import UIKit; |
||||
|
|
||||
|
@implementation SCCaptureMetadataObjectParser |
||||
|
|
||||
|
- (NSDictionary<NSNumber *, NSValue *> *)parseFaceBoundsByFaceIDFromMetadataObjects: |
||||
|
(NSArray<__kindof AVMetadataObject *> *)metadataObjects |
||||
|
{ |
||||
|
NSMutableArray *faceObjects = [NSMutableArray array]; |
||||
|
[metadataObjects |
||||
|
enumerateObjectsUsingBlock:^(__kindof AVMetadataObject *_Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) { |
||||
|
if ([obj isKindOfClass:[AVMetadataFaceObject class]]) { |
||||
|
[faceObjects addObject:obj]; |
||||
|
} |
||||
|
}]; |
||||
|
|
||||
|
SC_GUARD_ELSE_RETURN_VALUE(faceObjects.count > 0, nil); |
||||
|
|
||||
|
NSMutableDictionary<NSNumber *, NSValue *> *faceBoundsByFaceID = |
||||
|
[NSMutableDictionary dictionaryWithCapacity:faceObjects.count]; |
||||
|
for (AVMetadataFaceObject *faceObject in faceObjects) { |
||||
|
[faceBoundsByFaceID setObject:[NSValue valueWithCGRect:faceObject.bounds] forKey:@(faceObject.faceID)]; |
||||
|
} |
||||
|
return faceBoundsByFaceID; |
||||
|
} |
||||
|
|
||||
|
@end |
@ -0,0 +1,19 @@ |
|||||
|
// |
||||
|
// SCCaptureMetadataOutputDetector.h |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by Jiyang Zhu on 12/21/17. |
||||
|
// Copyright © 2017 Snapchat, Inc. All rights reserved. |
||||
|
// |
||||
|
// This class is intended to detect faces in Camera. It receives AVMetadataFaceObjects, and announce the bounds and |
||||
|
// faceIDs. |
||||
|
|
||||
|
#import "SCCaptureFaceDetector.h" |
||||
|
|
||||
|
#import <SCBase/SCMacros.h> |
||||
|
|
||||
|
@interface SCCaptureMetadataOutputDetector : NSObject <SCCaptureFaceDetector> |
||||
|
|
||||
|
SC_INIT_AND_NEW_UNAVAILABLE; |
||||
|
|
||||
|
@end |
@ -0,0 +1,175 @@ |
|||||
|
// |
||||
|
// SCCaptureMetadataOutputDetector.m |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by Jiyang Zhu on 12/21/17. |
||||
|
// Copyright © 2017 Snapchat, Inc. All rights reserved. |
||||
|
// |
||||
|
|
||||
|
#import "SCCaptureMetadataOutputDetector.h" |
||||
|
|
||||
|
#import "SCCameraTweaks.h" |
||||
|
#import "SCCaptureFaceDetectionParser.h" |
||||
|
#import "SCCaptureFaceDetectorTrigger.h" |
||||
|
#import "SCCaptureResource.h" |
||||
|
#import "SCManagedCaptureSession.h" |
||||
|
#import "SCManagedCapturer.h" |
||||
|
|
||||
|
#import <SCFoundation/SCAssertWrapper.h> |
||||
|
#import <SCFoundation/SCLog.h> |
||||
|
#import <SCFoundation/SCQueuePerformer.h> |
||||
|
#import <SCFoundation/SCTrace.h> |
||||
|
#import <SCFoundation/SCTraceODPCompatible.h> |
||||
|
#import <SCFoundation/SCZeroDependencyExperiments.h> |
||||
|
#import <SCFoundation/UIImage+CVPixelBufferRef.h> |
||||
|
|
||||
|
#define SCLogCaptureMetaDetectorInfo(fmt, ...) \ |
||||
|
SCLogCoreCameraInfo(@"[SCCaptureMetadataOutputDetector] " fmt, ##__VA_ARGS__) |
||||
|
#define SCLogCaptureMetaDetectorWarning(fmt, ...) \ |
||||
|
SCLogCoreCameraWarning(@"[SCCaptureMetadataOutputDetector] " fmt, ##__VA_ARGS__) |
||||
|
#define SCLogCaptureMetaDetectorError(fmt, ...) \ |
||||
|
SCLogCoreCameraError(@"[SCCaptureMetadataOutputDetector] " fmt, ##__VA_ARGS__) |
||||
|
|
||||
|
static char *const kSCCaptureMetadataOutputDetectorProcessQueue = |
||||
|
"com.snapchat.capture-metadata-output-detector-process"; |
||||
|
|
||||
|
static const NSInteger kDefaultNumberOfSequentialFramesWithFaces = -1; // -1 means no sequential frames with faces. |
||||
|
|
||||
|
@interface SCCaptureMetadataOutputDetector () <AVCaptureMetadataOutputObjectsDelegate> |
||||
|
|
||||
|
@end |
||||
|
|
||||
|
@implementation SCCaptureMetadataOutputDetector { |
||||
|
BOOL _isDetecting; |
||||
|
|
||||
|
AVCaptureMetadataOutput *_metadataOutput; |
||||
|
SCCaptureResource *_captureResource; |
||||
|
|
||||
|
SCCaptureFaceDetectionParser *_parser; |
||||
|
NSInteger _numberOfSequentialFramesWithFaces; |
||||
|
NSUInteger _detectionFrequency; |
||||
|
|
||||
|
SCQueuePerformer *_callbackPerformer; |
||||
|
SCQueuePerformer *_metadataProcessPerformer; |
||||
|
|
||||
|
SCCaptureFaceDetectorTrigger *_trigger; |
||||
|
} |
||||
|
|
||||
|
@synthesize trigger = _trigger; |
||||
|
@synthesize parser = _parser; |
||||
|
|
||||
|
- (instancetype)initWithCaptureResource:(SCCaptureResource *)captureResource |
||||
|
{ |
||||
|
SCTraceODPCompatibleStart(2); |
||||
|
self = [super init]; |
||||
|
if (self) { |
||||
|
SCAssert(captureResource, @"SCCaptureResource should not be nil"); |
||||
|
SCAssert(captureResource.managedSession.avSession, @"AVCaptureSession should not be nil"); |
||||
|
SCAssert(captureResource.queuePerformer, @"SCQueuePerformer should not be nil"); |
||||
|
_metadataOutput = [AVCaptureMetadataOutput new]; |
||||
|
_callbackPerformer = captureResource.queuePerformer; |
||||
|
_captureResource = captureResource; |
||||
|
_detectionFrequency = SCExperimentWithFaceDetectionFrequency(); |
||||
|
|
||||
|
_parser = [[SCCaptureFaceDetectionParser alloc] |
||||
|
initWithFaceBoundsAreaThreshold:pow(SCCameraFaceFocusMinFaceSize(), 2)]; |
||||
|
_metadataProcessPerformer = [[SCQueuePerformer alloc] initWithLabel:kSCCaptureMetadataOutputDetectorProcessQueue |
||||
|
qualityOfService:QOS_CLASS_DEFAULT |
||||
|
queueType:DISPATCH_QUEUE_SERIAL |
||||
|
context:SCQueuePerformerContextCamera]; |
||||
|
if ([self _initDetection]) { |
||||
|
_trigger = [[SCCaptureFaceDetectorTrigger alloc] initWithDetector:self]; |
||||
|
} |
||||
|
} |
||||
|
return self; |
||||
|
} |
||||
|
|
||||
|
- (AVCaptureSession *)_captureSession |
||||
|
{ |
||||
|
// _captureResource.avSession may change, so we don't retain any specific AVCaptureSession. |
||||
|
return _captureResource.managedSession.avSession; |
||||
|
} |
||||
|
|
||||
|
- (BOOL)_initDetection |
||||
|
{ |
||||
|
BOOL success = NO; |
||||
|
if ([[self _captureSession] canAddOutput:_metadataOutput]) { |
||||
|
[[self _captureSession] addOutput:_metadataOutput]; |
||||
|
if ([_metadataOutput.availableMetadataObjectTypes containsObject:AVMetadataObjectTypeFace]) { |
||||
|
_numberOfSequentialFramesWithFaces = kDefaultNumberOfSequentialFramesWithFaces; |
||||
|
_metadataOutput.metadataObjectTypes = @[ AVMetadataObjectTypeFace ]; |
||||
|
success = YES; |
||||
|
SCLogCaptureMetaDetectorInfo(@"AVMetadataObjectTypeFace detection successfully enabled."); |
||||
|
} else { |
||||
|
[[self _captureSession] removeOutput:_metadataOutput]; |
||||
|
success = NO; |
||||
|
SCLogCaptureMetaDetectorError(@"AVMetadataObjectTypeFace is not available for " |
||||
|
@"AVMetadataOutput[%@]", |
||||
|
_metadataOutput); |
||||
|
} |
||||
|
} else { |
||||
|
success = NO; |
||||
|
SCLogCaptureMetaDetectorError(@"AVCaptureSession[%@] cannot add AVMetadataOutput[%@] as an output", |
||||
|
[self _captureSession], _metadataOutput); |
||||
|
} |
||||
|
return success; |
||||
|
} |
||||
|
|
||||
|
- (void)startDetection |
||||
|
{ |
||||
|
SCAssert([[self detectionPerformer] isCurrentPerformer], @"Calling -startDetection in an invalid queue."); |
||||
|
SC_GUARD_ELSE_RETURN(!_isDetecting); |
||||
|
[_captureResource.queuePerformer performImmediatelyIfCurrentPerformer:^{ |
||||
|
[_metadataOutput setMetadataObjectsDelegate:self queue:_metadataProcessPerformer.queue]; |
||||
|
_isDetecting = YES; |
||||
|
SCLogCaptureMetaDetectorInfo(@"AVMetadataObjectTypeFace detection successfully enabled."); |
||||
|
}]; |
||||
|
} |
||||
|
|
||||
|
- (void)stopDetection |
||||
|
{ |
||||
|
SCAssert([[self detectionPerformer] isCurrentPerformer], @"Calling -stopDetection in an invalid queue."); |
||||
|
SC_GUARD_ELSE_RETURN(_isDetecting); |
||||
|
[_captureResource.queuePerformer performImmediatelyIfCurrentPerformer:^{ |
||||
|
[_metadataOutput setMetadataObjectsDelegate:nil queue:NULL]; |
||||
|
_isDetecting = NO; |
||||
|
SCLogCaptureMetaDetectorInfo(@"AVMetadataObjectTypeFace detection successfully disabled."); |
||||
|
}]; |
||||
|
} |
||||
|
|
||||
|
- (SCQueuePerformer *)detectionPerformer |
||||
|
{ |
||||
|
return _captureResource.queuePerformer; |
||||
|
} |
||||
|
|
||||
|
#pragma mark - AVCaptureMetadataOutputObjectsDelegate |
||||
|
- (void)captureOutput:(AVCaptureOutput *)output |
||||
|
didOutputMetadataObjects:(NSArray<__kindof AVMetadataObject *> *)metadataObjects |
||||
|
fromConnection:(AVCaptureConnection *)connection |
||||
|
{ |
||||
|
SCTraceODPCompatibleStart(2); |
||||
|
|
||||
|
BOOL shouldNotify = NO; |
||||
|
if (metadataObjects.count == 0 && |
||||
|
_numberOfSequentialFramesWithFaces != |
||||
|
kDefaultNumberOfSequentialFramesWithFaces) { // There were faces detected before, but there is no face right |
||||
|
// now, so send out the notification. |
||||
|
_numberOfSequentialFramesWithFaces = kDefaultNumberOfSequentialFramesWithFaces; |
||||
|
shouldNotify = YES; |
||||
|
} else if (metadataObjects.count > 0) { |
||||
|
_numberOfSequentialFramesWithFaces++; |
||||
|
shouldNotify = (_numberOfSequentialFramesWithFaces % _detectionFrequency == 0); |
||||
|
} |
||||
|
|
||||
|
SC_GUARD_ELSE_RETURN(shouldNotify); |
||||
|
|
||||
|
NSDictionary<NSNumber *, NSValue *> *faceBoundsByFaceID = |
||||
|
[_parser parseFaceBoundsByFaceIDFromMetadataObjects:metadataObjects]; |
||||
|
|
||||
|
[_callbackPerformer perform:^{ |
||||
|
[_captureResource.announcer managedCapturer:[SCManagedCapturer sharedInstance] |
||||
|
didDetectFaceBounds:faceBoundsByFaceID]; |
||||
|
}]; |
||||
|
} |
||||
|
|
||||
|
@end |
@ -0,0 +1,225 @@ |
|||||
|
// |
||||
|
// SCManagedCapturer.h |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by Liu Liu on 4/20/15. |
||||
|
// Copyright (c) 2015 Liu Liu. All rights reserved. |
||||
|
// |
||||
|
|
||||
|
#import "SCCaptureCommon.h" |
||||
|
#import "SCSnapCreationTriggers.h" |
||||
|
|
||||
|
#import <SCAudio/SCAudioConfiguration.h> |
||||
|
|
||||
|
#import <AVFoundation/AVFoundation.h> |
||||
|
#import <Foundation/Foundation.h> |
||||
|
|
||||
|
#define SCCapturerContext [NSString sc_stringWithFormat:@"%s/%d", __FUNCTION__, __LINE__] |
||||
|
|
||||
|
@class SCBlackCameraDetector; |
||||
|
@protocol SCManagedCapturerListener |
||||
|
, SCManagedCapturerLensAPI, SCDeviceMotionProvider, SCFileInputDecider, SCManagedCapturerARImageCaptureProvider, |
||||
|
SCManagedCapturerGLViewManagerAPI, SCManagedCapturerLensAPIProvider, SCManagedCapturerLSAComponentTrackerAPI, |
||||
|
SCManagedCapturePreviewLayerControllerDelegate; |
||||
|
|
||||
|
@protocol SCCapturer <NSObject> |
||||
|
|
||||
|
@property (nonatomic, readonly) SCBlackCameraDetector *blackCameraDetector; |
||||
|
|
||||
|
/** |
||||
|
* Returns id<SCLensProcessingCore> for the current capturer. |
||||
|
*/ |
||||
|
- (id<SCManagedCapturerLensAPI>)lensProcessingCore; |
||||
|
|
||||
|
- (CMTime)firstWrittenAudioBufferDelay; |
||||
|
- (BOOL)audioQueueStarted; |
||||
|
- (BOOL)isLensApplied; |
||||
|
- (BOOL)isVideoMirrored; |
||||
|
|
||||
|
- (SCVideoCaptureSessionInfo)activeSession; |
||||
|
|
||||
|
#pragma mark - Outside resources |
||||
|
|
||||
|
- (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; |
||||
|
|
||||
|
#pragma mark - Setup, Start & Stop |
||||
|
|
||||
|
// setupWithDevicePositionAsynchronously will be called on the main thread, executed off the main thread, exactly once |
||||
|
- (void)setupWithDevicePositionAsynchronously:(SCManagedCaptureDevicePosition)devicePosition |
||||
|
completionHandler:(dispatch_block_t)completionHandler |
||||
|
context:(NSString *)context; |
||||
|
|
||||
|
/** |
||||
|
* Important: Remember to call stopRunningAsynchronously to stop the capture session. Dismissing the view is not enough |
||||
|
* @param identifier is for knowing the callsite. Pass in the classname of the callsite is generally suggested. |
||||
|
* Currently it is used for debugging purposes. In other words the capture session will work without it. |
||||
|
*/ |
||||
|
- (SCCapturerToken *)startRunningAsynchronouslyWithCompletionHandler:(dispatch_block_t)completionHandler |
||||
|
context:(NSString *)context; |
||||
|
- (void)stopRunningAsynchronously:(SCCapturerToken *)token |
||||
|
completionHandler:(sc_managed_capturer_stop_running_completion_handler_t)completionHandler |
||||
|
context:(NSString *)context; |
||||
|
|
||||
|
- (void)stopRunningAsynchronously:(SCCapturerToken *)token |
||||
|
completionHandler:(sc_managed_capturer_stop_running_completion_handler_t)completionHandler |
||||
|
after:(NSTimeInterval)delay |
||||
|
context:(NSString *)context; |
||||
|
|
||||
|
- (void)startStreamingAsynchronouslyWithCompletionHandler:(dispatch_block_t)completionHandler |
||||
|
context:(NSString *)context; |
||||
|
|
||||
|
- (void)addSampleBufferDisplayController:(id<SCManagedSampleBufferDisplayController>)sampleBufferDisplayController |
||||
|
context:(NSString *)context; |
||||
|
|
||||
|
#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; |
||||
|
/** |
||||
|
* Unlike captureStillImageAsynchronouslyWithAspectRatio, this captures a single frame from the ongoing video |
||||
|
* stream. This should be faster but lower quality (and smaller size), and does not play the shutter sound. |
||||
|
*/ |
||||
|
- (void)captureSingleVideoFrameAsynchronouslyWithCompletionHandler: |
||||
|
(sc_managed_capturer_capture_video_frame_completion_handler_t)completionHandler |
||||
|
context:(NSString *)context; |
||||
|
|
||||
|
- (void)prepareForRecordingAsynchronouslyWithContext:(NSString *)context |
||||
|
audioConfiguration:(SCAudioConfiguration *)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; |
||||
|
- (void)stopRecordingAsynchronouslyWithContext:(NSString *)context; |
||||
|
- (void)cancelRecordingAsynchronouslyWithContext:(NSString *)context; |
||||
|
|
||||
|
- (void)startScanAsynchronouslyWithScanConfiguration:(SCScanConfiguration *)configuration context:(NSString *)context; |
||||
|
- (void)stopScanAsynchronouslyWithCompletionHandler:(dispatch_block_t)completionHandler context:(NSString *)context; |
||||
|
- (void)sampleFrameWithCompletionHandler:(void (^)(UIImage *frame, CMTime presentationTime))completionHandler |
||||
|
context:(NSString *)context; |
||||
|
|
||||
|
// AddTimedTask will schedule a task to run, it is thread safe API. Your task will run on main thread, so it is not |
||||
|
// recommended to add large amount of tasks which all have the same task target time. |
||||
|
- (void)addTimedTask:(SCTimedTask *)task context:(NSString *)context; |
||||
|
|
||||
|
// clearTimedTasks will cancel the tasks, it is thread safe API. |
||||
|
- (void)clearTimedTasksWithContext:(NSString *)context; |
||||
|
|
||||
|
#pragma mark - Utilities |
||||
|
|
||||
|
- (void)convertViewCoordinates:(CGPoint)viewCoordinates |
||||
|
completionHandler:(sc_managed_capturer_convert_view_coordniates_completion_handler_t)completionHandler |
||||
|
context:(NSString *)context; |
||||
|
|
||||
|
- (void)detectLensCategoryOnNextFrame:(CGPoint)point |
||||
|
lenses:(NSArray<SCLens *> *)lenses |
||||
|
completion:(sc_managed_lenses_processor_category_point_completion_handler_t)completion |
||||
|
context:(NSString *)context; |
||||
|
|
||||
|
#pragma mark - Configurations |
||||
|
|
||||
|
- (void)setDevicePositionAsynchronously:(SCManagedCaptureDevicePosition)devicePosition |
||||
|
completionHandler:(dispatch_block_t)completionHandler |
||||
|
context:(NSString *)context; |
||||
|
|
||||
|
- (void)setFlashActive:(BOOL)flashActive |
||||
|
completionHandler:(dispatch_block_t)completionHandler |
||||
|
context:(NSString *)context; |
||||
|
|
||||
|
- (void)setLensesActive:(BOOL)lensesActive |
||||
|
completionHandler:(dispatch_block_t)completionHandler |
||||
|
context:(NSString *)context; |
||||
|
|
||||
|
- (void)setLensesActive:(BOOL)lensesActive |
||||
|
filterFactory:(SCLookseryFilterFactory *)filterFactory |
||||
|
completionHandler:(dispatch_block_t)completionHandler |
||||
|
context:(NSString *)context; |
||||
|
|
||||
|
- (void)setLensesInTalkActive:(BOOL)lensesActive |
||||
|
completionHandler:(dispatch_block_t)completionHandler |
||||
|
context:(NSString *)context; |
||||
|
|
||||
|
- (void)setTorchActiveAsynchronously:(BOOL)torchActive |
||||
|
completionHandler:(dispatch_block_t)completionHandler |
||||
|
context:(NSString *)context; |
||||
|
|
||||
|
- (void)setNightModeActiveAsynchronously:(BOOL)active |
||||
|
completionHandler:(dispatch_block_t)completionHandler |
||||
|
context:(NSString *)context; |
||||
|
|
||||
|
- (void)lockZoomWithContext:(NSString *)context; |
||||
|
|
||||
|
- (void)unlockZoomWithContext:(NSString *)context; |
||||
|
|
||||
|
- (void)setZoomFactorAsynchronously:(CGFloat)zoomFactor context:(NSString *)context; |
||||
|
- (void)resetZoomFactorAsynchronously:(CGFloat)zoomFactor |
||||
|
devicePosition:(SCManagedCaptureDevicePosition)devicePosition |
||||
|
context:(NSString *)context; |
||||
|
|
||||
|
- (void)setExposurePointOfInterestAsynchronously:(CGPoint)pointOfInterest |
||||
|
fromUser:(BOOL)fromUser |
||||
|
completionHandler:(dispatch_block_t)completionHandler |
||||
|
context:(NSString *)context; |
||||
|
|
||||
|
- (void)setAutofocusPointOfInterestAsynchronously:(CGPoint)pointOfInterest |
||||
|
completionHandler:(dispatch_block_t)completionHandler |
||||
|
context:(NSString *)context; |
||||
|
|
||||
|
- (void)setPortraitModePointOfInterestAsynchronously:(CGPoint)pointOfInterest |
||||
|
completionHandler:(dispatch_block_t)completionHandler |
||||
|
context:(NSString *)context; |
||||
|
|
||||
|
- (void)continuousAutofocusAndExposureAsynchronouslyWithCompletionHandler:(dispatch_block_t)completionHandler |
||||
|
context:(NSString *)context; |
||||
|
|
||||
|
// I need to call these three methods from SCAppDelegate explicitly so that I get the latest information. |
||||
|
- (void)applicationDidEnterBackground; |
||||
|
- (void)applicationWillEnterForeground; |
||||
|
- (void)applicationDidBecomeActive; |
||||
|
- (void)applicationWillResignActive; |
||||
|
- (void)mediaServicesWereReset; |
||||
|
- (void)mediaServicesWereLost; |
||||
|
|
||||
|
#pragma mark - Add / Remove Listener |
||||
|
|
||||
|
- (void)addListener:(id<SCManagedCapturerListener>)listener; |
||||
|
- (void)removeListener:(id<SCManagedCapturerListener>)listener; |
||||
|
- (void)addVideoDataSourceListener:(id<SCManagedVideoDataSourceListener>)listener; |
||||
|
- (void)removeVideoDataSourceListener:(id<SCManagedVideoDataSourceListener>)listener; |
||||
|
- (void)addDeviceCapacityAnalyzerListener:(id<SCManagedDeviceCapacityAnalyzerListener>)listener; |
||||
|
- (void)removeDeviceCapacityAnalyzerListener:(id<SCManagedDeviceCapacityAnalyzerListener>)listener; |
||||
|
|
||||
|
- (NSString *)debugInfo; |
||||
|
|
||||
|
- (id<SCManagedVideoDataSource>)currentVideoDataSource; |
||||
|
|
||||
|
- (void)checkRestrictedCamera:(void (^)(BOOL, BOOL, AVAuthorizationStatus))callback; |
||||
|
|
||||
|
// Need to be visible so that classes like SCCaptureSessionFixer can manage capture session |
||||
|
- (void)recreateAVCaptureSession; |
||||
|
|
||||
|
#pragma mark - Snap Creation triggers |
||||
|
|
||||
|
- (SCSnapCreationTriggers *)snapCreationTriggers; |
||||
|
|
||||
|
@optional |
||||
|
|
||||
|
- (BOOL)authorizedForVideoCapture; |
||||
|
|
||||
|
- (void)preloadVideoCaptureAuthorization; |
||||
|
|
||||
|
@end |
@ -0,0 +1,44 @@ |
|||||
|
// |
||||
|
// SCCapturerBufferedVideoWriter.h |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by Chao Pang on 12/5/17. |
||||
|
// |
||||
|
|
||||
|
#import <SCFoundation/SCQueuePerformer.h> |
||||
|
|
||||
|
#import <SCManagedVideoCapturerOutputSettings.h> |
||||
|
|
||||
|
#import <AVFoundation/AVFoundation.h> |
||||
|
#import <Foundation/Foundation.h> |
||||
|
|
||||
|
@protocol SCCapturerBufferedVideoWriterDelegate <NSObject> |
||||
|
|
||||
|
- (void)videoWriterDidFailWritingWithError:(NSError *)error; |
||||
|
|
||||
|
@end |
||||
|
|
||||
|
@interface SCCapturerBufferedVideoWriter : NSObject |
||||
|
|
||||
|
- (instancetype)init NS_UNAVAILABLE; |
||||
|
|
||||
|
- (instancetype)initWithPerformer:(id<SCPerforming>)performer |
||||
|
outputURL:(NSURL *)outputURL |
||||
|
delegate:(id<SCCapturerBufferedVideoWriterDelegate>)delegate |
||||
|
error:(NSError **)error; |
||||
|
|
||||
|
- (BOOL)prepareWritingWithOutputSettings:(SCManagedVideoCapturerOutputSettings *)outputSettings; |
||||
|
|
||||
|
- (void)startWritingAtSourceTime:(CMTime)sourceTime; |
||||
|
|
||||
|
- (void)finishWritingAtSourceTime:(CMTime)sourceTime withCompletionHanlder:(dispatch_block_t)completionBlock; |
||||
|
|
||||
|
- (void)cancelWriting; |
||||
|
|
||||
|
- (void)appendVideoSampleBuffer:(CMSampleBufferRef)sampleBuffer; |
||||
|
|
||||
|
- (void)appendAudioSampleBuffer:(CMSampleBufferRef)sampleBuffer; |
||||
|
|
||||
|
- (void)cleanUp; |
||||
|
|
||||
|
@end |
@ -0,0 +1,430 @@ |
|||||
|
// |
||||
|
// SCCapturerBufferedVideoWriter.m |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by Chao Pang on 12/5/17. |
||||
|
// |
||||
|
|
||||
|
#import "SCCapturerBufferedVideoWriter.h" |
||||
|
|
||||
|
#import "SCAudioCaptureSession.h" |
||||
|
#import "SCCaptureCommon.h" |
||||
|
#import "SCManagedCapturerUtils.h" |
||||
|
|
||||
|
#import <SCBase/SCMacros.h> |
||||
|
#import <SCFoundation/SCAssertWrapper.h> |
||||
|
#import <SCFoundation/SCDeviceName.h> |
||||
|
#import <SCFoundation/SCLog.h> |
||||
|
#import <SCFoundation/SCTrace.h> |
||||
|
|
||||
|
#import <FBKVOController/FBKVOController.h> |
||||
|
|
||||
|
@implementation SCCapturerBufferedVideoWriter { |
||||
|
SCQueuePerformer *_performer; |
||||
|
__weak id<SCCapturerBufferedVideoWriterDelegate> _delegate; |
||||
|
FBKVOController *_observeController; |
||||
|
|
||||
|
AVAssetWriter *_assetWriter; |
||||
|
AVAssetWriterInput *_audioWriterInput; |
||||
|
AVAssetWriterInput *_videoWriterInput; |
||||
|
AVAssetWriterInputPixelBufferAdaptor *_pixelBufferAdaptor; |
||||
|
CVPixelBufferPoolRef _defaultPixelBufferPool; |
||||
|
CVPixelBufferPoolRef _nightPixelBufferPool; |
||||
|
CVPixelBufferPoolRef _lensesPixelBufferPool; |
||||
|
CMBufferQueueRef _videoBufferQueue; |
||||
|
CMBufferQueueRef _audioBufferQueue; |
||||
|
} |
||||
|
|
||||
|
- (instancetype)initWithPerformer:(id<SCPerforming>)performer |
||||
|
outputURL:(NSURL *)outputURL |
||||
|
delegate:(id<SCCapturerBufferedVideoWriterDelegate>)delegate |
||||
|
error:(NSError **)error |
||||
|
{ |
||||
|
self = [super init]; |
||||
|
if (self) { |
||||
|
_performer = performer; |
||||
|
_delegate = delegate; |
||||
|
_observeController = [[FBKVOController alloc] initWithObserver:self]; |
||||
|
CMBufferQueueCreate(kCFAllocatorDefault, 0, CMBufferQueueGetCallbacksForUnsortedSampleBuffers(), |
||||
|
&_videoBufferQueue); |
||||
|
CMBufferQueueCreate(kCFAllocatorDefault, 0, CMBufferQueueGetCallbacksForUnsortedSampleBuffers(), |
||||
|
&_audioBufferQueue); |
||||
|
_assetWriter = [[AVAssetWriter alloc] initWithURL:outputURL fileType:AVFileTypeMPEG4 error:error]; |
||||
|
if (*error) { |
||||
|
self = nil; |
||||
|
return self; |
||||
|
} |
||||
|
} |
||||
|
return self; |
||||
|
} |
||||
|
|
||||
|
- (BOOL)prepareWritingWithOutputSettings:(SCManagedVideoCapturerOutputSettings *)outputSettings |
||||
|
{ |
||||
|
SCTraceStart(); |
||||
|
SCAssert([_performer isCurrentPerformer], @""); |
||||
|
SCAssert(outputSettings, @"empty output setting"); |
||||
|
// Audio |
||||
|
SCTraceSignal(@"Derive audio output setting"); |
||||
|
NSDictionary *audioOutputSettings = @{ |
||||
|
AVFormatIDKey : @(kAudioFormatMPEG4AAC), |
||||
|
AVNumberOfChannelsKey : @(1), |
||||
|
AVSampleRateKey : @(kSCAudioCaptureSessionDefaultSampleRate), |
||||
|
AVEncoderBitRateKey : @(outputSettings.audioBitRate) |
||||
|
}; |
||||
|
_audioWriterInput = |
||||
|
[[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeAudio outputSettings:audioOutputSettings]; |
||||
|
_audioWriterInput.expectsMediaDataInRealTime = YES; |
||||
|
|
||||
|
// Video |
||||
|
SCTraceSignal(@"Derive video output setting"); |
||||
|
size_t outputWidth = outputSettings.width; |
||||
|
size_t outputHeight = outputSettings.height; |
||||
|
SCAssert(outputWidth > 0 && outputHeight > 0 && (outputWidth % 2 == 0) && (outputHeight % 2 == 0), |
||||
|
@"invalid output size"); |
||||
|
NSDictionary *videoCompressionSettings = @{ |
||||
|
AVVideoAverageBitRateKey : @(outputSettings.videoBitRate), |
||||
|
AVVideoMaxKeyFrameIntervalKey : @(outputSettings.keyFrameInterval) |
||||
|
}; |
||||
|
NSDictionary *videoOutputSettings = @{ |
||||
|
AVVideoCodecKey : AVVideoCodecH264, |
||||
|
AVVideoWidthKey : @(outputWidth), |
||||
|
AVVideoHeightKey : @(outputHeight), |
||||
|
AVVideoScalingModeKey : AVVideoScalingModeResizeAspectFill, |
||||
|
AVVideoCompressionPropertiesKey : videoCompressionSettings |
||||
|
}; |
||||
|
_videoWriterInput = |
||||
|
[[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeVideo outputSettings:videoOutputSettings]; |
||||
|
_videoWriterInput.expectsMediaDataInRealTime = YES; |
||||
|
CGAffineTransform transform = CGAffineTransformMakeTranslation(outputHeight, 0); |
||||
|
_videoWriterInput.transform = CGAffineTransformRotate(transform, M_PI_2); |
||||
|
_pixelBufferAdaptor = [[AVAssetWriterInputPixelBufferAdaptor alloc] |
||||
|
initWithAssetWriterInput:_videoWriterInput |
||||
|
sourcePixelBufferAttributes:@{ |
||||
|
(NSString *) |
||||
|
kCVPixelBufferPixelFormatTypeKey : @(kCVPixelFormatType_420YpCbCr8BiPlanarFullRange), (NSString *) |
||||
|
kCVPixelBufferWidthKey : @(outputWidth), (NSString *) |
||||
|
kCVPixelBufferHeightKey : @(outputHeight) |
||||
|
}]; |
||||
|
|
||||
|
SCTraceSignal(@"Setup video writer input"); |
||||
|
if ([_assetWriter canAddInput:_videoWriterInput]) { |
||||
|
[_assetWriter addInput:_videoWriterInput]; |
||||
|
} else { |
||||
|
return NO; |
||||
|
} |
||||
|
|
||||
|
SCTraceSignal(@"Setup audio writer input"); |
||||
|
if ([_assetWriter canAddInput:_audioWriterInput]) { |
||||
|
[_assetWriter addInput:_audioWriterInput]; |
||||
|
} else { |
||||
|
return NO; |
||||
|
} |
||||
|
|
||||
|
return YES; |
||||
|
} |
||||
|
|
||||
|
- (void)appendVideoSampleBuffer:(CMSampleBufferRef)sampleBuffer |
||||
|
{ |
||||
|
SCAssert([_performer isCurrentPerformer], @""); |
||||
|
SC_GUARD_ELSE_RETURN(sampleBuffer); |
||||
|
if (!CMBufferQueueIsEmpty(_videoBufferQueue)) { |
||||
|
// We need to drain the buffer queue in this case |
||||
|
while (_videoWriterInput.readyForMoreMediaData) { // TODO: also need to break out in case of errors |
||||
|
CMSampleBufferRef dequeuedSampleBuffer = |
||||
|
(CMSampleBufferRef)CMBufferQueueDequeueAndRetain(_videoBufferQueue); |
||||
|
if (dequeuedSampleBuffer == NULL) { |
||||
|
break; |
||||
|
} |
||||
|
[self _appendVideoSampleBuffer:dequeuedSampleBuffer]; |
||||
|
CFRelease(dequeuedSampleBuffer); |
||||
|
} |
||||
|
} |
||||
|
// Fast path, just append this sample buffer if ready |
||||
|
if (_videoWriterInput.readyForMoreMediaData) { |
||||
|
[self _appendVideoSampleBuffer:sampleBuffer]; |
||||
|
} else { |
||||
|
// It is not ready, queuing the sample buffer |
||||
|
CMBufferQueueEnqueue(_videoBufferQueue, sampleBuffer); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
- (void)appendAudioSampleBuffer:(CMSampleBufferRef)sampleBuffer |
||||
|
{ |
||||
|
SCAssert([_performer isCurrentPerformer], @""); |
||||
|
SC_GUARD_ELSE_RETURN(sampleBuffer); |
||||
|
if (!CMBufferQueueIsEmpty(_audioBufferQueue)) { |
||||
|
// We need to drain the buffer queue in this case |
||||
|
while (_audioWriterInput.readyForMoreMediaData) { |
||||
|
CMSampleBufferRef dequeuedSampleBuffer = |
||||
|
(CMSampleBufferRef)CMBufferQueueDequeueAndRetain(_audioBufferQueue); |
||||
|
if (dequeuedSampleBuffer == NULL) { |
||||
|
break; |
||||
|
} |
||||
|
[_audioWriterInput appendSampleBuffer:sampleBuffer]; |
||||
|
CFRelease(dequeuedSampleBuffer); |
||||
|
} |
||||
|
} |
||||
|
// fast path, just append this sample buffer if ready |
||||
|
if ((_audioWriterInput.readyForMoreMediaData)) { |
||||
|
[_audioWriterInput appendSampleBuffer:sampleBuffer]; |
||||
|
} else { |
||||
|
// it is not ready, queuing the sample buffer |
||||
|
CMBufferQueueEnqueue(_audioBufferQueue, sampleBuffer); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
- (void)startWritingAtSourceTime:(CMTime)sourceTime |
||||
|
{ |
||||
|
SCTraceStart(); |
||||
|
SCAssert([_performer isCurrentPerformer], @""); |
||||
|
// To observe the status change on assetWriter because when assetWriter errors out, it only changes the |
||||
|
// status, no further delegate callbacks etc. |
||||
|
[_observeController observe:_assetWriter |
||||
|
keyPath:@keypath(_assetWriter, status) |
||||
|
options:NSKeyValueObservingOptionNew |
||||
|
action:@selector(assetWriterStatusChanged:)]; |
||||
|
[_assetWriter startWriting]; |
||||
|
[_assetWriter startSessionAtSourceTime:sourceTime]; |
||||
|
} |
||||
|
|
||||
|
- (void)cancelWriting |
||||
|
{ |
||||
|
SCTraceStart(); |
||||
|
SCAssert([_performer isCurrentPerformer], @""); |
||||
|
CMBufferQueueReset(_videoBufferQueue); |
||||
|
CMBufferQueueReset(_audioBufferQueue); |
||||
|
[_assetWriter cancelWriting]; |
||||
|
} |
||||
|
|
||||
|
- (void)finishWritingAtSourceTime:(CMTime)sourceTime withCompletionHanlder:(dispatch_block_t)completionBlock |
||||
|
{ |
||||
|
SCTraceStart(); |
||||
|
SCAssert([_performer isCurrentPerformer], @""); |
||||
|
|
||||
|
while (_audioWriterInput.readyForMoreMediaData && !CMBufferQueueIsEmpty(_audioBufferQueue)) { |
||||
|
CMSampleBufferRef audioSampleBuffer = (CMSampleBufferRef)CMBufferQueueDequeueAndRetain(_audioBufferQueue); |
||||
|
if (audioSampleBuffer == NULL) { |
||||
|
break; |
||||
|
} |
||||
|
[_audioWriterInput appendSampleBuffer:audioSampleBuffer]; |
||||
|
CFRelease(audioSampleBuffer); |
||||
|
} |
||||
|
while (_videoWriterInput.readyForMoreMediaData && !CMBufferQueueIsEmpty(_videoBufferQueue)) { |
||||
|
CMSampleBufferRef videoSampleBuffer = (CMSampleBufferRef)CMBufferQueueDequeueAndRetain(_videoBufferQueue); |
||||
|
if (videoSampleBuffer == NULL) { |
||||
|
break; |
||||
|
} |
||||
|
[_videoWriterInput appendSampleBuffer:videoSampleBuffer]; |
||||
|
CFRelease(videoSampleBuffer); |
||||
|
} |
||||
|
|
||||
|
dispatch_block_t finishWritingBlock = ^() { |
||||
|
[_assetWriter endSessionAtSourceTime:sourceTime]; |
||||
|
[_audioWriterInput markAsFinished]; |
||||
|
[_videoWriterInput markAsFinished]; |
||||
|
[_assetWriter finishWritingWithCompletionHandler:^{ |
||||
|
if (completionBlock) { |
||||
|
completionBlock(); |
||||
|
} |
||||
|
}]; |
||||
|
}; |
||||
|
|
||||
|
if (CMBufferQueueIsEmpty(_audioBufferQueue) && CMBufferQueueIsEmpty(_videoBufferQueue)) { |
||||
|
finishWritingBlock(); |
||||
|
} else { |
||||
|
// We need to drain the samples from the queues before finish writing |
||||
|
__block BOOL isAudioDone = NO; |
||||
|
__block BOOL isVideoDone = NO; |
||||
|
// Audio |
||||
|
[_audioWriterInput |
||||
|
requestMediaDataWhenReadyOnQueue:_performer.queue |
||||
|
usingBlock:^{ |
||||
|
if (!CMBufferQueueIsEmpty(_audioBufferQueue) && |
||||
|
_assetWriter.status == AVAssetWriterStatusWriting) { |
||||
|
CMSampleBufferRef audioSampleBuffer = |
||||
|
(CMSampleBufferRef)CMBufferQueueDequeueAndRetain(_audioBufferQueue); |
||||
|
if (audioSampleBuffer) { |
||||
|
[_audioWriterInput appendSampleBuffer:audioSampleBuffer]; |
||||
|
CFRelease(audioSampleBuffer); |
||||
|
} |
||||
|
} else if (!isAudioDone) { |
||||
|
isAudioDone = YES; |
||||
|
} |
||||
|
if (isAudioDone && isVideoDone) { |
||||
|
finishWritingBlock(); |
||||
|
} |
||||
|
}]; |
||||
|
|
||||
|
// Video |
||||
|
[_videoWriterInput |
||||
|
requestMediaDataWhenReadyOnQueue:_performer.queue |
||||
|
usingBlock:^{ |
||||
|
if (!CMBufferQueueIsEmpty(_videoBufferQueue) && |
||||
|
_assetWriter.status == AVAssetWriterStatusWriting) { |
||||
|
CMSampleBufferRef videoSampleBuffer = |
||||
|
(CMSampleBufferRef)CMBufferQueueDequeueAndRetain(_videoBufferQueue); |
||||
|
if (videoSampleBuffer) { |
||||
|
[_videoWriterInput appendSampleBuffer:videoSampleBuffer]; |
||||
|
CFRelease(videoSampleBuffer); |
||||
|
} |
||||
|
} else if (!isVideoDone) { |
||||
|
isVideoDone = YES; |
||||
|
} |
||||
|
if (isAudioDone && isVideoDone) { |
||||
|
finishWritingBlock(); |
||||
|
} |
||||
|
}]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
- (void)cleanUp |
||||
|
{ |
||||
|
_assetWriter = nil; |
||||
|
_videoWriterInput = nil; |
||||
|
_audioWriterInput = nil; |
||||
|
_pixelBufferAdaptor = nil; |
||||
|
} |
||||
|
|
||||
|
- (void)dealloc |
||||
|
{ |
||||
|
CFRelease(_videoBufferQueue); |
||||
|
CFRelease(_audioBufferQueue); |
||||
|
CVPixelBufferPoolRelease(_defaultPixelBufferPool); |
||||
|
CVPixelBufferPoolRelease(_nightPixelBufferPool); |
||||
|
CVPixelBufferPoolRelease(_lensesPixelBufferPool); |
||||
|
[_observeController unobserveAll]; |
||||
|
} |
||||
|
|
||||
|
- (void)assetWriterStatusChanged:(NSDictionary *)change |
||||
|
{ |
||||
|
SCTraceStart(); |
||||
|
if (_assetWriter.status == AVAssetWriterStatusFailed) { |
||||
|
SCTraceSignal(@"Asset writer status failed %@, error %@", change, _assetWriter.error); |
||||
|
[_delegate videoWriterDidFailWritingWithError:[_assetWriter.error copy]]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
#pragma - Private methods |
||||
|
|
||||
|
- (CVImageBufferRef)_croppedPixelBufferWithInputPixelBuffer:(CVImageBufferRef)inputPixelBuffer |
||||
|
{ |
||||
|
SCAssertTrue([SCDeviceName isIphoneX]); |
||||
|
const size_t inputBufferWidth = CVPixelBufferGetWidth(inputPixelBuffer); |
||||
|
const size_t inputBufferHeight = CVPixelBufferGetHeight(inputPixelBuffer); |
||||
|
const size_t croppedBufferWidth = (size_t)(inputBufferWidth * kSCIPhoneXCapturedImageVideoCropRatio) / 2 * 2; |
||||
|
const size_t croppedBufferHeight = |
||||
|
(size_t)(croppedBufferWidth * SCManagedCapturedImageAndVideoAspectRatio()) / 2 * 2; |
||||
|
const size_t offsetPointX = inputBufferWidth - croppedBufferWidth; |
||||
|
const size_t offsetPointY = (inputBufferHeight - croppedBufferHeight) / 4 * 2; |
||||
|
|
||||
|
SC_GUARD_ELSE_RUN_AND_RETURN_VALUE((inputBufferWidth >= croppedBufferWidth) && |
||||
|
(inputBufferHeight >= croppedBufferHeight) && (offsetPointX % 2 == 0) && |
||||
|
(offsetPointY % 2 == 0) && |
||||
|
(inputBufferWidth >= croppedBufferWidth + offsetPointX) && |
||||
|
(inputBufferHeight >= croppedBufferHeight + offsetPointY), |
||||
|
SCLogGeneralError(@"Invalid cropping configuration"), NULL); |
||||
|
|
||||
|
CVPixelBufferRef croppedPixelBuffer = NULL; |
||||
|
CVPixelBufferPoolRef pixelBufferPool = |
||||
|
[self _pixelBufferPoolWithInputSize:CGSizeMake(inputBufferWidth, inputBufferHeight) |
||||
|
croppedSize:CGSizeMake(croppedBufferWidth, croppedBufferHeight)]; |
||||
|
|
||||
|
if (pixelBufferPool) { |
||||
|
CVReturn result = CVPixelBufferPoolCreatePixelBuffer(kCFAllocatorDefault, pixelBufferPool, &croppedPixelBuffer); |
||||
|
if ((result != kCVReturnSuccess) || (croppedPixelBuffer == NULL)) { |
||||
|
SCLogGeneralError(@"[SCCapturerVideoWriterInput] Error creating croppedPixelBuffer"); |
||||
|
return NULL; |
||||
|
} |
||||
|
} else { |
||||
|
SCAssertFail(@"[SCCapturerVideoWriterInput] PixelBufferPool is NULL with inputBufferWidth:%@, " |
||||
|
@"inputBufferHeight:%@, croppedBufferWidth:%@, croppedBufferHeight:%@", |
||||
|
@(inputBufferWidth), @(inputBufferHeight), @(croppedBufferWidth), @(croppedBufferHeight)); |
||||
|
return NULL; |
||||
|
} |
||||
|
CVPixelBufferLockBaseAddress(inputPixelBuffer, kCVPixelBufferLock_ReadOnly); |
||||
|
CVPixelBufferLockBaseAddress(croppedPixelBuffer, 0); |
||||
|
|
||||
|
const size_t planesCount = CVPixelBufferGetPlaneCount(inputPixelBuffer); |
||||
|
for (int planeIndex = 0; planeIndex < planesCount; planeIndex++) { |
||||
|
size_t inPlaneHeight = CVPixelBufferGetHeightOfPlane(inputPixelBuffer, planeIndex); |
||||
|
size_t inPlaneBytesPerRow = CVPixelBufferGetBytesPerRowOfPlane(inputPixelBuffer, planeIndex); |
||||
|
uint8_t *inPlaneAdress = (uint8_t *)CVPixelBufferGetBaseAddressOfPlane(inputPixelBuffer, planeIndex); |
||||
|
|
||||
|
size_t croppedPlaneHeight = CVPixelBufferGetHeightOfPlane(croppedPixelBuffer, planeIndex); |
||||
|
size_t croppedPlaneBytesPerRow = CVPixelBufferGetBytesPerRowOfPlane(croppedPixelBuffer, planeIndex); |
||||
|
uint8_t *croppedPlaneAdress = (uint8_t *)CVPixelBufferGetBaseAddressOfPlane(croppedPixelBuffer, planeIndex); |
||||
|
|
||||
|
// Note that inPlaneBytesPerRow is not strictly 2x of inPlaneWidth for some devices (e.g. iPhone X). |
||||
|
// However, since UV are packed together in memory, we can use offsetPointX for all planes |
||||
|
size_t offsetPlaneBytesX = offsetPointX; |
||||
|
size_t offsetPlaneBytesY = offsetPointY * inPlaneHeight / inputBufferHeight; |
||||
|
|
||||
|
inPlaneAdress = inPlaneAdress + offsetPlaneBytesY * inPlaneBytesPerRow + offsetPlaneBytesX; |
||||
|
size_t bytesToCopyPerRow = MIN(inPlaneBytesPerRow - offsetPlaneBytesX, croppedPlaneBytesPerRow); |
||||
|
for (int i = 0; i < croppedPlaneHeight; i++) { |
||||
|
memcpy(croppedPlaneAdress, inPlaneAdress, bytesToCopyPerRow); |
||||
|
inPlaneAdress += inPlaneBytesPerRow; |
||||
|
croppedPlaneAdress += croppedPlaneBytesPerRow; |
||||
|
} |
||||
|
} |
||||
|
CVPixelBufferUnlockBaseAddress(inputPixelBuffer, kCVPixelBufferLock_ReadOnly); |
||||
|
CVPixelBufferUnlockBaseAddress(croppedPixelBuffer, 0); |
||||
|
return croppedPixelBuffer; |
||||
|
} |
||||
|
|
||||
|
- (CVPixelBufferPoolRef)_pixelBufferPoolWithInputSize:(CGSize)inputSize croppedSize:(CGSize)croppedSize |
||||
|
{ |
||||
|
if (CGSizeEqualToSize(inputSize, [SCManagedCaptureDevice defaultActiveFormatResolution])) { |
||||
|
if (_defaultPixelBufferPool == NULL) { |
||||
|
_defaultPixelBufferPool = [self _newPixelBufferPoolWithWidth:croppedSize.width height:croppedSize.height]; |
||||
|
} |
||||
|
return _defaultPixelBufferPool; |
||||
|
} else if (CGSizeEqualToSize(inputSize, [SCManagedCaptureDevice nightModeActiveFormatResolution])) { |
||||
|
if (_nightPixelBufferPool == NULL) { |
||||
|
_nightPixelBufferPool = [self _newPixelBufferPoolWithWidth:croppedSize.width height:croppedSize.height]; |
||||
|
} |
||||
|
return _nightPixelBufferPool; |
||||
|
} else { |
||||
|
if (_lensesPixelBufferPool == NULL) { |
||||
|
_lensesPixelBufferPool = [self _newPixelBufferPoolWithWidth:croppedSize.width height:croppedSize.height]; |
||||
|
} |
||||
|
return _lensesPixelBufferPool; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
- (CVPixelBufferPoolRef)_newPixelBufferPoolWithWidth:(size_t)width height:(size_t)height |
||||
|
{ |
||||
|
NSDictionary *attributes = @{ |
||||
|
(NSString *) kCVPixelBufferIOSurfacePropertiesKey : @{}, (NSString *) |
||||
|
kCVPixelBufferPixelFormatTypeKey : @(kCVPixelFormatType_420YpCbCr8BiPlanarFullRange), (NSString *) |
||||
|
kCVPixelBufferWidthKey : @(width), (NSString *) |
||||
|
kCVPixelBufferHeightKey : @(height) |
||||
|
}; |
||||
|
CVPixelBufferPoolRef pixelBufferPool = NULL; |
||||
|
CVReturn result = CVPixelBufferPoolCreate(kCFAllocatorDefault, NULL, |
||||
|
(__bridge CFDictionaryRef _Nullable)(attributes), &pixelBufferPool); |
||||
|
if (result != kCVReturnSuccess) { |
||||
|
SCLogGeneralError(@"[SCCapturerBufferredVideoWriter] Error creating pixel buffer pool %i", result); |
||||
|
return NULL; |
||||
|
} |
||||
|
|
||||
|
return pixelBufferPool; |
||||
|
} |
||||
|
|
||||
|
- (void)_appendVideoSampleBuffer:(CMSampleBufferRef)sampleBuffer |
||||
|
{ |
||||
|
SCAssert([_performer isCurrentPerformer], @""); |
||||
|
CMTime presentationTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer); |
||||
|
CVImageBufferRef inputPixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer); |
||||
|
if ([SCDeviceName isIphoneX]) { |
||||
|
CVImageBufferRef croppedPixelBuffer = [self _croppedPixelBufferWithInputPixelBuffer:inputPixelBuffer]; |
||||
|
if (croppedPixelBuffer) { |
||||
|
[_pixelBufferAdaptor appendPixelBuffer:croppedPixelBuffer withPresentationTime:presentationTime]; |
||||
|
CVPixelBufferRelease(croppedPixelBuffer); |
||||
|
} |
||||
|
} else { |
||||
|
[_pixelBufferAdaptor appendPixelBuffer:inputPixelBuffer withPresentationTime:presentationTime]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@end |
@ -0,0 +1,20 @@ |
|||||
|
// |
||||
|
// SCCapturerDefines.h |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by Chao Pang on 12/20/17. |
||||
|
// |
||||
|
|
||||
|
#import <Foundation/Foundation.h> |
||||
|
|
||||
|
typedef NS_ENUM(NSInteger, SCCapturerLightingConditionType) { |
||||
|
SCCapturerLightingConditionTypeNormal = 0, |
||||
|
SCCapturerLightingConditionTypeDark, |
||||
|
SCCapturerLightingConditionTypeExtremeDark, |
||||
|
}; |
||||
|
|
||||
|
typedef struct SampleBufferMetadata { |
||||
|
int isoSpeedRating; |
||||
|
float exposureTime; |
||||
|
float brightness; |
||||
|
} SampleBufferMetadata; |
@ -0,0 +1,18 @@ |
|||||
|
// |
||||
|
// SCCapturerToken.h |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by Xishuo Liu on 3/24/17. |
||||
|
// Copyright © 2017 Snapchat, Inc. All rights reserved. |
||||
|
// |
||||
|
|
||||
|
#import <Foundation/Foundation.h> |
||||
|
|
||||
|
@interface SCCapturerToken : NSObject |
||||
|
|
||||
|
- (instancetype)initWithIdentifier:(NSString *)identifier NS_DESIGNATED_INITIALIZER; |
||||
|
|
||||
|
- (instancetype)init __attribute__((unavailable("Use initWithIdentifier: instead."))); |
||||
|
- (instancetype) new __attribute__((unavailable("Use initWithIdentifier: instead."))); |
||||
|
|
||||
|
@end |
@ -0,0 +1,30 @@ |
|||||
|
// |
||||
|
// SCCapturerToken.m |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by Xishuo Liu on 3/24/17. |
||||
|
// Copyright © 2017 Snapchat, Inc. All rights reserved. |
||||
|
// |
||||
|
|
||||
|
#import "SCCapturerToken.h" |
||||
|
|
||||
|
#import <SCFoundation/NSString+SCFormat.h> |
||||
|
|
||||
|
@implementation SCCapturerToken { |
||||
|
NSString *_identifier; |
||||
|
} |
||||
|
|
||||
|
- (instancetype)initWithIdentifier:(NSString *)identifier |
||||
|
{ |
||||
|
if (self = [super init]) { |
||||
|
_identifier = identifier.copy; |
||||
|
} |
||||
|
return self; |
||||
|
} |
||||
|
|
||||
|
- (NSString *)debugDescription |
||||
|
{ |
||||
|
return [NSString sc_stringWithFormat:@"%@_%@", _identifier, self]; |
||||
|
} |
||||
|
|
||||
|
@end |
@ -0,0 +1,20 @@ |
|||||
|
// |
||||
|
// Created by Aaron Levine on 10/16/17. |
||||
|
// |
||||
|
|
||||
|
#import <SCBase/SCMacros.h> |
||||
|
|
||||
|
#import <Foundation/Foundation.h> |
||||
|
|
||||
|
@class SCCapturerToken; |
||||
|
|
||||
|
NS_ASSUME_NONNULL_BEGIN |
||||
|
@interface SCCapturerTokenProvider : NSObject |
||||
|
|
||||
|
SC_INIT_AND_NEW_UNAVAILABLE |
||||
|
+ (instancetype)providerWithToken:(SCCapturerToken *)token; |
||||
|
|
||||
|
- (nullable SCCapturerToken *)getTokenAndInvalidate; |
||||
|
|
||||
|
@end |
||||
|
NS_ASSUME_NONNULL_END |
@ -0,0 +1,42 @@ |
|||||
|
// |
||||
|
// Created by Aaron Levine on 10/16/17. |
||||
|
// |
||||
|
|
||||
|
#import "SCCapturerTokenProvider.h" |
||||
|
|
||||
|
#import "SCCapturerToken.h" |
||||
|
|
||||
|
#import <SCBase/SCAssignment.h> |
||||
|
#import <SCFoundation/SCAssertWrapper.h> |
||||
|
|
||||
|
@implementation SCCapturerTokenProvider { |
||||
|
SCCapturerToken *_Nullable _token; |
||||
|
} |
||||
|
|
||||
|
+ (instancetype)providerWithToken:(SCCapturerToken *)token |
||||
|
{ |
||||
|
return [[self alloc] initWithToken:token]; |
||||
|
} |
||||
|
|
||||
|
- (instancetype)initWithToken:(SCCapturerToken *)token |
||||
|
{ |
||||
|
self = [super init]; |
||||
|
if (self) { |
||||
|
_token = token; |
||||
|
} |
||||
|
|
||||
|
return self; |
||||
|
} |
||||
|
|
||||
|
- (nullable SCCapturerToken *)getTokenAndInvalidate |
||||
|
{ |
||||
|
// ensure serial access by requiring calls be on the main thread |
||||
|
SCAssertMainThread(); |
||||
|
|
||||
|
let token = _token; |
||||
|
_token = nil; |
||||
|
|
||||
|
return token; |
||||
|
} |
||||
|
|
||||
|
@end |
@ -0,0 +1,18 @@ |
|||||
|
// |
||||
|
// SCExposureState.h |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by Derek Peirce on 4/10/17. |
||||
|
// Copyright © 2017 Snapchat, Inc. All rights reserved. |
||||
|
// |
||||
|
|
||||
|
#import <AVFoundation/AVFoundation.h> |
||||
|
#import <Foundation/Foundation.h> |
||||
|
|
||||
|
@interface SCExposureState : NSObject |
||||
|
|
||||
|
- (instancetype)initWithDevice:(AVCaptureDevice *)device; |
||||
|
|
||||
|
- (void)applyISOAndExposureDurationToDevice:(AVCaptureDevice *)device; |
||||
|
|
||||
|
@end |
@ -0,0 +1,47 @@ |
|||||
|
// |
||||
|
// SCExposureState.m |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by Derek Peirce on 4/10/17. |
||||
|
// Copyright © 2017 Snapchat, Inc. All rights reserved. |
||||
|
// |
||||
|
|
||||
|
#import "SCExposureState.h" |
||||
|
|
||||
|
#import "AVCaptureDevice+ConfigurationLock.h" |
||||
|
|
||||
|
#import <SCBase/SCMacros.h> |
||||
|
|
||||
|
@import AVFoundation; |
||||
|
|
||||
|
@implementation SCExposureState { |
||||
|
float _ISO; |
||||
|
CMTime _exposureDuration; |
||||
|
} |
||||
|
|
||||
|
- (instancetype)initWithDevice:(AVCaptureDevice *)device |
||||
|
{ |
||||
|
if (self = [super init]) { |
||||
|
_ISO = device.ISO; |
||||
|
_exposureDuration = device.exposureDuration; |
||||
|
} |
||||
|
return self; |
||||
|
} |
||||
|
|
||||
|
- (void)applyISOAndExposureDurationToDevice:(AVCaptureDevice *)device |
||||
|
{ |
||||
|
if ([device isExposureModeSupported:AVCaptureExposureModeCustom]) { |
||||
|
[device runTask:@"set prior exposure" |
||||
|
withLockedConfiguration:^() { |
||||
|
CMTime exposureDuration = |
||||
|
CMTimeClampToRange(_exposureDuration, CMTimeRangeMake(device.activeFormat.minExposureDuration, |
||||
|
device.activeFormat.maxExposureDuration)); |
||||
|
[device setExposureModeCustomWithDuration:exposureDuration |
||||
|
ISO:SC_CLAMP(_ISO, device.activeFormat.minISO, |
||||
|
device.activeFormat.maxISO) |
||||
|
completionHandler:nil]; |
||||
|
}]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@end |
@ -0,0 +1,19 @@ |
|||||
|
// |
||||
|
// SCFileAudioCaptureSession.h |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by Xiaomu Wu on 2/2/17. |
||||
|
// Copyright © 2017 Snapchat, Inc. All rights reserved. |
||||
|
// |
||||
|
|
||||
|
#import "SCAudioCaptureSession.h" |
||||
|
|
||||
|
#import <Foundation/Foundation.h> |
||||
|
|
||||
|
@interface SCFileAudioCaptureSession : NSObject <SCAudioCaptureSession> |
||||
|
|
||||
|
// Linear PCM is required. |
||||
|
// To best mimic `SCAudioCaptureSession`, use an audio file recorded from it. |
||||
|
- (void)setFileURL:(NSURL *)fileURL; |
||||
|
|
||||
|
@end |
@ -0,0 +1,243 @@ |
|||||
|
// |
||||
|
// SCFileAudioCaptureSession.m |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by Xiaomu Wu on 2/2/17. |
||||
|
// Copyright © 2017 Snapchat, Inc. All rights reserved. |
||||
|
// |
||||
|
|
||||
|
#import "SCFileAudioCaptureSession.h" |
||||
|
|
||||
|
#import <SCFoundation/SCAssertWrapper.h> |
||||
|
#import <SCFoundation/SCLog.h> |
||||
|
#import <SCFoundation/SCQueuePerformer.h> |
||||
|
#import <SCFoundation/SCSentinel.h> |
||||
|
|
||||
|
@import AudioToolbox; |
||||
|
|
||||
|
static float const kAudioBufferDurationInSeconds = 0.2; // same as SCAudioCaptureSession |
||||
|
|
||||
|
static char *const kSCFileAudioCaptureSessionQueueLabel = "com.snapchat.file-audio-capture-session"; |
||||
|
|
||||
|
@implementation SCFileAudioCaptureSession { |
||||
|
SCQueuePerformer *_performer; |
||||
|
SCSentinel *_sentinel; |
||||
|
|
||||
|
NSURL *_fileURL; |
||||
|
|
||||
|
AudioFileID _audioFile; // audio file |
||||
|
AudioStreamBasicDescription _asbd; // audio format (core audio) |
||||
|
CMAudioFormatDescriptionRef _formatDescription; // audio format (core media) |
||||
|
SInt64 _readCurPacket; // current packet index to read |
||||
|
UInt32 _readNumPackets; // number of packets to read every time |
||||
|
UInt32 _readNumBytes; // number of bytes to read every time |
||||
|
void *_readBuffer; // data buffer to hold read packets |
||||
|
} |
||||
|
|
||||
|
@synthesize delegate = _delegate; |
||||
|
|
||||
|
#pragma mark - Public |
||||
|
|
||||
|
- (instancetype)init |
||||
|
{ |
||||
|
self = [super init]; |
||||
|
if (self) { |
||||
|
_performer = [[SCQueuePerformer alloc] initWithLabel:kSCFileAudioCaptureSessionQueueLabel |
||||
|
qualityOfService:QOS_CLASS_UNSPECIFIED |
||||
|
queueType:DISPATCH_QUEUE_SERIAL |
||||
|
context:SCQueuePerformerContextCamera]; |
||||
|
_sentinel = [[SCSentinel alloc] init]; |
||||
|
} |
||||
|
return self; |
||||
|
} |
||||
|
|
||||
|
- (void)dealloc |
||||
|
{ |
||||
|
if (_audioFile) { |
||||
|
AudioFileClose(_audioFile); |
||||
|
} |
||||
|
if (_formatDescription) { |
||||
|
CFRelease(_formatDescription); |
||||
|
} |
||||
|
if (_readBuffer) { |
||||
|
free(_readBuffer); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
- (void)setFileURL:(NSURL *)fileURL |
||||
|
{ |
||||
|
[_performer perform:^{ |
||||
|
_fileURL = fileURL; |
||||
|
}]; |
||||
|
} |
||||
|
|
||||
|
#pragma mark - SCAudioCaptureSession |
||||
|
|
||||
|
- (void)beginAudioRecordingAsynchronouslyWithSampleRate:(double)sampleRate // `sampleRate` ignored |
||||
|
completionHandler:(audio_capture_session_block)completionHandler |
||||
|
{ |
||||
|
[_performer perform:^{ |
||||
|
BOOL succeeded = [self _setup]; |
||||
|
int32_t sentinelValue = [_sentinel value]; |
||||
|
if (completionHandler) { |
||||
|
completionHandler(nil); |
||||
|
} |
||||
|
if (succeeded) { |
||||
|
[_performer perform:^{ |
||||
|
SC_GUARD_ELSE_RETURN([_sentinel value] == sentinelValue); |
||||
|
[self _read]; |
||||
|
} |
||||
|
after:kAudioBufferDurationInSeconds]; |
||||
|
} |
||||
|
}]; |
||||
|
} |
||||
|
|
||||
|
- (void)disposeAudioRecordingSynchronouslyWithCompletionHandler:(dispatch_block_t)completionHandler |
||||
|
{ |
||||
|
[_performer performAndWait:^{ |
||||
|
[self _teardown]; |
||||
|
if (completionHandler) { |
||||
|
completionHandler(); |
||||
|
} |
||||
|
}]; |
||||
|
} |
||||
|
|
||||
|
#pragma mark - Private |
||||
|
|
||||
|
- (BOOL)_setup |
||||
|
{ |
||||
|
SCAssert([_performer isCurrentPerformer], @""); |
||||
|
|
||||
|
[_sentinel increment]; |
||||
|
|
||||
|
OSStatus status = noErr; |
||||
|
|
||||
|
status = AudioFileOpenURL((__bridge CFURLRef)_fileURL, kAudioFileReadPermission, 0, &_audioFile); |
||||
|
if (noErr != status) { |
||||
|
SCLogGeneralError(@"Cannot open file at URL %@, error code %d", _fileURL, (int)status); |
||||
|
return NO; |
||||
|
} |
||||
|
|
||||
|
_asbd = (AudioStreamBasicDescription){0}; |
||||
|
UInt32 asbdSize = sizeof(_asbd); |
||||
|
status = AudioFileGetProperty(_audioFile, kAudioFilePropertyDataFormat, &asbdSize, &_asbd); |
||||
|
if (noErr != status) { |
||||
|
SCLogGeneralError(@"Cannot get audio data format, error code %d", (int)status); |
||||
|
AudioFileClose(_audioFile); |
||||
|
_audioFile = NULL; |
||||
|
return NO; |
||||
|
} |
||||
|
|
||||
|
if (kAudioFormatLinearPCM != _asbd.mFormatID) { |
||||
|
SCLogGeneralError(@"Linear PCM is required"); |
||||
|
AudioFileClose(_audioFile); |
||||
|
_audioFile = NULL; |
||||
|
_asbd = (AudioStreamBasicDescription){0}; |
||||
|
return NO; |
||||
|
} |
||||
|
|
||||
|
UInt32 aclSize = 0; |
||||
|
AudioChannelLayout *acl = NULL; |
||||
|
status = AudioFileGetPropertyInfo(_audioFile, kAudioFilePropertyChannelLayout, &aclSize, NULL); |
||||
|
if (noErr == status) { |
||||
|
acl = malloc(aclSize); |
||||
|
status = AudioFileGetProperty(_audioFile, kAudioFilePropertyChannelLayout, &aclSize, acl); |
||||
|
if (noErr != status) { |
||||
|
aclSize = 0; |
||||
|
free(acl); |
||||
|
acl = NULL; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
status = CMAudioFormatDescriptionCreate(NULL, &_asbd, aclSize, acl, 0, NULL, NULL, &_formatDescription); |
||||
|
if (acl) { |
||||
|
free(acl); |
||||
|
acl = NULL; |
||||
|
} |
||||
|
if (noErr != status) { |
||||
|
SCLogGeneralError(@"Cannot create format description, error code %d", (int)status); |
||||
|
AudioFileClose(_audioFile); |
||||
|
_audioFile = NULL; |
||||
|
_asbd = (AudioStreamBasicDescription){0}; |
||||
|
return NO; |
||||
|
} |
||||
|
|
||||
|
_readCurPacket = 0; |
||||
|
_readNumPackets = ceil(_asbd.mSampleRate * kAudioBufferDurationInSeconds); |
||||
|
_readNumBytes = _asbd.mBytesPerPacket * _readNumPackets; |
||||
|
_readBuffer = malloc(_readNumBytes); |
||||
|
|
||||
|
return YES; |
||||
|
} |
||||
|
|
||||
|
- (void)_read |
||||
|
{ |
||||
|
SCAssert([_performer isCurrentPerformer], @""); |
||||
|
|
||||
|
OSStatus status = noErr; |
||||
|
|
||||
|
UInt32 numBytes = _readNumBytes; |
||||
|
UInt32 numPackets = _readNumPackets; |
||||
|
status = AudioFileReadPacketData(_audioFile, NO, &numBytes, NULL, _readCurPacket, &numPackets, _readBuffer); |
||||
|
if (noErr != status) { |
||||
|
SCLogGeneralError(@"Cannot read audio data, error code %d", (int)status); |
||||
|
return; |
||||
|
} |
||||
|
if (0 == numPackets) { |
||||
|
return; |
||||
|
} |
||||
|
CMTime PTS = CMTimeMakeWithSeconds(_readCurPacket / _asbd.mSampleRate, 600); |
||||
|
|
||||
|
_readCurPacket += numPackets; |
||||
|
|
||||
|
CMBlockBufferRef dataBuffer = NULL; |
||||
|
status = CMBlockBufferCreateWithMemoryBlock(NULL, NULL, numBytes, NULL, NULL, 0, numBytes, 0, &dataBuffer); |
||||
|
if (kCMBlockBufferNoErr == status) { |
||||
|
if (dataBuffer) { |
||||
|
CMBlockBufferReplaceDataBytes(_readBuffer, dataBuffer, 0, numBytes); |
||||
|
CMSampleBufferRef sampleBuffer = NULL; |
||||
|
CMAudioSampleBufferCreateWithPacketDescriptions(NULL, dataBuffer, true, NULL, NULL, _formatDescription, |
||||
|
numPackets, PTS, NULL, &sampleBuffer); |
||||
|
if (sampleBuffer) { |
||||
|
[_delegate audioCaptureSession:self didOutputSampleBuffer:sampleBuffer]; |
||||
|
CFRelease(sampleBuffer); |
||||
|
} |
||||
|
CFRelease(dataBuffer); |
||||
|
} |
||||
|
} else { |
||||
|
SCLogGeneralError(@"Cannot create data buffer, error code %d", (int)status); |
||||
|
} |
||||
|
|
||||
|
int32_t sentinelValue = [_sentinel value]; |
||||
|
[_performer perform:^{ |
||||
|
SC_GUARD_ELSE_RETURN([_sentinel value] == sentinelValue); |
||||
|
[self _read]; |
||||
|
} |
||||
|
after:kAudioBufferDurationInSeconds]; |
||||
|
} |
||||
|
|
||||
|
- (void)_teardown |
||||
|
{ |
||||
|
SCAssert([_performer isCurrentPerformer], @""); |
||||
|
|
||||
|
[_sentinel increment]; |
||||
|
|
||||
|
if (_audioFile) { |
||||
|
AudioFileClose(_audioFile); |
||||
|
_audioFile = NULL; |
||||
|
} |
||||
|
_asbd = (AudioStreamBasicDescription){0}; |
||||
|
if (_formatDescription) { |
||||
|
CFRelease(_formatDescription); |
||||
|
_formatDescription = NULL; |
||||
|
} |
||||
|
_readCurPacket = 0; |
||||
|
_readNumPackets = 0; |
||||
|
_readNumBytes = 0; |
||||
|
if (_readBuffer) { |
||||
|
free(_readBuffer); |
||||
|
_readBuffer = NULL; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@end |
@ -0,0 +1,20 @@ |
|||||
|
// |
||||
|
// SCManagedAudioStreamer.h |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by Ricardo Sánchez-Sáez on 7/28/16. |
||||
|
// Copyright © 2016 Snapchat, Inc. All rights reserved. |
||||
|
// |
||||
|
|
||||
|
#import <SCCameraFoundation/SCManagedAudioDataSource.h> |
||||
|
|
||||
|
#import <Foundation/Foundation.h> |
||||
|
|
||||
|
@interface SCManagedAudioStreamer : NSObject <SCManagedAudioDataSource> |
||||
|
|
||||
|
+ (instancetype)sharedInstance; |
||||
|
|
||||
|
+ (instancetype) new NS_UNAVAILABLE; |
||||
|
- (instancetype)init NS_UNAVAILABLE; |
||||
|
|
||||
|
@end |
@ -0,0 +1,115 @@ |
|||||
|
// |
||||
|
// SCManagedAudioStreamer.m |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by Ricardo Sánchez-Sáez on 7/28/16. |
||||
|
// Copyright © 2016 Snapchat, Inc. All rights reserved. |
||||
|
// |
||||
|
|
||||
|
#import "SCManagedAudioStreamer.h" |
||||
|
|
||||
|
#import "SCAudioCaptureSession.h" |
||||
|
|
||||
|
#import <SCAudio/SCAudioSession.h> |
||||
|
#import <SCCameraFoundation/SCManagedAudioDataSourceListenerAnnouncer.h> |
||||
|
#import <SCFoundation/SCQueuePerformer.h> |
||||
|
#import <SCFoundation/SCTrace.h> |
||||
|
|
||||
|
#import <SCAudioScope/SCAudioScope.h> |
||||
|
#import <SCAudioScope/SCAudioSessionExperimentAdapter.h> |
||||
|
|
||||
|
static char *const kSCManagedAudioStreamerQueueLabel = "com.snapchat.audioStreamerQueue"; |
||||
|
|
||||
|
@interface SCManagedAudioStreamer () <SCAudioCaptureSessionDelegate> |
||||
|
|
||||
|
@end |
||||
|
|
||||
|
@implementation SCManagedAudioStreamer { |
||||
|
SCAudioCaptureSession *_captureSession; |
||||
|
SCAudioConfigurationToken *_audioConfiguration; |
||||
|
SCManagedAudioDataSourceListenerAnnouncer *_announcer; |
||||
|
SCScopedAccess<SCMutableAudioSession *> *_scopedMutableAudioSession; |
||||
|
} |
||||
|
|
||||
|
@synthesize performer = _performer; |
||||
|
|
||||
|
+ (instancetype)sharedInstance |
||||
|
{ |
||||
|
static dispatch_once_t onceToken; |
||||
|
static SCManagedAudioStreamer *managedAudioStreamer; |
||||
|
dispatch_once(&onceToken, ^{ |
||||
|
managedAudioStreamer = [[SCManagedAudioStreamer alloc] initSharedInstance]; |
||||
|
}); |
||||
|
return managedAudioStreamer; |
||||
|
} |
||||
|
|
||||
|
- (instancetype)initSharedInstance |
||||
|
{ |
||||
|
SCTraceStart(); |
||||
|
self = [super init]; |
||||
|
if (self) { |
||||
|
_performer = [[SCQueuePerformer alloc] initWithLabel:kSCManagedAudioStreamerQueueLabel |
||||
|
qualityOfService:QOS_CLASS_USER_INTERACTIVE |
||||
|
queueType:DISPATCH_QUEUE_SERIAL |
||||
|
context:SCQueuePerformerContextCamera]; |
||||
|
_announcer = [[SCManagedAudioDataSourceListenerAnnouncer alloc] init]; |
||||
|
_captureSession = [[SCAudioCaptureSession alloc] init]; |
||||
|
_captureSession.delegate = self; |
||||
|
} |
||||
|
return self; |
||||
|
} |
||||
|
|
||||
|
- (BOOL)isStreaming |
||||
|
{ |
||||
|
return _audioConfiguration != nil; |
||||
|
} |
||||
|
|
||||
|
- (void)startStreamingWithAudioConfiguration:(SCAudioConfiguration *)configuration |
||||
|
{ |
||||
|
SCTraceStart(); |
||||
|
[_performer perform:^{ |
||||
|
if (!self.isStreaming) { |
||||
|
// Begin audio recording asynchronously. First we need to have the proper audio session category. |
||||
|
_audioConfiguration = [SCAudioSessionExperimentAdapter |
||||
|
configureWith:configuration |
||||
|
performer:_performer |
||||
|
completion:^(NSError *error) { |
||||
|
[_captureSession |
||||
|
beginAudioRecordingAsynchronouslyWithSampleRate:kSCAudioCaptureSessionDefaultSampleRate |
||||
|
completionHandler:NULL]; |
||||
|
|
||||
|
}]; |
||||
|
} |
||||
|
}]; |
||||
|
} |
||||
|
|
||||
|
- (void)stopStreaming |
||||
|
{ |
||||
|
[_performer perform:^{ |
||||
|
if (self.isStreaming) { |
||||
|
[_captureSession disposeAudioRecordingSynchronouslyWithCompletionHandler:NULL]; |
||||
|
[SCAudioSessionExperimentAdapter relinquishConfiguration:_audioConfiguration performer:nil completion:nil]; |
||||
|
_audioConfiguration = nil; |
||||
|
} |
||||
|
}]; |
||||
|
} |
||||
|
|
||||
|
- (void)addListener:(id<SCManagedAudioDataSourceListener>)listener |
||||
|
{ |
||||
|
SCTraceStart(); |
||||
|
[_announcer addListener:listener]; |
||||
|
} |
||||
|
|
||||
|
- (void)removeListener:(id<SCManagedAudioDataSourceListener>)listener |
||||
|
{ |
||||
|
SCTraceStart(); |
||||
|
[_announcer removeListener:listener]; |
||||
|
} |
||||
|
|
||||
|
- (void)audioCaptureSession:(SCAudioCaptureSession *)audioCaptureSession |
||||
|
didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer |
||||
|
{ |
||||
|
[_announcer managedAudioDataSource:self didOutputSampleBuffer:sampleBuffer]; |
||||
|
} |
||||
|
|
||||
|
@end |
@ -0,0 +1,71 @@ |
|||||
|
// |
||||
|
// SCManagedCaptureDevice+SCManagedCapturer.h |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by Liu Liu on 5/9/15. |
||||
|
// Copyright (c) 2015 Snapchat, Inc. All rights reserved. |
||||
|
// |
||||
|
|
||||
|
#import "SCManagedCaptureDevice.h" |
||||
|
|
||||
|
#import <AVFoundation/AVFoundation.h> |
||||
|
|
||||
|
@interface SCManagedCaptureDevice (SCManagedCapturer) |
||||
|
|
||||
|
@property (nonatomic, strong, readonly) AVCaptureDevice *device; |
||||
|
|
||||
|
@property (nonatomic, strong, readonly) AVCaptureDeviceInput *deviceInput; |
||||
|
|
||||
|
@property (nonatomic, copy, readonly) NSError *error; |
||||
|
|
||||
|
@property (nonatomic, assign, readonly) BOOL isConnected; |
||||
|
|
||||
|
@property (nonatomic, strong, readonly) AVCaptureDeviceFormat *activeFormat; |
||||
|
|
||||
|
// Setup and hook up with device |
||||
|
|
||||
|
- (BOOL)setDeviceAsInput:(AVCaptureSession *)session; |
||||
|
|
||||
|
- (void)removeDeviceAsInput:(AVCaptureSession *)session; |
||||
|
|
||||
|
- (void)resetDeviceAsInput; |
||||
|
|
||||
|
// Configurations |
||||
|
|
||||
|
@property (nonatomic, assign) BOOL flashActive; |
||||
|
|
||||
|
@property (nonatomic, assign) BOOL torchActive; |
||||
|
|
||||
|
@property (nonatomic, assign) float zoomFactor; |
||||
|
|
||||
|
@property (nonatomic, assign, readonly) BOOL liveVideoStreamingActive; |
||||
|
|
||||
|
@property (nonatomic, assign, readonly) BOOL isNightModeActive; |
||||
|
|
||||
|
@property (nonatomic, assign, readonly) BOOL isFlashSupported; |
||||
|
|
||||
|
@property (nonatomic, assign, readonly) BOOL isTorchSupported; |
||||
|
|
||||
|
- (void)setNightModeActive:(BOOL)nightModeActive session:(AVCaptureSession *)session; |
||||
|
|
||||
|
- (void)setLiveVideoStreaming:(BOOL)liveVideoStreaming session:(AVCaptureSession *)session; |
||||
|
|
||||
|
- (void)setCaptureDepthData:(BOOL)captureDepthData session:(AVCaptureSession *)session; |
||||
|
|
||||
|
- (void)setExposurePointOfInterest:(CGPoint)pointOfInterest fromUser:(BOOL)fromUser; |
||||
|
|
||||
|
- (void)setAutofocusPointOfInterest:(CGPoint)pointOfInterest; |
||||
|
|
||||
|
- (void)continuousAutofocus; |
||||
|
|
||||
|
- (void)setRecording:(BOOL)recording; |
||||
|
|
||||
|
- (void)updateActiveFormatWithSession:(AVCaptureSession *)session; |
||||
|
|
||||
|
// Utilities |
||||
|
|
||||
|
- (CGPoint)convertViewCoordinates:(CGPoint)viewCoordinates |
||||
|
viewSize:(CGSize)viewSize |
||||
|
videoGravity:(NSString *)videoGravity; |
||||
|
|
||||
|
@end |
@ -0,0 +1,17 @@ |
|||||
|
// |
||||
|
// SCManagedCaptureDevice+SCManagedDeviceCapacityAnalyzer.h |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by Kam Sheffield on 10/29/15. |
||||
|
// Copyright © 2015 Snapchat, Inc. All rights reserved. |
||||
|
// |
||||
|
|
||||
|
#import "SCManagedCaptureDevice.h" |
||||
|
|
||||
|
#import <AVFoundation/AVFoundation.h> |
||||
|
|
||||
|
@interface SCManagedCaptureDevice (SCManagedDeviceCapacityAnalyzer) |
||||
|
|
||||
|
@property (nonatomic, strong, readonly) AVCaptureDevice *device; |
||||
|
|
||||
|
@end |
@ -0,0 +1,60 @@ |
|||||
|
// |
||||
|
// SCManagedCaptureDevice.h |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by Liu Liu on 4/22/15. |
||||
|
// Copyright (c) 2015 Liu Liu. All rights reserved. |
||||
|
// |
||||
|
|
||||
|
#import <SCCameraFoundation/SCManagedCaptureDevicePosition.h> |
||||
|
#import <SCCameraFoundation/SCManagedCaptureDeviceProtocol.h> |
||||
|
|
||||
|
#import <AVFoundation/AVFoundation.h> |
||||
|
#import <Foundation/Foundation.h> |
||||
|
|
||||
|
extern CGFloat const kSCMaxVideoZoomFactor; |
||||
|
extern CGFloat const kSCMinVideoZoomFactor; |
||||
|
|
||||
|
@class SCManagedCaptureDevice; |
||||
|
|
||||
|
@protocol SCManagedCaptureDeviceDelegate <NSObject> |
||||
|
|
||||
|
@optional |
||||
|
- (void)managedCaptureDevice:(SCManagedCaptureDevice *)device didChangeAdjustingExposure:(BOOL)adjustingExposure; |
||||
|
- (void)managedCaptureDevice:(SCManagedCaptureDevice *)device didChangeExposurePoint:(CGPoint)exposurePoint; |
||||
|
- (void)managedCaptureDevice:(SCManagedCaptureDevice *)device didChangeFocusPoint:(CGPoint)focusPoint; |
||||
|
|
||||
|
@end |
||||
|
|
||||
|
@interface SCManagedCaptureDevice : NSObject <SCManagedCaptureDeviceProtocol> |
||||
|
|
||||
|
@property (nonatomic, weak) id<SCManagedCaptureDeviceDelegate> delegate; |
||||
|
|
||||
|
// These two class methods are thread safe |
||||
|
+ (instancetype)front; |
||||
|
|
||||
|
+ (instancetype)back; |
||||
|
|
||||
|
+ (instancetype)dualCamera; |
||||
|
|
||||
|
+ (instancetype)deviceWithPosition:(SCManagedCaptureDevicePosition)position; |
||||
|
|
||||
|
+ (BOOL)is1080pSupported; |
||||
|
|
||||
|
+ (BOOL)isMixCaptureSupported; |
||||
|
|
||||
|
+ (BOOL)isNightModeSupported; |
||||
|
|
||||
|
+ (BOOL)isEnhancedNightModeSupported; |
||||
|
|
||||
|
+ (CGSize)defaultActiveFormatResolution; |
||||
|
|
||||
|
+ (CGSize)nightModeActiveFormatResolution; |
||||
|
|
||||
|
- (BOOL)softwareZoom; |
||||
|
|
||||
|
- (SCManagedCaptureDevicePosition)position; |
||||
|
|
||||
|
- (BOOL)isAvailable; |
||||
|
|
||||
|
@end |
@ -0,0 +1,821 @@ |
|||||
|
// |
||||
|
// SCManagedCaptureDevice.m |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by Liu Liu on 4/22/15. |
||||
|
// Copyright (c) 2015 Liu Liu. All rights reserved. |
||||
|
// |
||||
|
|
||||
|
#import "SCManagedCaptureDevice.h" |
||||
|
|
||||
|
#import "AVCaptureDevice+ConfigurationLock.h" |
||||
|
#import "SCCameraTweaks.h" |
||||
|
#import "SCCaptureCommon.h" |
||||
|
#import "SCCaptureDeviceResolver.h" |
||||
|
#import "SCManagedCaptureDevice+SCManagedCapturer.h" |
||||
|
#import "SCManagedCaptureDeviceAutoExposureHandler.h" |
||||
|
#import "SCManagedCaptureDeviceAutoFocusHandler.h" |
||||
|
#import "SCManagedCaptureDeviceExposureHandler.h" |
||||
|
#import "SCManagedCaptureDeviceFaceDetectionAutoExposureHandler.h" |
||||
|
#import "SCManagedCaptureDeviceFaceDetectionAutoFocusHandler.h" |
||||
|
#import "SCManagedCaptureDeviceFocusHandler.h" |
||||
|
#import "SCManagedCapturer.h" |
||||
|
#import "SCManagedDeviceCapacityAnalyzer.h" |
||||
|
|
||||
|
#import <SCFoundation/SCDeviceName.h> |
||||
|
#import <SCFoundation/SCLog.h> |
||||
|
#import <SCFoundation/SCTrace.h> |
||||
|
|
||||
|
#import <FBKVOController/FBKVOController.h> |
||||
|
|
||||
|
static int32_t const kSCManagedCaptureDeviceMaximumHighFrameRate = 30; |
||||
|
static int32_t const kSCManagedCaptureDeviceMaximumLowFrameRate = 24; |
||||
|
|
||||
|
static float const kSCManagedCaptureDevicecSoftwareMaxZoomFactor = 8; |
||||
|
|
||||
|
CGFloat const kSCMaxVideoZoomFactor = 100; // the max videoZoomFactor acceptable |
||||
|
CGFloat const kSCMinVideoZoomFactor = 1; |
||||
|
|
||||
|
static NSDictionary *SCBestHRSIFormatsForHeights(NSArray *desiredHeights, NSArray *formats, BOOL shouldSupportDepth) |
||||
|
{ |
||||
|
NSMutableDictionary *bestHRSIHeights = [NSMutableDictionary dictionary]; |
||||
|
for (NSNumber *height in desiredHeights) { |
||||
|
bestHRSIHeights[height] = @0; |
||||
|
} |
||||
|
NSMutableDictionary *bestHRSIFormats = [NSMutableDictionary dictionary]; |
||||
|
for (AVCaptureDeviceFormat *format in formats) { |
||||
|
if (@available(ios 11.0, *)) { |
||||
|
if (shouldSupportDepth && format.supportedDepthDataFormats.count == 0) { |
||||
|
continue; |
||||
|
} |
||||
|
} |
||||
|
if (CMFormatDescriptionGetMediaSubType(format.formatDescription) != |
||||
|
kCVPixelFormatType_420YpCbCr8BiPlanarFullRange) { |
||||
|
continue; |
||||
|
} |
||||
|
CMVideoDimensions dimensions = CMVideoFormatDescriptionGetDimensions(format.formatDescription); |
||||
|
NSNumber *height = @(dimensions.height); |
||||
|
NSNumber *bestHRSI = bestHRSIHeights[height]; |
||||
|
if (bestHRSI) { |
||||
|
CMVideoDimensions hrsi = format.highResolutionStillImageDimensions; |
||||
|
// If we enabled HSRI, we only intersted in the ones that is good. |
||||
|
if (hrsi.height > [bestHRSI intValue]) { |
||||
|
bestHRSIHeights[height] = @(hrsi.height); |
||||
|
bestHRSIFormats[height] = format; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
return [bestHRSIFormats copy]; |
||||
|
} |
||||
|
|
||||
|
static inline float SCDegreesToRadians(float theta) |
||||
|
{ |
||||
|
return theta * (float)M_PI / 180.f; |
||||
|
} |
||||
|
|
||||
|
static inline float SCRadiansToDegrees(float theta) |
||||
|
{ |
||||
|
return theta * 180.f / (float)M_PI; |
||||
|
} |
||||
|
|
||||
|
@implementation SCManagedCaptureDevice { |
||||
|
AVCaptureDevice *_device; |
||||
|
AVCaptureDeviceInput *_deviceInput; |
||||
|
AVCaptureDeviceFormat *_defaultFormat; |
||||
|
AVCaptureDeviceFormat *_nightFormat; |
||||
|
AVCaptureDeviceFormat *_liveVideoStreamingFormat; |
||||
|
SCManagedCaptureDevicePosition _devicePosition; |
||||
|
|
||||
|
// Configurations on the device, shortcut to avoid re-configurations |
||||
|
id<SCManagedCaptureDeviceExposureHandler> _exposureHandler; |
||||
|
id<SCManagedCaptureDeviceFocusHandler> _focusHandler; |
||||
|
|
||||
|
FBKVOController *_observeController; |
||||
|
|
||||
|
// For the private category methods |
||||
|
NSError *_error; |
||||
|
BOOL _softwareZoom; |
||||
|
BOOL _isConnected; |
||||
|
BOOL _flashActive; |
||||
|
BOOL _torchActive; |
||||
|
BOOL _liveVideoStreamingActive; |
||||
|
float _zoomFactor; |
||||
|
BOOL _isNightModeActive; |
||||
|
BOOL _captureDepthData; |
||||
|
} |
||||
|
@synthesize fieldOfView = _fieldOfView; |
||||
|
|
||||
|
+ (instancetype)front |
||||
|
{ |
||||
|
SCTraceStart(); |
||||
|
static dispatch_once_t onceToken; |
||||
|
static SCManagedCaptureDevice *front; |
||||
|
static dispatch_semaphore_t semaphore; |
||||
|
dispatch_once(&onceToken, ^{ |
||||
|
semaphore = dispatch_semaphore_create(1); |
||||
|
}); |
||||
|
/* You can use the tweak below to intentionally kill camera in debug. |
||||
|
if (SCIsDebugBuild() && SCCameraTweaksKillFrontCamera()) { |
||||
|
return nil; |
||||
|
} |
||||
|
*/ |
||||
|
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); |
||||
|
if (!front) { |
||||
|
AVCaptureDevice *device = |
||||
|
[[SCCaptureDeviceResolver sharedInstance] findAVCaptureDevice:AVCaptureDevicePositionFront]; |
||||
|
if (device) { |
||||
|
front = [[SCManagedCaptureDevice alloc] initWithDevice:device |
||||
|
devicePosition:SCManagedCaptureDevicePositionFront]; |
||||
|
} |
||||
|
} |
||||
|
dispatch_semaphore_signal(semaphore); |
||||
|
return front; |
||||
|
} |
||||
|
|
||||
|
+ (instancetype)back |
||||
|
{ |
||||
|
SCTraceStart(); |
||||
|
static dispatch_once_t onceToken; |
||||
|
static SCManagedCaptureDevice *back; |
||||
|
static dispatch_semaphore_t semaphore; |
||||
|
dispatch_once(&onceToken, ^{ |
||||
|
semaphore = dispatch_semaphore_create(1); |
||||
|
}); |
||||
|
/* You can use the tweak below to intentionally kill camera in debug. |
||||
|
if (SCIsDebugBuild() && SCCameraTweaksKillBackCamera()) { |
||||
|
return nil; |
||||
|
} |
||||
|
*/ |
||||
|
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); |
||||
|
if (!back) { |
||||
|
AVCaptureDevice *device = |
||||
|
[[SCCaptureDeviceResolver sharedInstance] findAVCaptureDevice:AVCaptureDevicePositionBack]; |
||||
|
if (device) { |
||||
|
back = [[SCManagedCaptureDevice alloc] initWithDevice:device |
||||
|
devicePosition:SCManagedCaptureDevicePositionBack]; |
||||
|
} |
||||
|
} |
||||
|
dispatch_semaphore_signal(semaphore); |
||||
|
return back; |
||||
|
} |
||||
|
|
||||
|
+ (SCManagedCaptureDevice *)dualCamera |
||||
|
{ |
||||
|
SCTraceStart(); |
||||
|
static dispatch_once_t onceToken; |
||||
|
static SCManagedCaptureDevice *dualCamera; |
||||
|
static dispatch_semaphore_t semaphore; |
||||
|
dispatch_once(&onceToken, ^{ |
||||
|
semaphore = dispatch_semaphore_create(1); |
||||
|
}); |
||||
|
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); |
||||
|
if (!dualCamera) { |
||||
|
AVCaptureDevice *device = [[SCCaptureDeviceResolver sharedInstance] findDualCamera]; |
||||
|
if (device) { |
||||
|
dualCamera = [[SCManagedCaptureDevice alloc] initWithDevice:device |
||||
|
devicePosition:SCManagedCaptureDevicePositionBackDualCamera]; |
||||
|
} |
||||
|
} |
||||
|
dispatch_semaphore_signal(semaphore); |
||||
|
return dualCamera; |
||||
|
} |
||||
|
|
||||
|
+ (instancetype)deviceWithPosition:(SCManagedCaptureDevicePosition)position |
||||
|
{ |
||||
|
switch (position) { |
||||
|
case SCManagedCaptureDevicePositionFront: |
||||
|
return [self front]; |
||||
|
case SCManagedCaptureDevicePositionBack: |
||||
|
return [self back]; |
||||
|
case SCManagedCaptureDevicePositionBackDualCamera: |
||||
|
return [self dualCamera]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
+ (BOOL)is1080pSupported |
||||
|
{ |
||||
|
return [SCDeviceName isIphone] && [SCDeviceName isSimilarToIphone6SorNewer]; |
||||
|
} |
||||
|
|
||||
|
+ (BOOL)isMixCaptureSupported |
||||
|
{ |
||||
|
return !![self front] && !![self back]; |
||||
|
} |
||||
|
|
||||
|
+ (BOOL)isNightModeSupported |
||||
|
{ |
||||
|
return [SCDeviceName isIphone] && [SCDeviceName isSimilarToIphone6orNewer]; |
||||
|
} |
||||
|
|
||||
|
+ (BOOL)isEnhancedNightModeSupported |
||||
|
{ |
||||
|
if (SC_AT_LEAST_IOS_11) { |
||||
|
return [SCDeviceName isIphone] && [SCDeviceName isSimilarToIphone6SorNewer]; |
||||
|
} |
||||
|
return NO; |
||||
|
} |
||||
|
|
||||
|
+ (CGSize)defaultActiveFormatResolution |
||||
|
{ |
||||
|
if ([SCDeviceName isIphoneX]) { |
||||
|
return CGSizeMake(kSCManagedCapturerVideoActiveFormatWidth1080p, |
||||
|
kSCManagedCapturerVideoActiveFormatHeight1080p); |
||||
|
} |
||||
|
return CGSizeMake(kSCManagedCapturerDefaultVideoActiveFormatWidth, |
||||
|
kSCManagedCapturerDefaultVideoActiveFormatHeight); |
||||
|
} |
||||
|
|
||||
|
+ (CGSize)nightModeActiveFormatResolution |
||||
|
{ |
||||
|
if ([SCManagedCaptureDevice isEnhancedNightModeSupported]) { |
||||
|
return CGSizeMake(kSCManagedCapturerNightVideoHighResActiveFormatWidth, |
||||
|
kSCManagedCapturerNightVideoHighResActiveFormatHeight); |
||||
|
} |
||||
|
return CGSizeMake(kSCManagedCapturerNightVideoDefaultResActiveFormatWidth, |
||||
|
kSCManagedCapturerNightVideoDefaultResActiveFormatHeight); |
||||
|
} |
||||
|
|
||||
|
- (instancetype)initWithDevice:(AVCaptureDevice *)device devicePosition:(SCManagedCaptureDevicePosition)devicePosition |
||||
|
{ |
||||
|
SCTraceStart(); |
||||
|
self = [super init]; |
||||
|
if (self) { |
||||
|
_device = device; |
||||
|
_devicePosition = devicePosition; |
||||
|
|
||||
|
if (SCCameraTweaksEnableFaceDetectionFocus(devicePosition)) { |
||||
|
_exposureHandler = [[SCManagedCaptureDeviceFaceDetectionAutoExposureHandler alloc] |
||||
|
initWithDevice:device |
||||
|
pointOfInterest:CGPointMake(0.5, 0.5) |
||||
|
managedCapturer:[SCManagedCapturer sharedInstance]]; |
||||
|
_focusHandler = [[SCManagedCaptureDeviceFaceDetectionAutoFocusHandler alloc] |
||||
|
initWithDevice:device |
||||
|
pointOfInterest:CGPointMake(0.5, 0.5) |
||||
|
managedCapturer:[SCManagedCapturer sharedInstance]]; |
||||
|
} else { |
||||
|
_exposureHandler = [[SCManagedCaptureDeviceAutoExposureHandler alloc] initWithDevice:device |
||||
|
pointOfInterest:CGPointMake(0.5, 0.5)]; |
||||
|
_focusHandler = [[SCManagedCaptureDeviceAutoFocusHandler alloc] initWithDevice:device |
||||
|
pointOfInterest:CGPointMake(0.5, 0.5)]; |
||||
|
} |
||||
|
_observeController = [[FBKVOController alloc] initWithObserver:self]; |
||||
|
[self _setAsExposureListenerForDevice:device]; |
||||
|
if (SCCameraTweaksEnableExposurePointObservation()) { |
||||
|
[self _observeExposurePointForDevice:device]; |
||||
|
} |
||||
|
if (SCCameraTweaksEnableFocusPointObservation()) { |
||||
|
[self _observeFocusPointForDevice:device]; |
||||
|
} |
||||
|
|
||||
|
_zoomFactor = 1.0; |
||||
|
[self _findSupportedFormats]; |
||||
|
} |
||||
|
return self; |
||||
|
} |
||||
|
|
||||
|
- (SCManagedCaptureDevicePosition)position |
||||
|
{ |
||||
|
return _devicePosition; |
||||
|
} |
||||
|
|
||||
|
#pragma mark - Setup and hook up with device |
||||
|
|
||||
|
- (BOOL)setDeviceAsInput:(AVCaptureSession *)session |
||||
|
{ |
||||
|
SCTraceStart(); |
||||
|
AVCaptureDeviceInput *deviceInput = [self deviceInput]; |
||||
|
if ([session canAddInput:deviceInput]) { |
||||
|
[session addInput:deviceInput]; |
||||
|
} else { |
||||
|
NSString *previousSessionPreset = session.sessionPreset; |
||||
|
session.sessionPreset = AVCaptureSessionPresetInputPriority; |
||||
|
// Now we surely can add input |
||||
|
if ([session canAddInput:deviceInput]) { |
||||
|
[session addInput:deviceInput]; |
||||
|
} else { |
||||
|
session.sessionPreset = previousSessionPreset; |
||||
|
return NO; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[self _enableSubjectAreaChangeMonitoring]; |
||||
|
|
||||
|
[self _updateActiveFormatWithSession:session fallbackPreset:AVCaptureSessionPreset640x480]; |
||||
|
if (_device.activeFormat.videoMaxZoomFactor < 1 + 1e-5) { |
||||
|
_softwareZoom = YES; |
||||
|
} else { |
||||
|
_softwareZoom = NO; |
||||
|
if (_device.videoZoomFactor != _zoomFactor) { |
||||
|
// Reset the zoom factor |
||||
|
[self setZoomFactor:_zoomFactor]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[_exposureHandler setVisible:YES]; |
||||
|
[_focusHandler setVisible:YES]; |
||||
|
|
||||
|
_isConnected = YES; |
||||
|
|
||||
|
return YES; |
||||
|
} |
||||
|
|
||||
|
- (void)removeDeviceAsInput:(AVCaptureSession *)session |
||||
|
{ |
||||
|
SCTraceStart(); |
||||
|
if (_isConnected) { |
||||
|
[session removeInput:_deviceInput]; |
||||
|
[_exposureHandler setVisible:NO]; |
||||
|
[_focusHandler setVisible:NO]; |
||||
|
_isConnected = NO; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
- (void)resetDeviceAsInput |
||||
|
{ |
||||
|
_deviceInput = nil; |
||||
|
AVCaptureDevice *deviceFound; |
||||
|
switch (_devicePosition) { |
||||
|
case SCManagedCaptureDevicePositionFront: |
||||
|
deviceFound = [[SCCaptureDeviceResolver sharedInstance] findAVCaptureDevice:AVCaptureDevicePositionFront]; |
||||
|
break; |
||||
|
case SCManagedCaptureDevicePositionBack: |
||||
|
deviceFound = [[SCCaptureDeviceResolver sharedInstance] findAVCaptureDevice:AVCaptureDevicePositionBack]; |
||||
|
break; |
||||
|
case SCManagedCaptureDevicePositionBackDualCamera: |
||||
|
deviceFound = [[SCCaptureDeviceResolver sharedInstance] findDualCamera]; |
||||
|
break; |
||||
|
} |
||||
|
if (deviceFound) { |
||||
|
_device = deviceFound; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
#pragma mark - Configurations |
||||
|
|
||||
|
- (void)_findSupportedFormats |
||||
|
{ |
||||
|
NSInteger defaultHeight = [SCManagedCaptureDevice defaultActiveFormatResolution].height; |
||||
|
NSInteger nightHeight = [SCManagedCaptureDevice nightModeActiveFormatResolution].height; |
||||
|
NSInteger liveVideoStreamingHeight = kSCManagedCapturerLiveStreamingVideoActiveFormatHeight; |
||||
|
NSArray *heights = @[ @(nightHeight), @(defaultHeight), @(liveVideoStreamingHeight) ]; |
||||
|
BOOL formatsShouldSupportDepth = _devicePosition == SCManagedCaptureDevicePositionBackDualCamera; |
||||
|
NSDictionary *formats = SCBestHRSIFormatsForHeights(heights, _device.formats, formatsShouldSupportDepth); |
||||
|
_nightFormat = formats[@(nightHeight)]; |
||||
|
_defaultFormat = formats[@(defaultHeight)]; |
||||
|
_liveVideoStreamingFormat = formats[@(liveVideoStreamingHeight)]; |
||||
|
} |
||||
|
|
||||
|
- (AVCaptureDeviceFormat *)_bestSupportedFormat |
||||
|
{ |
||||
|
if (_isNightModeActive) { |
||||
|
return _nightFormat; |
||||
|
} |
||||
|
if (_liveVideoStreamingActive) { |
||||
|
return _liveVideoStreamingFormat; |
||||
|
} |
||||
|
return _defaultFormat; |
||||
|
} |
||||
|
|
||||
|
- (void)setNightModeActive:(BOOL)nightModeActive session:(AVCaptureSession *)session |
||||
|
{ |
||||
|
SCTraceStart(); |
||||
|
if (![SCManagedCaptureDevice isNightModeSupported]) { |
||||
|
return; |
||||
|
} |
||||
|
if (_isNightModeActive == nightModeActive) { |
||||
|
return; |
||||
|
} |
||||
|
_isNightModeActive = nightModeActive; |
||||
|
[self updateActiveFormatWithSession:session]; |
||||
|
} |
||||
|
|
||||
|
- (void)setLiveVideoStreaming:(BOOL)liveVideoStreaming session:(AVCaptureSession *)session |
||||
|
{ |
||||
|
SCTraceStart(); |
||||
|
if (_liveVideoStreamingActive == liveVideoStreaming) { |
||||
|
return; |
||||
|
} |
||||
|
_liveVideoStreamingActive = liveVideoStreaming; |
||||
|
[self updateActiveFormatWithSession:session]; |
||||
|
} |
||||
|
|
||||
|
- (void)setCaptureDepthData:(BOOL)captureDepthData session:(AVCaptureSession *)session |
||||
|
{ |
||||
|
SCTraceStart(); |
||||
|
_captureDepthData = captureDepthData; |
||||
|
[self _findSupportedFormats]; |
||||
|
[self updateActiveFormatWithSession:session]; |
||||
|
} |
||||
|
|
||||
|
- (void)updateActiveFormatWithSession:(AVCaptureSession *)session |
||||
|
{ |
||||
|
[self _updateActiveFormatWithSession:session fallbackPreset:AVCaptureSessionPreset640x480]; |
||||
|
if (_device.videoZoomFactor != _zoomFactor) { |
||||
|
[self setZoomFactor:_zoomFactor]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
- (void)_updateActiveFormatWithSession:(AVCaptureSession *)session fallbackPreset:(NSString *)fallbackPreset |
||||
|
{ |
||||
|
AVCaptureDeviceFormat *nextFormat = [self _bestSupportedFormat]; |
||||
|
if (nextFormat && [session canSetSessionPreset:AVCaptureSessionPresetInputPriority]) { |
||||
|
session.sessionPreset = AVCaptureSessionPresetInputPriority; |
||||
|
if (nextFormat == _device.activeFormat) { |
||||
|
// Need to reconfigure frame rate though active format unchanged |
||||
|
[_device runTask:@"update frame rate" |
||||
|
withLockedConfiguration:^() { |
||||
|
[self _updateDeviceFrameRate]; |
||||
|
}]; |
||||
|
} else { |
||||
|
[_device runTask:@"update active format" |
||||
|
withLockedConfiguration:^() { |
||||
|
_device.activeFormat = nextFormat; |
||||
|
[self _updateDeviceFrameRate]; |
||||
|
}]; |
||||
|
} |
||||
|
} else { |
||||
|
session.sessionPreset = fallbackPreset; |
||||
|
} |
||||
|
[self _updateFieldOfView]; |
||||
|
} |
||||
|
|
||||
|
- (void)_updateDeviceFrameRate |
||||
|
{ |
||||
|
int32_t deviceFrameRate; |
||||
|
if (_liveVideoStreamingActive) { |
||||
|
deviceFrameRate = kSCManagedCaptureDeviceMaximumLowFrameRate; |
||||
|
} else { |
||||
|
deviceFrameRate = kSCManagedCaptureDeviceMaximumHighFrameRate; |
||||
|
} |
||||
|
CMTime frameDuration = CMTimeMake(1, deviceFrameRate); |
||||
|
if (@available(ios 11.0, *)) { |
||||
|
if (_captureDepthData) { |
||||
|
// Sync the video frame rate to the max depth frame rate (24 fps) |
||||
|
if (_device.activeDepthDataFormat.videoSupportedFrameRateRanges.firstObject) { |
||||
|
frameDuration = |
||||
|
_device.activeDepthDataFormat.videoSupportedFrameRateRanges.firstObject.minFrameDuration; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
_device.activeVideoMaxFrameDuration = frameDuration; |
||||
|
_device.activeVideoMinFrameDuration = frameDuration; |
||||
|
if (_device.lowLightBoostSupported) { |
||||
|
_device.automaticallyEnablesLowLightBoostWhenAvailable = YES; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
- (void)setZoomFactor:(float)zoomFactor |
||||
|
{ |
||||
|
SCTraceStart(); |
||||
|
if (_softwareZoom) { |
||||
|
// Just remember the software zoom scale |
||||
|
if (zoomFactor <= kSCManagedCaptureDevicecSoftwareMaxZoomFactor && zoomFactor >= 1) { |
||||
|
_zoomFactor = zoomFactor; |
||||
|
} |
||||
|
} else { |
||||
|
[_device runTask:@"set zoom factor" |
||||
|
withLockedConfiguration:^() { |
||||
|
if (zoomFactor <= _device.activeFormat.videoMaxZoomFactor && zoomFactor >= 1) { |
||||
|
_zoomFactor = zoomFactor; |
||||
|
if (_device.videoZoomFactor != _zoomFactor) { |
||||
|
_device.videoZoomFactor = _zoomFactor; |
||||
|
} |
||||
|
} |
||||
|
}]; |
||||
|
} |
||||
|
[self _updateFieldOfView]; |
||||
|
} |
||||
|
|
||||
|
- (void)_updateFieldOfView |
||||
|
{ |
||||
|
float fieldOfView = _device.activeFormat.videoFieldOfView; |
||||
|
if (_zoomFactor > 1.f) { |
||||
|
// Adjust the field of view to take the zoom factor into account. |
||||
|
// Note: this assumes the zoom factor linearly affects the focal length. |
||||
|
fieldOfView = 2.f * SCRadiansToDegrees(atanf(tanf(SCDegreesToRadians(0.5f * fieldOfView)) / _zoomFactor)); |
||||
|
} |
||||
|
self.fieldOfView = fieldOfView; |
||||
|
} |
||||
|
|
||||
|
- (void)setExposurePointOfInterest:(CGPoint)pointOfInterest fromUser:(BOOL)fromUser |
||||
|
{ |
||||
|
[_exposureHandler setExposurePointOfInterest:pointOfInterest fromUser:fromUser]; |
||||
|
} |
||||
|
|
||||
|
// called when user taps on a point on screen, to re-adjust camera focus onto that tapped spot. |
||||
|
// this re-adjustment is always necessary, regardless of scenarios (recording video, taking photo, etc), |
||||
|
// therefore we don't have to check _focusLock in this method. |
||||
|
- (void)setAutofocusPointOfInterest:(CGPoint)pointOfInterest |
||||
|
{ |
||||
|
SCTraceStart(); |
||||
|
[_focusHandler setAutofocusPointOfInterest:pointOfInterest]; |
||||
|
} |
||||
|
|
||||
|
- (void)continuousAutofocus |
||||
|
{ |
||||
|
SCTraceStart(); |
||||
|
[_focusHandler continuousAutofocus]; |
||||
|
} |
||||
|
|
||||
|
- (void)setRecording:(BOOL)recording |
||||
|
{ |
||||
|
if (SCCameraTweaksSmoothAutoFocusWhileRecording() && [_device isSmoothAutoFocusSupported]) { |
||||
|
[self _setSmoothFocus:recording]; |
||||
|
} else { |
||||
|
[self _setFocusLock:recording]; |
||||
|
} |
||||
|
[_exposureHandler setStableExposure:recording]; |
||||
|
} |
||||
|
|
||||
|
- (void)_setFocusLock:(BOOL)focusLock |
||||
|
{ |
||||
|
SCTraceStart(); |
||||
|
[_focusHandler setFocusLock:focusLock]; |
||||
|
} |
||||
|
|
||||
|
- (void)_setSmoothFocus:(BOOL)smoothFocus |
||||
|
{ |
||||
|
SCTraceStart(); |
||||
|
[_focusHandler setSmoothFocus:smoothFocus]; |
||||
|
} |
||||
|
|
||||
|
- (void)setFlashActive:(BOOL)flashActive |
||||
|
{ |
||||
|
SCTraceStart(); |
||||
|
if (_flashActive != flashActive) { |
||||
|
if ([_device hasFlash]) { |
||||
|
#pragma clang diagnostic push |
||||
|
#pragma clang diagnostic ignored "-Wdeprecated-declarations" |
||||
|
if (flashActive && [_device isFlashModeSupported:AVCaptureFlashModeOn]) { |
||||
|
[_device runTask:@"set flash active" |
||||
|
withLockedConfiguration:^() { |
||||
|
_device.flashMode = AVCaptureFlashModeOn; |
||||
|
}]; |
||||
|
} else if (!flashActive && [_device isFlashModeSupported:AVCaptureFlashModeOff]) { |
||||
|
[_device runTask:@"set flash off" |
||||
|
withLockedConfiguration:^() { |
||||
|
_device.flashMode = AVCaptureFlashModeOff; |
||||
|
}]; |
||||
|
} |
||||
|
#pragma clang diagnostic pop |
||||
|
_flashActive = flashActive; |
||||
|
} else { |
||||
|
_flashActive = NO; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
- (void)setTorchActive:(BOOL)torchActive |
||||
|
{ |
||||
|
SCTraceStart(); |
||||
|
if (_torchActive != torchActive) { |
||||
|
if ([_device hasTorch]) { |
||||
|
if (torchActive && [_device isTorchModeSupported:AVCaptureTorchModeOn]) { |
||||
|
[_device runTask:@"set torch active" |
||||
|
withLockedConfiguration:^() { |
||||
|
[_device setTorchMode:AVCaptureTorchModeOn]; |
||||
|
}]; |
||||
|
} else if (!torchActive && [_device isTorchModeSupported:AVCaptureTorchModeOff]) { |
||||
|
[_device runTask:@"set torch off" |
||||
|
withLockedConfiguration:^() { |
||||
|
_device.torchMode = AVCaptureTorchModeOff; |
||||
|
}]; |
||||
|
} |
||||
|
_torchActive = torchActive; |
||||
|
} else { |
||||
|
_torchActive = NO; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
#pragma mark - Utilities |
||||
|
|
||||
|
- (BOOL)isFlashSupported |
||||
|
{ |
||||
|
return _device.hasFlash; |
||||
|
} |
||||
|
|
||||
|
- (BOOL)isTorchSupported |
||||
|
{ |
||||
|
return _device.hasTorch; |
||||
|
} |
||||
|
|
||||
|
- (CGPoint)convertViewCoordinates:(CGPoint)viewCoordinates |
||||
|
viewSize:(CGSize)viewSize |
||||
|
videoGravity:(NSString *)videoGravity |
||||
|
{ |
||||
|
SCTraceStart(); |
||||
|
CGPoint pointOfInterest = CGPointMake(.5f, .5f); |
||||
|
CGRect cleanAperture; |
||||
|
AVCaptureDeviceInput *deviceInput = [self deviceInput]; |
||||
|
NSArray *ports = [deviceInput.ports copy]; |
||||
|
if ([videoGravity isEqualToString:AVLayerVideoGravityResize]) { |
||||
|
// Scale, switch x and y, and reverse x |
||||
|
return CGPointMake(viewCoordinates.y / viewSize.height, 1.f - (viewCoordinates.x / viewSize.width)); |
||||
|
} |
||||
|
for (AVCaptureInputPort *port in ports) { |
||||
|
if ([port mediaType] == AVMediaTypeVideo && port.formatDescription) { |
||||
|
cleanAperture = CMVideoFormatDescriptionGetCleanAperture(port.formatDescription, YES); |
||||
|
CGSize apertureSize = cleanAperture.size; |
||||
|
CGPoint point = viewCoordinates; |
||||
|
CGFloat apertureRatio = apertureSize.height / apertureSize.width; |
||||
|
CGFloat viewRatio = viewSize.width / viewSize.height; |
||||
|
CGFloat xc = .5f; |
||||
|
CGFloat yc = .5f; |
||||
|
if ([videoGravity isEqualToString:AVLayerVideoGravityResizeAspect]) { |
||||
|
if (viewRatio > apertureRatio) { |
||||
|
CGFloat y2 = viewSize.height; |
||||
|
CGFloat x2 = viewSize.height * apertureRatio; |
||||
|
CGFloat x1 = viewSize.width; |
||||
|
CGFloat blackBar = (x1 - x2) / 2; |
||||
|
// If point is inside letterboxed area, do coordinate conversion; otherwise, don't change the |
||||
|
// default value returned (.5,.5) |
||||
|
if (point.x >= blackBar && point.x <= blackBar + x2) { |
||||
|
// Scale (accounting for the letterboxing on the left and right of the video preview), |
||||
|
// switch x and y, and reverse x |
||||
|
xc = point.y / y2; |
||||
|
yc = 1.f - ((point.x - blackBar) / x2); |
||||
|
} |
||||
|
} else { |
||||
|
CGFloat y2 = viewSize.width / apertureRatio; |
||||
|
CGFloat y1 = viewSize.height; |
||||
|
CGFloat x2 = viewSize.width; |
||||
|
CGFloat blackBar = (y1 - y2) / 2; |
||||
|
// If point is inside letterboxed area, do coordinate conversion. Otherwise, don't change the |
||||
|
// default value returned (.5,.5) |
||||
|
if (point.y >= blackBar && point.y <= blackBar + y2) { |
||||
|
// Scale (accounting for the letterboxing on the top and bottom of the video preview), |
||||
|
// switch x and y, and reverse x |
||||
|
xc = ((point.y - blackBar) / y2); |
||||
|
yc = 1.f - (point.x / x2); |
||||
|
} |
||||
|
} |
||||
|
} else if ([videoGravity isEqualToString:AVLayerVideoGravityResizeAspectFill]) { |
||||
|
// Scale, switch x and y, and reverse x |
||||
|
if (viewRatio > apertureRatio) { |
||||
|
CGFloat y2 = apertureSize.width * (viewSize.width / apertureSize.height); |
||||
|
xc = (point.y + ((y2 - viewSize.height) / 2.f)) / y2; // Account for cropped height |
||||
|
yc = (viewSize.width - point.x) / viewSize.width; |
||||
|
} else { |
||||
|
CGFloat x2 = apertureSize.height * (viewSize.height / apertureSize.width); |
||||
|
yc = 1.f - ((point.x + ((x2 - viewSize.width) / 2)) / x2); // Account for cropped width |
||||
|
xc = point.y / viewSize.height; |
||||
|
} |
||||
|
} |
||||
|
pointOfInterest = CGPointMake(xc, yc); |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
return pointOfInterest; |
||||
|
} |
||||
|
|
||||
|
#pragma mark - SCManagedCapturer friendly methods |
||||
|
|
||||
|
- (AVCaptureDevice *)device |
||||
|
{ |
||||
|
return _device; |
||||
|
} |
||||
|
|
||||
|
- (AVCaptureDeviceInput *)deviceInput |
||||
|
{ |
||||
|
SCTraceStart(); |
||||
|
if (!_deviceInput) { |
||||
|
NSError *error = nil; |
||||
|
_deviceInput = [[AVCaptureDeviceInput alloc] initWithDevice:_device error:&error]; |
||||
|
if (!_deviceInput) { |
||||
|
_error = [error copy]; |
||||
|
} |
||||
|
} |
||||
|
return _deviceInput; |
||||
|
} |
||||
|
|
||||
|
- (NSError *)error |
||||
|
{ |
||||
|
return _error; |
||||
|
} |
||||
|
|
||||
|
- (BOOL)softwareZoom |
||||
|
{ |
||||
|
return _softwareZoom; |
||||
|
} |
||||
|
|
||||
|
- (BOOL)isConnected |
||||
|
{ |
||||
|
return _isConnected; |
||||
|
} |
||||
|
|
||||
|
- (BOOL)flashActive |
||||
|
{ |
||||
|
return _flashActive; |
||||
|
} |
||||
|
|
||||
|
- (BOOL)torchActive |
||||
|
{ |
||||
|
return _torchActive; |
||||
|
} |
||||
|
|
||||
|
- (float)zoomFactor |
||||
|
{ |
||||
|
return _zoomFactor; |
||||
|
} |
||||
|
|
||||
|
- (BOOL)isNightModeActive |
||||
|
{ |
||||
|
return _isNightModeActive; |
||||
|
} |
||||
|
|
||||
|
- (BOOL)liveVideoStreamingActive |
||||
|
{ |
||||
|
return _liveVideoStreamingActive; |
||||
|
} |
||||
|
|
||||
|
- (BOOL)isAvailable |
||||
|
{ |
||||
|
return [_device isConnected]; |
||||
|
} |
||||
|
|
||||
|
#pragma mark - Private methods |
||||
|
|
||||
|
- (void)_enableSubjectAreaChangeMonitoring |
||||
|
{ |
||||
|
SCTraceStart(); |
||||
|
[_device runTask:@"enable SubjectAreaChangeMonitoring" |
||||
|
withLockedConfiguration:^() { |
||||
|
_device.subjectAreaChangeMonitoringEnabled = YES; |
||||
|
}]; |
||||
|
} |
||||
|
|
||||
|
- (AVCaptureDeviceFormat *)activeFormat |
||||
|
{ |
||||
|
return _device.activeFormat; |
||||
|
} |
||||
|
|
||||
|
#pragma mark - Observe -adjustingExposure |
||||
|
- (void)_setAsExposureListenerForDevice:(AVCaptureDevice *)device |
||||
|
{ |
||||
|
SCTraceStart(); |
||||
|
SCLogCoreCameraInfo(@"Set exposure adjustment KVO for device: %ld", (long)device.position); |
||||
|
[_observeController observe:device |
||||
|
keyPath:@keypath(device, adjustingExposure) |
||||
|
options:NSKeyValueObservingOptionNew |
||||
|
action:@selector(_adjustingExposureChanged:)]; |
||||
|
} |
||||
|
|
||||
|
- (void)_adjustingExposureChanged:(NSDictionary *)change |
||||
|
{ |
||||
|
SCTraceStart(); |
||||
|
BOOL adjustingExposure = [change[NSKeyValueChangeNewKey] boolValue]; |
||||
|
SCLogCoreCameraInfo(@"KVO exposure changed to %d", adjustingExposure); |
||||
|
if ([self.delegate respondsToSelector:@selector(managedCaptureDevice:didChangeAdjustingExposure:)]) { |
||||
|
[self.delegate managedCaptureDevice:self didChangeAdjustingExposure:adjustingExposure]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
#pragma mark - Observe -exposurePointOfInterest |
||||
|
- (void)_observeExposurePointForDevice:(AVCaptureDevice *)device |
||||
|
{ |
||||
|
SCTraceStart(); |
||||
|
SCLogCoreCameraInfo(@"Set exposure point KVO for device: %ld", (long)device.position); |
||||
|
[_observeController observe:device |
||||
|
keyPath:@keypath(device, exposurePointOfInterest) |
||||
|
options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew |
||||
|
action:@selector(_exposurePointOfInterestChanged:)]; |
||||
|
} |
||||
|
|
||||
|
- (void)_exposurePointOfInterestChanged:(NSDictionary *)change |
||||
|
{ |
||||
|
SCTraceStart(); |
||||
|
CGPoint exposurePoint = [change[NSKeyValueChangeNewKey] CGPointValue]; |
||||
|
SCLogCoreCameraInfo(@"KVO exposure point changed to %@", NSStringFromCGPoint(exposurePoint)); |
||||
|
if ([self.delegate respondsToSelector:@selector(managedCaptureDevice:didChangeExposurePoint:)]) { |
||||
|
[self.delegate managedCaptureDevice:self didChangeExposurePoint:exposurePoint]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
#pragma mark - Observe -focusPointOfInterest |
||||
|
- (void)_observeFocusPointForDevice:(AVCaptureDevice *)device |
||||
|
{ |
||||
|
SCTraceStart(); |
||||
|
SCLogCoreCameraInfo(@"Set focus point KVO for device: %ld", (long)device.position); |
||||
|
[_observeController observe:device |
||||
|
keyPath:@keypath(device, focusPointOfInterest) |
||||
|
options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew |
||||
|
action:@selector(_focusPointOfInterestChanged:)]; |
||||
|
} |
||||
|
|
||||
|
- (void)_focusPointOfInterestChanged:(NSDictionary *)change |
||||
|
{ |
||||
|
SCTraceStart(); |
||||
|
CGPoint focusPoint = [change[NSKeyValueChangeNewKey] CGPointValue]; |
||||
|
SCLogCoreCameraInfo(@"KVO focus point changed to %@", NSStringFromCGPoint(focusPoint)); |
||||
|
if ([self.delegate respondsToSelector:@selector(managedCaptureDevice:didChangeFocusPoint:)]) { |
||||
|
[self.delegate managedCaptureDevice:self didChangeFocusPoint:focusPoint]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
- (void)dealloc |
||||
|
{ |
||||
|
[_observeController unobserveAll]; |
||||
|
} |
||||
|
|
||||
|
@end |
@ -0,0 +1,17 @@ |
|||||
|
// |
||||
|
// SCManagedCaptureDeviceAutoExposureHandler.h |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by Derek Peirce on 3/21/17. |
||||
|
// Copyright © 2017 Snapchat, Inc. All rights reserved. |
||||
|
// |
||||
|
|
||||
|
#import "SCManagedCaptureDeviceExposureHandler.h" |
||||
|
|
||||
|
#import <AVFoundation/AVFoundation.h> |
||||
|
|
||||
|
@interface SCManagedCaptureDeviceAutoExposureHandler : NSObject <SCManagedCaptureDeviceExposureHandler> |
||||
|
|
||||
|
- (instancetype)initWithDevice:(AVCaptureDevice *)device pointOfInterest:(CGPoint)pointOfInterest; |
||||
|
|
||||
|
@end |
@ -0,0 +1,63 @@ |
|||||
|
// |
||||
|
// SCManagedCaptureDeviceAutoExposureHandler.m |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by Derek Peirce on 3/21/17. |
||||
|
// Copyright © 2017 Snapchat, Inc. All rights reserved. |
||||
|
// |
||||
|
|
||||
|
#import "SCManagedCaptureDeviceAutoExposureHandler.h" |
||||
|
|
||||
|
#import "AVCaptureDevice+ConfigurationLock.h" |
||||
|
#import "SCManagedCaptureDeviceExposureHandler.h" |
||||
|
|
||||
|
#import <SCFoundation/SCTrace.h> |
||||
|
|
||||
|
@import AVFoundation; |
||||
|
|
||||
|
@implementation SCManagedCaptureDeviceAutoExposureHandler { |
||||
|
CGPoint _exposurePointOfInterest; |
||||
|
AVCaptureDevice *_device; |
||||
|
} |
||||
|
|
||||
|
- (instancetype)initWithDevice:(AVCaptureDevice *)device pointOfInterest:(CGPoint)pointOfInterest |
||||
|
{ |
||||
|
if (self = [super init]) { |
||||
|
_device = device; |
||||
|
_exposurePointOfInterest = pointOfInterest; |
||||
|
} |
||||
|
return self; |
||||
|
} |
||||
|
|
||||
|
- (CGPoint)getExposurePointOfInterest |
||||
|
{ |
||||
|
return _exposurePointOfInterest; |
||||
|
} |
||||
|
|
||||
|
- (void)setExposurePointOfInterest:(CGPoint)pointOfInterest fromUser:(BOOL)fromUser |
||||
|
{ |
||||
|
SCTraceStart(); |
||||
|
if (!CGPointEqualToPoint(pointOfInterest, _exposurePointOfInterest)) { |
||||
|
if ([_device isExposureModeSupported:AVCaptureExposureModeContinuousAutoExposure] && |
||||
|
[_device isExposurePointOfInterestSupported]) { |
||||
|
[_device runTask:@"set exposure" |
||||
|
withLockedConfiguration:^() { |
||||
|
// Set exposure point before changing focus mode |
||||
|
// Be noticed that order does matter |
||||
|
_device.exposurePointOfInterest = pointOfInterest; |
||||
|
_device.exposureMode = AVCaptureExposureModeContinuousAutoExposure; |
||||
|
}]; |
||||
|
} |
||||
|
_exposurePointOfInterest = pointOfInterest; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
- (void)setStableExposure:(BOOL)stableExposure |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
- (void)setVisible:(BOOL)visible |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
@end |
@ -0,0 +1,18 @@ |
|||||
|
// |
||||
|
// SCManagedCaptureDeviceAutoFocusHandler.h |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by Jiyang Zhu on 3/7/18. |
||||
|
// Copyright © 2018 Snapchat, Inc. All rights reserved. |
||||
|
// |
||||
|
// This class is used to adjust focus related parameters of camera, including focus mode and focus point. |
||||
|
|
||||
|
#import "SCManagedCaptureDeviceFocusHandler.h" |
||||
|
|
||||
|
#import <AVFoundation/AVFoundation.h> |
||||
|
|
||||
|
@interface SCManagedCaptureDeviceAutoFocusHandler : NSObject <SCManagedCaptureDeviceFocusHandler> |
||||
|
|
||||
|
- (instancetype)initWithDevice:(AVCaptureDevice *)device pointOfInterest:(CGPoint)pointOfInterest; |
||||
|
|
||||
|
@end |
@ -0,0 +1,131 @@ |
|||||
|
// |
||||
|
// SCManagedCaptureDeviceAutoFocusHandler.m |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by Jiyang Zhu on 3/7/18. |
||||
|
// Copyright © 2018 Snapchat, Inc. All rights reserved. |
||||
|
// |
||||
|
|
||||
|
#import "SCManagedCaptureDeviceAutoFocusHandler.h" |
||||
|
|
||||
|
#import "AVCaptureDevice+ConfigurationLock.h" |
||||
|
|
||||
|
#import <SCFoundation/SCTrace.h> |
||||
|
#import <SCFoundation/SCTraceODPCompatible.h> |
||||
|
|
||||
|
@import CoreGraphics; |
||||
|
|
||||
|
@interface SCManagedCaptureDeviceAutoFocusHandler () |
||||
|
|
||||
|
@property (nonatomic, assign) CGPoint focusPointOfInterest; |
||||
|
@property (nonatomic, strong) AVCaptureDevice *device; |
||||
|
|
||||
|
@property (nonatomic, assign) BOOL isContinuousAutofocus; |
||||
|
@property (nonatomic, assign) BOOL isFocusLock; |
||||
|
|
||||
|
@end |
||||
|
|
||||
|
@implementation SCManagedCaptureDeviceAutoFocusHandler |
||||
|
|
||||
|
- (instancetype)initWithDevice:(AVCaptureDevice *)device pointOfInterest:(CGPoint)pointOfInterest |
||||
|
{ |
||||
|
if (self = [super init]) { |
||||
|
_device = device; |
||||
|
_focusPointOfInterest = pointOfInterest; |
||||
|
_isContinuousAutofocus = YES; |
||||
|
_isFocusLock = NO; |
||||
|
} |
||||
|
return self; |
||||
|
} |
||||
|
|
||||
|
- (CGPoint)getFocusPointOfInterest |
||||
|
{ |
||||
|
return self.focusPointOfInterest; |
||||
|
} |
||||
|
|
||||
|
// called when user taps on a point on screen, to re-adjust camera focus onto that tapped spot. |
||||
|
// this re-adjustment is always necessary, regardless of scenarios (recording video, taking photo, etc), |
||||
|
// therefore we don't have to check self.isFocusLock in this method. |
||||
|
- (void)setAutofocusPointOfInterest:(CGPoint)pointOfInterest |
||||
|
{ |
||||
|
SCTraceODPCompatibleStart(2); |
||||
|
SC_GUARD_ELSE_RETURN(!CGPointEqualToPoint(pointOfInterest, self.focusPointOfInterest) || self.isContinuousAutofocus) |
||||
|
// Do the setup immediately if the focus lock is off. |
||||
|
if ([self.device isFocusModeSupported:AVCaptureFocusModeAutoFocus] && |
||||
|
[self.device isFocusPointOfInterestSupported]) { |
||||
|
[self.device runTask:@"set autofocus" |
||||
|
withLockedConfiguration:^() { |
||||
|
// Set focus point before changing focus mode |
||||
|
// Be noticed that order does matter |
||||
|
self.device.focusPointOfInterest = pointOfInterest; |
||||
|
self.device.focusMode = AVCaptureFocusModeAutoFocus; |
||||
|
}]; |
||||
|
} |
||||
|
self.focusPointOfInterest = pointOfInterest; |
||||
|
self.isContinuousAutofocus = NO; |
||||
|
} |
||||
|
|
||||
|
- (void)continuousAutofocus |
||||
|
{ |
||||
|
SCTraceODPCompatibleStart(2); |
||||
|
SC_GUARD_ELSE_RETURN(!self.isContinuousAutofocus); |
||||
|
if (!self.isFocusLock) { |
||||
|
// Do the setup immediately if the focus lock is off. |
||||
|
if ([self.device isFocusModeSupported:AVCaptureFocusModeContinuousAutoFocus] && |
||||
|
[self.device isFocusPointOfInterestSupported]) { |
||||
|
[self.device runTask:@"set continuous autofocus" |
||||
|
withLockedConfiguration:^() { |
||||
|
// Set focus point before changing focus mode |
||||
|
// Be noticed that order does matter |
||||
|
self.device.focusPointOfInterest = CGPointMake(0.5, 0.5); |
||||
|
self.device.focusMode = AVCaptureFocusModeContinuousAutoFocus; |
||||
|
}]; |
||||
|
} |
||||
|
} |
||||
|
self.focusPointOfInterest = CGPointMake(0.5, 0.5); |
||||
|
self.isContinuousAutofocus = YES; |
||||
|
} |
||||
|
|
||||
|
- (void)setFocusLock:(BOOL)focusLock |
||||
|
{ |
||||
|
SCTraceODPCompatibleStart(2); |
||||
|
SC_GUARD_ELSE_RETURN(self.isFocusLock != focusLock); |
||||
|
// This is the old lock, we only do focus lock on back camera |
||||
|
if (focusLock) { |
||||
|
if ([self.device isFocusModeSupported:AVCaptureFocusModeLocked]) { |
||||
|
[self.device runTask:@"set focus lock on" |
||||
|
withLockedConfiguration:^() { |
||||
|
self.device.focusMode = AVCaptureFocusModeLocked; |
||||
|
}]; |
||||
|
} |
||||
|
} else { |
||||
|
// Restore to previous autofocus configurations |
||||
|
if ([self.device isFocusModeSupported:(self.isContinuousAutofocus ? AVCaptureFocusModeContinuousAutoFocus |
||||
|
: AVCaptureFocusModeAutoFocus)] && |
||||
|
[self.device isFocusPointOfInterestSupported]) { |
||||
|
[self.device runTask:@"set focus lock on" |
||||
|
withLockedConfiguration:^() { |
||||
|
self.device.focusPointOfInterest = self.focusPointOfInterest; |
||||
|
self.device.focusMode = self.isContinuousAutofocus ? AVCaptureFocusModeContinuousAutoFocus |
||||
|
: AVCaptureFocusModeAutoFocus; |
||||
|
}]; |
||||
|
} |
||||
|
} |
||||
|
self.isFocusLock = focusLock; |
||||
|
} |
||||
|
|
||||
|
- (void)setSmoothFocus:(BOOL)smoothFocus |
||||
|
{ |
||||
|
SCTraceODPCompatibleStart(2); |
||||
|
SC_GUARD_ELSE_RETURN(smoothFocus != self.device.smoothAutoFocusEnabled); |
||||
|
[self.device runTask:@"set smooth autofocus" |
||||
|
withLockedConfiguration:^() { |
||||
|
[self.device setSmoothAutoFocusEnabled:smoothFocus]; |
||||
|
}]; |
||||
|
} |
||||
|
|
||||
|
- (void)setVisible:(BOOL)visible |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
@end |
@ -0,0 +1,25 @@ |
|||||
|
// |
||||
|
// SCManagedCaptureDeviceDefaultZoomHandler.h |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by Yu-Kuan Lai on 4/12/17. |
||||
|
// Copyright © 2017 Snapchat, Inc. All rights reserved. |
||||
|
// |
||||
|
|
||||
|
#import <SCBase/SCMacros.h> |
||||
|
|
||||
|
#import <CoreGraphics/CoreGraphics.h> |
||||
|
#import <Foundation/Foundation.h> |
||||
|
|
||||
|
@class SCManagedCaptureDevice; |
||||
|
@class SCCaptureResource; |
||||
|
|
||||
|
@interface SCManagedCaptureDeviceDefaultZoomHandler : NSObject |
||||
|
|
||||
|
SC_INIT_AND_NEW_UNAVAILABLE |
||||
|
- (instancetype)initWithCaptureResource:(SCCaptureResource *)captureResource; |
||||
|
|
||||
|
- (void)setZoomFactor:(CGFloat)zoomFactor forDevice:(SCManagedCaptureDevice *)device immediately:(BOOL)immediately; |
||||
|
- (void)softwareZoomWithDevice:(SCManagedCaptureDevice *)device; |
||||
|
|
||||
|
@end |
@ -0,0 +1,93 @@ |
|||||
|
// |
||||
|
// SCManagedCaptureDeviceDefaultZoomHandler.m |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by Yu-Kuan Lai on 4/12/17. |
||||
|
// Copyright © 2017 Snapchat, Inc. All rights reserved. |
||||
|
// |
||||
|
|
||||
|
#import "SCManagedCaptureDeviceDefaultZoomHandler_Private.h" |
||||
|
|
||||
|
#import "SCCaptureResource.h" |
||||
|
#import "SCManagedCaptureDevice+SCManagedCapturer.h" |
||||
|
#import "SCManagedCapturer.h" |
||||
|
#import "SCManagedCapturerLogging.h" |
||||
|
#import "SCManagedCapturerStateBuilder.h" |
||||
|
#import "SCMetalUtils.h" |
||||
|
|
||||
|
#import <SCFoundation/SCAssertWrapper.h> |
||||
|
#import <SCFoundation/SCQueuePerformer.h> |
||||
|
#import <SCFoundation/SCThreadHelpers.h> |
||||
|
#import <SCFoundation/SCTraceODPCompatible.h> |
||||
|
|
||||
|
@implementation SCManagedCaptureDeviceDefaultZoomHandler |
||||
|
|
||||
|
- (instancetype)initWithCaptureResource:(SCCaptureResource *)captureResource |
||||
|
{ |
||||
|
self = [super init]; |
||||
|
if (self) { |
||||
|
_captureResource = captureResource; |
||||
|
} |
||||
|
|
||||
|
return self; |
||||
|
} |
||||
|
|
||||
|
- (void)setZoomFactor:(CGFloat)zoomFactor forDevice:(SCManagedCaptureDevice *)device immediately:(BOOL)immediately |
||||
|
{ |
||||
|
[self _setZoomFactor:zoomFactor forManagedCaptureDevice:device]; |
||||
|
} |
||||
|
|
||||
|
- (void)softwareZoomWithDevice:(SCManagedCaptureDevice *)device |
||||
|
{ |
||||
|
SCTraceODPCompatibleStart(2); |
||||
|
SCAssert([_captureResource.queuePerformer isCurrentPerformer] || |
||||
|
[[SCQueuePerformer mainQueuePerformer] isCurrentPerformer], |
||||
|
@""); |
||||
|
SCAssert(device.softwareZoom, @"Only do software zoom for software zoom device"); |
||||
|
|
||||
|
SC_GUARD_ELSE_RETURN(!SCDeviceSupportsMetal()); |
||||
|
float zoomFactor = device.zoomFactor; |
||||
|
SCLogCapturerInfo(@"Adjusting software zoom factor to: %f", zoomFactor); |
||||
|
AVCaptureVideoPreviewLayer *videoPreviewLayer = _captureResource.videoPreviewLayer; |
||||
|
[[SCQueuePerformer mainQueuePerformer] perform:^{ |
||||
|
[CATransaction begin]; |
||||
|
[CATransaction setDisableActions:YES]; |
||||
|
// I end up need to change its superlayer transform to get the zoom effect |
||||
|
videoPreviewLayer.superlayer.affineTransform = CGAffineTransformMakeScale(zoomFactor, zoomFactor); |
||||
|
[CATransaction commit]; |
||||
|
}]; |
||||
|
} |
||||
|
|
||||
|
- (void)_setZoomFactor:(CGFloat)zoomFactor forManagedCaptureDevice:(SCManagedCaptureDevice *)device |
||||
|
{ |
||||
|
SCTraceODPCompatibleStart(2); |
||||
|
[_captureResource.queuePerformer perform:^{ |
||||
|
SCTraceStart(); |
||||
|
if (device) { |
||||
|
SCLogCapturerInfo(@"Set zoom factor: %f -> %f", _captureResource.state.zoomFactor, zoomFactor); |
||||
|
[device setZoomFactor:zoomFactor]; |
||||
|
BOOL zoomFactorChanged = NO; |
||||
|
// If the device is our current device, send the notification, update the |
||||
|
// state. |
||||
|
if (device.isConnected && device == _captureResource.device) { |
||||
|
if (device.softwareZoom) { |
||||
|
[self softwareZoomWithDevice:device]; |
||||
|
} |
||||
|
_captureResource.state = [[[SCManagedCapturerStateBuilder |
||||
|
withManagedCapturerState:_captureResource.state] setZoomFactor:zoomFactor] build]; |
||||
|
zoomFactorChanged = YES; |
||||
|
} |
||||
|
SCManagedCapturerState *state = [_captureResource.state copy]; |
||||
|
runOnMainThreadAsynchronously(^{ |
||||
|
if (zoomFactorChanged) { |
||||
|
[_captureResource.announcer managedCapturer:[SCManagedCapturer sharedInstance] |
||||
|
didChangeState:state]; |
||||
|
[_captureResource.announcer managedCapturer:[SCManagedCapturer sharedInstance] |
||||
|
didChangeZoomFactor:state]; |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
}]; |
||||
|
} |
||||
|
|
||||
|
@end |
@ -0,0 +1,17 @@ |
|||||
|
// |
||||
|
// SCManagedCaptureDeviceDefaultZoomHandler_Private.h |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by Joe Qiao on 04/01/2018. |
||||
|
// |
||||
|
|
||||
|
#import "SCManagedCaptureDeviceDefaultZoomHandler.h" |
||||
|
|
||||
|
@interface SCManagedCaptureDeviceDefaultZoomHandler () |
||||
|
|
||||
|
@property (nonatomic, weak) SCCaptureResource *captureResource; |
||||
|
@property (nonatomic, weak) SCManagedCaptureDevice *currentDevice; |
||||
|
|
||||
|
- (void)_setZoomFactor:(CGFloat)zoomFactor forManagedCaptureDevice:(SCManagedCaptureDevice *)device; |
||||
|
|
||||
|
@end |
@ -0,0 +1,22 @@ |
|||||
|
// |
||||
|
// SCManagedCaptureDeviceExposureHandler.h |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by Derek Peirce on 3/21/17. |
||||
|
// Copyright © 2017 Snapchat, Inc. All rights reserved. |
||||
|
// |
||||
|
|
||||
|
#import <CoreGraphics/CoreGraphics.h> |
||||
|
#import <Foundation/Foundation.h> |
||||
|
|
||||
|
@protocol SCManagedCaptureDeviceExposureHandler <NSObject> |
||||
|
|
||||
|
- (CGPoint)getExposurePointOfInterest; |
||||
|
|
||||
|
- (void)setStableExposure:(BOOL)stableExposure; |
||||
|
|
||||
|
- (void)setExposurePointOfInterest:(CGPoint)pointOfInterest fromUser:(BOOL)fromUser; |
||||
|
|
||||
|
- (void)setVisible:(BOOL)visible; |
||||
|
|
||||
|
@end |
@ -0,0 +1,28 @@ |
|||||
|
// |
||||
|
// SCManagedCaptureDeviceFaceDetectionAutoExposureHandler.h |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by Jiyang Zhu on 3/6/18. |
||||
|
// Copyright © 2018 Snapchat, Inc. All rights reserved. |
||||
|
// |
||||
|
// This class is used to |
||||
|
// 1. adjust exposure related parameters of camera, including exposure mode and exposure point. |
||||
|
// 2. receive detected face bounds, and set exposure point to a preferred face if needed. |
||||
|
|
||||
|
#import "SCManagedCaptureDeviceExposureHandler.h" |
||||
|
|
||||
|
#import <SCBase/SCMacros.h> |
||||
|
|
||||
|
#import <AVFoundation/AVFoundation.h> |
||||
|
|
||||
|
@protocol SCCapturer; |
||||
|
|
||||
|
@interface SCManagedCaptureDeviceFaceDetectionAutoExposureHandler : NSObject <SCManagedCaptureDeviceExposureHandler> |
||||
|
|
||||
|
SC_INIT_AND_NEW_UNAVAILABLE |
||||
|
|
||||
|
- (instancetype)initWithDevice:(AVCaptureDevice *)device |
||||
|
pointOfInterest:(CGPoint)pointOfInterest |
||||
|
managedCapturer:(id<SCCapturer>)managedCapturer; |
||||
|
|
||||
|
@end |
@ -0,0 +1,121 @@ |
|||||
|
// |
||||
|
// SCManagedCaptureDeviceFaceDetectionAutoExposureHandler.m |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by Jiyang Zhu on 3/6/18. |
||||
|
// Copyright © 2018 Snapchat, Inc. All rights reserved. |
||||
|
// |
||||
|
|
||||
|
#import "SCManagedCaptureDeviceFaceDetectionAutoExposureHandler.h" |
||||
|
|
||||
|
#import "AVCaptureDevice+ConfigurationLock.h" |
||||
|
#import "SCCameraTweaks.h" |
||||
|
#import "SCManagedCaptureDeviceExposureHandler.h" |
||||
|
#import "SCManagedCaptureFaceDetectionAdjustingPOIResource.h" |
||||
|
#import "SCManagedCapturer.h" |
||||
|
#import "SCManagedCapturerListener.h" |
||||
|
|
||||
|
#import <SCFoundation/SCAssertWrapper.h> |
||||
|
#import <SCFoundation/SCTrace.h> |
||||
|
#import <SCFoundation/SCTraceODPCompatible.h> |
||||
|
|
||||
|
@import AVFoundation; |
||||
|
|
||||
|
@interface SCManagedCaptureDeviceFaceDetectionAutoExposureHandler () <SCManagedCapturerListener> |
||||
|
|
||||
|
@property (nonatomic, strong) AVCaptureDevice *device; |
||||
|
@property (nonatomic, weak) id<SCCapturer> managedCapturer; |
||||
|
@property (nonatomic, assign) CGPoint exposurePointOfInterest; |
||||
|
@property (nonatomic, assign) BOOL isVisible; |
||||
|
|
||||
|
@property (nonatomic, copy) NSDictionary<NSNumber *, NSValue *> *faceBoundsByFaceID; |
||||
|
@property (nonatomic, strong) SCManagedCaptureFaceDetectionAdjustingPOIResource *resource; |
||||
|
|
||||
|
@end |
||||
|
|
||||
|
@implementation SCManagedCaptureDeviceFaceDetectionAutoExposureHandler |
||||
|
|
||||
|
- (instancetype)initWithDevice:(AVCaptureDevice *)device |
||||
|
pointOfInterest:(CGPoint)pointOfInterest |
||||
|
managedCapturer:(id<SCCapturer>)managedCapturer |
||||
|
{ |
||||
|
if (self = [super init]) { |
||||
|
SCAssert(device, @"AVCaptureDevice should not be nil."); |
||||
|
SCAssert(managedCapturer, @"id<SCCapturer> should not be nil."); |
||||
|
_device = device; |
||||
|
_exposurePointOfInterest = pointOfInterest; |
||||
|
SCManagedCaptureDevicePosition position = |
||||
|
(device.position == AVCaptureDevicePositionFront ? SCManagedCaptureDevicePositionFront |
||||
|
: SCManagedCaptureDevicePositionBack); |
||||
|
_resource = [[SCManagedCaptureFaceDetectionAdjustingPOIResource alloc] |
||||
|
initWithDefaultPointOfInterest:pointOfInterest |
||||
|
shouldTargetOnFaceAutomatically:SCCameraTweaksTurnOnFaceDetectionFocusByDefault(position)]; |
||||
|
_managedCapturer = managedCapturer; |
||||
|
} |
||||
|
return self; |
||||
|
} |
||||
|
|
||||
|
- (void)dealloc |
||||
|
{ |
||||
|
[_managedCapturer removeListener:self]; |
||||
|
} |
||||
|
|
||||
|
- (CGPoint)getExposurePointOfInterest |
||||
|
{ |
||||
|
return self.exposurePointOfInterest; |
||||
|
} |
||||
|
|
||||
|
- (void)setExposurePointOfInterest:(CGPoint)pointOfInterest fromUser:(BOOL)fromUser |
||||
|
{ |
||||
|
SCTraceODPCompatibleStart(2); |
||||
|
|
||||
|
pointOfInterest = [self.resource updateWithNewProposedPointOfInterest:pointOfInterest fromUser:fromUser]; |
||||
|
|
||||
|
[self _actuallySetExposurePointOfInterestIfNeeded:pointOfInterest]; |
||||
|
} |
||||
|
|
||||
|
- (void)_actuallySetExposurePointOfInterestIfNeeded:(CGPoint)pointOfInterest |
||||
|
{ |
||||
|
SCTraceODPCompatibleStart(2); |
||||
|
SC_GUARD_ELSE_RETURN(!CGPointEqualToPoint(pointOfInterest, self.exposurePointOfInterest)); |
||||
|
if ([self.device isExposureModeSupported:AVCaptureExposureModeContinuousAutoExposure] && |
||||
|
[self.device isExposurePointOfInterestSupported]) { |
||||
|
[self.device runTask:@"set exposure" |
||||
|
withLockedConfiguration:^() { |
||||
|
// Set exposure point before changing exposure mode |
||||
|
// Be noticed that order does matter |
||||
|
self.device.exposurePointOfInterest = pointOfInterest; |
||||
|
self.device.exposureMode = AVCaptureExposureModeContinuousAutoExposure; |
||||
|
}]; |
||||
|
} |
||||
|
self.exposurePointOfInterest = pointOfInterest; |
||||
|
} |
||||
|
|
||||
|
- (void)setStableExposure:(BOOL)stableExposure |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
- (void)setVisible:(BOOL)visible |
||||
|
{ |
||||
|
SCTraceODPCompatibleStart(2); |
||||
|
SC_GUARD_ELSE_RETURN(_isVisible != visible); |
||||
|
_isVisible = visible; |
||||
|
if (visible) { |
||||
|
[self.managedCapturer addListener:self]; |
||||
|
} else { |
||||
|
[self.managedCapturer removeListener:self]; |
||||
|
[self.resource reset]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
#pragma mark - SCManagedCapturerListener |
||||
|
- (void)managedCapturer:(id<SCCapturer>)managedCapturer |
||||
|
didDetectFaceBounds:(NSDictionary<NSNumber *, NSValue *> *)faceBoundsByFaceID |
||||
|
{ |
||||
|
SCTraceODPCompatibleStart(2); |
||||
|
SC_GUARD_ELSE_RETURN(self.isVisible); |
||||
|
CGPoint pointOfInterest = [self.resource updateWithNewDetectedFaceBounds:faceBoundsByFaceID]; |
||||
|
[self _actuallySetExposurePointOfInterestIfNeeded:pointOfInterest]; |
||||
|
} |
||||
|
|
||||
|
@end |
@ -0,0 +1,28 @@ |
|||||
|
// |
||||
|
// SCManagedCaptureDeviceFaceDetectionAutoFocusHandler.h |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by Jiyang Zhu on 3/7/18. |
||||
|
// Copyright © 2018 Snapchat, Inc. All rights reserved. |
||||
|
// |
||||
|
// This class is used to |
||||
|
// 1. adjust focus related parameters of camera, including focus mode and focus point. |
||||
|
// 2. receive detected face bounds, and focus to a preferred face if needed. |
||||
|
|
||||
|
#import "SCManagedCaptureDeviceFocusHandler.h" |
||||
|
|
||||
|
#import <SCBase/SCMacros.h> |
||||
|
|
||||
|
#import <AVFoundation/AVFoundation.h> |
||||
|
|
||||
|
@protocol SCCapturer; |
||||
|
|
||||
|
@interface SCManagedCaptureDeviceFaceDetectionAutoFocusHandler : NSObject <SCManagedCaptureDeviceFocusHandler> |
||||
|
|
||||
|
SC_INIT_AND_NEW_UNAVAILABLE |
||||
|
|
||||
|
- (instancetype)initWithDevice:(AVCaptureDevice *)device |
||||
|
pointOfInterest:(CGPoint)pointOfInterest |
||||
|
managedCapturer:(id<SCCapturer>)managedCapturer; |
||||
|
|
||||
|
@end |
@ -0,0 +1,153 @@ |
|||||
|
// |
||||
|
// SCManagedCaptureDeviceFaceDetectionAutoFocusHandler.m |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by Jiyang Zhu on 3/7/18. |
||||
|
// Copyright © 2018 Snapchat, Inc. All rights reserved. |
||||
|
// |
||||
|
|
||||
|
#import "SCManagedCaptureDeviceFaceDetectionAutoFocusHandler.h" |
||||
|
|
||||
|
#import "AVCaptureDevice+ConfigurationLock.h" |
||||
|
#import "SCCameraTweaks.h" |
||||
|
#import "SCManagedCaptureFaceDetectionAdjustingPOIResource.h" |
||||
|
#import "SCManagedCapturer.h" |
||||
|
#import "SCManagedCapturerListener.h" |
||||
|
|
||||
|
#import <SCFoundation/SCAssertWrapper.h> |
||||
|
#import <SCFoundation/SCTrace.h> |
||||
|
#import <SCFoundation/SCTraceODPCompatible.h> |
||||
|
|
||||
|
@interface SCManagedCaptureDeviceFaceDetectionAutoFocusHandler () <SCManagedCapturerListener> |
||||
|
|
||||
|
@property (nonatomic, strong) AVCaptureDevice *device; |
||||
|
@property (nonatomic, weak) id<SCCapturer> managedCapturer; |
||||
|
@property (nonatomic, assign) CGPoint focusPointOfInterest; |
||||
|
|
||||
|
@property (nonatomic, assign) BOOL isVisible; |
||||
|
@property (nonatomic, assign) BOOL isContinuousAutofocus; |
||||
|
@property (nonatomic, assign) BOOL focusLock; |
||||
|
|
||||
|
@property (nonatomic, copy) NSDictionary<NSNumber *, NSValue *> *faceBoundsByFaceID; |
||||
|
@property (nonatomic, strong) SCManagedCaptureFaceDetectionAdjustingPOIResource *resource; |
||||
|
|
||||
|
@end |
||||
|
|
||||
|
@implementation SCManagedCaptureDeviceFaceDetectionAutoFocusHandler |
||||
|
|
||||
|
- (instancetype)initWithDevice:(AVCaptureDevice *)device |
||||
|
pointOfInterest:(CGPoint)pointOfInterest |
||||
|
managedCapturer:(id<SCCapturer>)managedCapturer |
||||
|
{ |
||||
|
if (self = [super init]) { |
||||
|
SCAssert(device, @"AVCaptureDevice should not be nil."); |
||||
|
SCAssert(managedCapturer, @"id<SCCapturer> should not be nil."); |
||||
|
_device = device; |
||||
|
_focusPointOfInterest = pointOfInterest; |
||||
|
SCManagedCaptureDevicePosition position = |
||||
|
(device.position == AVCaptureDevicePositionFront ? SCManagedCaptureDevicePositionFront |
||||
|
: SCManagedCaptureDevicePositionBack); |
||||
|
_resource = [[SCManagedCaptureFaceDetectionAdjustingPOIResource alloc] |
||||
|
initWithDefaultPointOfInterest:pointOfInterest |
||||
|
shouldTargetOnFaceAutomatically:SCCameraTweaksTurnOnFaceDetectionFocusByDefault(position)]; |
||||
|
_managedCapturer = managedCapturer; |
||||
|
} |
||||
|
return self; |
||||
|
} |
||||
|
|
||||
|
- (CGPoint)getFocusPointOfInterest |
||||
|
{ |
||||
|
return self.focusPointOfInterest; |
||||
|
} |
||||
|
|
||||
|
// called when user taps on a point on screen, to re-adjust camera focus onto that tapped spot. |
||||
|
// this re-adjustment is always necessary, regardless of scenarios (recording video, taking photo, etc), |
||||
|
// therefore we don't have to check self.focusLock in this method. |
||||
|
- (void)setAutofocusPointOfInterest:(CGPoint)pointOfInterest |
||||
|
{ |
||||
|
SCTraceODPCompatibleStart(2); |
||||
|
pointOfInterest = [self.resource updateWithNewProposedPointOfInterest:pointOfInterest fromUser:YES]; |
||||
|
SC_GUARD_ELSE_RETURN(!CGPointEqualToPoint(pointOfInterest, self.focusPointOfInterest) || |
||||
|
self.isContinuousAutofocus); |
||||
|
[self _actuallySetFocusPointOfInterestIfNeeded:pointOfInterest |
||||
|
withFocusMode:AVCaptureFocusModeAutoFocus |
||||
|
taskName:@"set autofocus"]; |
||||
|
} |
||||
|
|
||||
|
- (void)continuousAutofocus |
||||
|
{ |
||||
|
SCTraceODPCompatibleStart(2); |
||||
|
SC_GUARD_ELSE_RETURN(!self.isContinuousAutofocus); |
||||
|
CGPoint pointOfInterest = [self.resource updateWithNewProposedPointOfInterest:CGPointMake(0.5, 0.5) fromUser:NO]; |
||||
|
[self _actuallySetFocusPointOfInterestIfNeeded:pointOfInterest |
||||
|
withFocusMode:AVCaptureFocusModeContinuousAutoFocus |
||||
|
taskName:@"set continuous autofocus"]; |
||||
|
} |
||||
|
|
||||
|
- (void)setFocusLock:(BOOL)focusLock |
||||
|
{ |
||||
|
// Disabled focus lock for face detection and focus handler. |
||||
|
} |
||||
|
|
||||
|
- (void)setSmoothFocus:(BOOL)smoothFocus |
||||
|
{ |
||||
|
SCTraceODPCompatibleStart(2); |
||||
|
SC_GUARD_ELSE_RETURN(smoothFocus != self.device.smoothAutoFocusEnabled); |
||||
|
[self.device runTask:@"set smooth autofocus" |
||||
|
withLockedConfiguration:^() { |
||||
|
[self.device setSmoothAutoFocusEnabled:smoothFocus]; |
||||
|
}]; |
||||
|
} |
||||
|
|
||||
|
- (void)setVisible:(BOOL)visible |
||||
|
{ |
||||
|
SCTraceODPCompatibleStart(2); |
||||
|
SC_GUARD_ELSE_RETURN(_isVisible != visible); |
||||
|
self.isVisible = visible; |
||||
|
if (visible) { |
||||
|
[[SCManagedCapturer sharedInstance] addListener:self]; |
||||
|
} else { |
||||
|
[[SCManagedCapturer sharedInstance] removeListener:self]; |
||||
|
[self.resource reset]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
- (void)_actuallySetFocusPointOfInterestIfNeeded:(CGPoint)pointOfInterest |
||||
|
withFocusMode:(AVCaptureFocusMode)focusMode |
||||
|
taskName:(NSString *)taskName |
||||
|
{ |
||||
|
SCTraceODPCompatibleStart(2); |
||||
|
SC_GUARD_ELSE_RETURN(!CGPointEqualToPoint(pointOfInterest, self.focusPointOfInterest) && |
||||
|
[self.device isFocusModeSupported:focusMode] && [self.device isFocusPointOfInterestSupported]); |
||||
|
[self.device runTask:taskName |
||||
|
withLockedConfiguration:^() { |
||||
|
// Set focus point before changing focus mode |
||||
|
// Be noticed that order does matter |
||||
|
self.device.focusPointOfInterest = pointOfInterest; |
||||
|
self.device.focusMode = focusMode; |
||||
|
}]; |
||||
|
|
||||
|
self.focusPointOfInterest = pointOfInterest; |
||||
|
self.isContinuousAutofocus = (focusMode == AVCaptureFocusModeContinuousAutoFocus); |
||||
|
} |
||||
|
|
||||
|
#pragma mark - SCManagedCapturerListener |
||||
|
- (void)managedCapturer:(id<SCCapturer>)managedCapturer |
||||
|
didDetectFaceBounds:(NSDictionary<NSNumber *, NSValue *> *)faceBoundsByFaceID |
||||
|
{ |
||||
|
SCTraceODPCompatibleStart(2); |
||||
|
SC_GUARD_ELSE_RETURN(self.isVisible); |
||||
|
CGPoint pointOfInterest = [self.resource updateWithNewDetectedFaceBounds:faceBoundsByFaceID]; |
||||
|
// If pointOfInterest is equal to CGPointMake(0.5, 0.5), it means no valid face is found, so that we should reset to |
||||
|
// AVCaptureFocusModeContinuousAutoFocus. Otherwise, focus on the point and set the mode as |
||||
|
// AVCaptureFocusModeAutoFocus. |
||||
|
// TODO(Jiyang): Refactor SCManagedCaptureFaceDetectionAdjustingPOIResource to include focusMode and exposureMode. |
||||
|
AVCaptureFocusMode focusMode = CGPointEqualToPoint(pointOfInterest, CGPointMake(0.5, 0.5)) |
||||
|
? AVCaptureFocusModeContinuousAutoFocus |
||||
|
: AVCaptureFocusModeAutoFocus; |
||||
|
[self _actuallySetFocusPointOfInterestIfNeeded:pointOfInterest |
||||
|
withFocusMode:focusMode |
||||
|
taskName:@"set autofocus from face detection"]; |
||||
|
} |
||||
|
|
||||
|
@end |
@ -0,0 +1,28 @@ |
|||||
|
// |
||||
|
// SCManagedCaptureDeviceFocusHandler.h |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by Jiyang Zhu on 3/7/18. |
||||
|
// Copyright © 2018 Snapchat, Inc. All rights reserved. |
||||
|
// |
||||
|
|
||||
|
#import <CoreGraphics/CoreGraphics.h> |
||||
|
#import <Foundation/Foundation.h> |
||||
|
|
||||
|
@protocol SCManagedCaptureDeviceFocusHandler <NSObject> |
||||
|
|
||||
|
- (CGPoint)getFocusPointOfInterest; |
||||
|
|
||||
|
/// Called when subject area changes. |
||||
|
- (void)continuousAutofocus; |
||||
|
|
||||
|
/// Called when user taps. |
||||
|
- (void)setAutofocusPointOfInterest:(CGPoint)pointOfInterest; |
||||
|
|
||||
|
- (void)setSmoothFocus:(BOOL)smoothFocus; |
||||
|
|
||||
|
- (void)setFocusLock:(BOOL)focusLock; |
||||
|
|
||||
|
- (void)setVisible:(BOOL)visible; |
||||
|
|
||||
|
@end |
@ -0,0 +1,23 @@ |
|||||
|
// |
||||
|
// SCManagedCaptureDeviceHandler.h |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by Jiyang Zhu on 3/8/18. |
||||
|
// Copyright © 2018 Snapchat, Inc. All rights reserved. |
||||
|
// |
||||
|
|
||||
|
#import "SCManagedCaptureDevice.h" |
||||
|
|
||||
|
#import <SCBase/SCMacros.h> |
||||
|
|
||||
|
#import <Foundation/Foundation.h> |
||||
|
|
||||
|
@class SCCaptureResource; |
||||
|
|
||||
|
@interface SCManagedCaptureDeviceHandler : NSObject <SCManagedCaptureDeviceDelegate> |
||||
|
|
||||
|
SC_INIT_AND_NEW_UNAVAILABLE |
||||
|
|
||||
|
- (instancetype)initWithCaptureResource:(SCCaptureResource *)captureResource; |
||||
|
|
||||
|
@end |
@ -0,0 +1,77 @@ |
|||||
|
// |
||||
|
// SCManagedCaptureDeviceHandler.m |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by Jiyang Zhu on 3/8/18. |
||||
|
// Copyright © 2018 Snapchat, Inc. All rights reserved. |
||||
|
// |
||||
|
|
||||
|
#import "SCManagedCaptureDeviceHandler.h" |
||||
|
|
||||
|
#import "SCCaptureResource.h" |
||||
|
#import "SCManagedCapturer.h" |
||||
|
#import "SCManagedCapturerLogging.h" |
||||
|
#import "SCManagedCapturerState.h" |
||||
|
#import "SCManagedCapturerStateBuilder.h" |
||||
|
|
||||
|
#import <SCFoundation/SCAssertWrapper.h> |
||||
|
#import <SCFoundation/SCQueuePerformer.h> |
||||
|
#import <SCFoundation/SCThreadHelpers.h> |
||||
|
#import <SCFoundation/SCTraceODPCompatible.h> |
||||
|
|
||||
|
@interface SCManagedCaptureDeviceHandler () |
||||
|
|
||||
|
@property (nonatomic, weak) SCCaptureResource *captureResource; |
||||
|
|
||||
|
@end |
||||
|
|
||||
|
@implementation SCManagedCaptureDeviceHandler |
||||
|
|
||||
|
- (instancetype)initWithCaptureResource:(SCCaptureResource *)captureResource |
||||
|
{ |
||||
|
self = [super init]; |
||||
|
if (self) { |
||||
|
SCAssert(captureResource, @"SCCaptureResource should not be nil."); |
||||
|
_captureResource = captureResource; |
||||
|
} |
||||
|
return self; |
||||
|
} |
||||
|
|
||||
|
- (void)managedCaptureDevice:(SCManagedCaptureDevice *)device didChangeAdjustingExposure:(BOOL)adjustingExposure |
||||
|
{ |
||||
|
SC_GUARD_ELSE_RETURN(device == _captureResource.device); |
||||
|
SCTraceODPCompatibleStart(2); |
||||
|
SCLogCapturerInfo(@"KVO Changes adjustingExposure %d", adjustingExposure); |
||||
|
[_captureResource.queuePerformer perform:^{ |
||||
|
_captureResource.state = [[[SCManagedCapturerStateBuilder withManagedCapturerState:_captureResource.state] |
||||
|
setAdjustingExposure:adjustingExposure] build]; |
||||
|
SCManagedCapturerState *state = [_captureResource.state copy]; |
||||
|
runOnMainThreadAsynchronously(^{ |
||||
|
[_captureResource.announcer managedCapturer:[SCManagedCapturer sharedInstance] didChangeState:state]; |
||||
|
[_captureResource.announcer managedCapturer:[SCManagedCapturer sharedInstance] |
||||
|
didChangeAdjustingExposure:state]; |
||||
|
}); |
||||
|
}]; |
||||
|
} |
||||
|
|
||||
|
- (void)managedCaptureDevice:(SCManagedCaptureDevice *)device didChangeExposurePoint:(CGPoint)exposurePoint |
||||
|
{ |
||||
|
SC_GUARD_ELSE_RETURN(device == self.captureResource.device); |
||||
|
SCTraceODPCompatibleStart(2); |
||||
|
runOnMainThreadAsynchronously(^{ |
||||
|
[self.captureResource.announcer managedCapturer:[SCManagedCapturer sharedInstance] |
||||
|
didChangeExposurePoint:exposurePoint]; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
- (void)managedCaptureDevice:(SCManagedCaptureDevice *)device didChangeFocusPoint:(CGPoint)focusPoint |
||||
|
{ |
||||
|
SC_GUARD_ELSE_RETURN(device == self.captureResource.device); |
||||
|
SCTraceODPCompatibleStart(2); |
||||
|
runOnMainThreadAsynchronously(^{ |
||||
|
[self.captureResource.announcer managedCapturer:[SCManagedCapturer sharedInstance] |
||||
|
didChangeFocusPoint:focusPoint]; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
@end |
@ -0,0 +1,12 @@ |
|||||
|
// |
||||
|
// SCManagedCaptureDeviceLinearInterpolationZoomHandler.h |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by Joe Qiao on 03/01/2018. |
||||
|
// |
||||
|
|
||||
|
#import "SCManagedCaptureDeviceDefaultZoomHandler.h" |
||||
|
|
||||
|
@interface SCManagedCaptureDeviceLinearInterpolationZoomHandler : SCManagedCaptureDeviceDefaultZoomHandler |
||||
|
|
||||
|
@end |
@ -0,0 +1,190 @@ |
|||||
|
// |
||||
|
// SCManagedCaptureDeviceLinearInterpolationZoomHandler.m |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by Joe Qiao on 03/01/2018. |
||||
|
// |
||||
|
|
||||
|
#import "SCManagedCaptureDeviceLinearInterpolationZoomHandler.h" |
||||
|
|
||||
|
#import "SCCameraTweaks.h" |
||||
|
#import "SCManagedCaptureDeviceDefaultZoomHandler_Private.h" |
||||
|
#import "SCManagedCapturerLogging.h" |
||||
|
|
||||
|
#import <SCFoundation/SCAssertWrapper.h> |
||||
|
#import <SCFoundation/SCMathUtils.h> |
||||
|
|
||||
|
@interface SCManagedCaptureDeviceLinearInterpolationZoomHandler () |
||||
|
|
||||
|
@property (nonatomic, strong) CADisplayLink *displayLink; |
||||
|
@property (nonatomic, assign) double timestamp; |
||||
|
@property (nonatomic, assign) float targetFactor; |
||||
|
@property (nonatomic, assign) float intermediateFactor; |
||||
|
@property (nonatomic, assign) int trend; |
||||
|
@property (nonatomic, assign) float stepLength; |
||||
|
|
||||
|
@end |
||||
|
|
||||
|
@implementation SCManagedCaptureDeviceLinearInterpolationZoomHandler |
||||
|
|
||||
|
- (instancetype)initWithCaptureResource:(SCCaptureResource *)captureResource |
||||
|
{ |
||||
|
self = [super initWithCaptureResource:captureResource]; |
||||
|
if (self) { |
||||
|
_timestamp = -1.0; |
||||
|
_targetFactor = 1.0; |
||||
|
_intermediateFactor = _targetFactor; |
||||
|
_trend = 1; |
||||
|
_stepLength = 0.0; |
||||
|
} |
||||
|
|
||||
|
return self; |
||||
|
} |
||||
|
|
||||
|
- (void)dealloc |
||||
|
{ |
||||
|
[self _invalidate]; |
||||
|
} |
||||
|
|
||||
|
- (void)setZoomFactor:(CGFloat)zoomFactor forDevice:(SCManagedCaptureDevice *)device immediately:(BOOL)immediately |
||||
|
{ |
||||
|
if (self.currentDevice != device) { |
||||
|
if (_displayLink) { |
||||
|
// if device changed, interupt smoothing process |
||||
|
// and reset to target zoom factor immediately |
||||
|
[self _resetToZoomFactor:_targetFactor]; |
||||
|
} |
||||
|
self.currentDevice = device; |
||||
|
immediately = YES; |
||||
|
} |
||||
|
|
||||
|
if (immediately) { |
||||
|
[self _resetToZoomFactor:zoomFactor]; |
||||
|
} else { |
||||
|
[self _addTargetZoomFactor:zoomFactor]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
#pragma mark - Configurable |
||||
|
// smoothen if the update time interval is greater than the threshold |
||||
|
- (double)_thresholdTimeIntervalToSmoothen |
||||
|
{ |
||||
|
return SCCameraTweaksSmoothZoomThresholdTime(); |
||||
|
} |
||||
|
|
||||
|
- (double)_thresholdFactorDiffToSmoothen |
||||
|
{ |
||||
|
return SCCameraTweaksSmoothZoomThresholdFactor(); |
||||
|
} |
||||
|
|
||||
|
- (int)_intermediateFactorFramesPerSecond |
||||
|
{ |
||||
|
return SCCameraTweaksSmoothZoomIntermediateFramesPerSecond(); |
||||
|
} |
||||
|
|
||||
|
- (double)_delayTolerantTime |
||||
|
{ |
||||
|
return SCCameraTweaksSmoothZoomDelayTolerantTime(); |
||||
|
} |
||||
|
|
||||
|
// minimum step length between two intermediate factors, |
||||
|
// the greater the better as long as could provide a 'smooth experience' during smoothing process |
||||
|
- (float)_minimumStepLength |
||||
|
{ |
||||
|
return SCCameraTweaksSmoothZoomMinStepLength(); |
||||
|
} |
||||
|
|
||||
|
#pragma mark - Private methods |
||||
|
- (void)_addTargetZoomFactor:(float)factor |
||||
|
{ |
||||
|
SCAssertMainThread(); |
||||
|
|
||||
|
SCLogCapturerInfo(@"Smooth Zoom - [1] t=%f zf=%f", CACurrentMediaTime(), factor); |
||||
|
if (SCFloatEqual(factor, _targetFactor)) { |
||||
|
return; |
||||
|
} |
||||
|
_targetFactor = factor; |
||||
|
|
||||
|
float diff = _targetFactor - _intermediateFactor; |
||||
|
if ([self _isDuringSmoothingProcess]) { |
||||
|
// during smoothing, only update data |
||||
|
[self _updateDataWithDiff:diff]; |
||||
|
} else { |
||||
|
double curTimestamp = CACurrentMediaTime(); |
||||
|
if (!SCFloatEqual(_timestamp, -1.0) && (curTimestamp - _timestamp) > [self _thresholdTimeIntervalToSmoothen] && |
||||
|
ABS(diff) > [self _thresholdFactorDiffToSmoothen]) { |
||||
|
// need smoothing |
||||
|
[self _updateDataWithDiff:diff]; |
||||
|
if ([self _nextStep]) { |
||||
|
// use timer to interpolate intermediate factors to avoid sharp jump |
||||
|
_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(_nextStep)]; |
||||
|
_displayLink.preferredFramesPerSecond = [self _intermediateFactorFramesPerSecond]; |
||||
|
[_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; |
||||
|
} |
||||
|
} else { |
||||
|
_timestamp = curTimestamp; |
||||
|
_intermediateFactor = factor; |
||||
|
|
||||
|
SCLogCapturerInfo(@"Smooth Zoom - [2] t=%f zf=%f", CACurrentMediaTime(), _intermediateFactor); |
||||
|
[self _setZoomFactor:_intermediateFactor forManagedCaptureDevice:self.currentDevice]; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
- (void)_resetToZoomFactor:(float)factor |
||||
|
{ |
||||
|
[self _invalidate]; |
||||
|
|
||||
|
_timestamp = -1.0; |
||||
|
_targetFactor = factor; |
||||
|
_intermediateFactor = _targetFactor; |
||||
|
|
||||
|
[self _setZoomFactor:_intermediateFactor forManagedCaptureDevice:self.currentDevice]; |
||||
|
} |
||||
|
|
||||
|
- (BOOL)_nextStep |
||||
|
{ |
||||
|
_timestamp = CACurrentMediaTime(); |
||||
|
_intermediateFactor += (_trend * _stepLength); |
||||
|
|
||||
|
BOOL hasNext = YES; |
||||
|
if (_trend < 0.0) { |
||||
|
_intermediateFactor = MAX(_intermediateFactor, _targetFactor); |
||||
|
} else { |
||||
|
_intermediateFactor = MIN(_intermediateFactor, _targetFactor); |
||||
|
} |
||||
|
|
||||
|
SCLogCapturerInfo(@"Smooth Zoom - [3] t=%f zf=%f", CACurrentMediaTime(), _intermediateFactor); |
||||
|
[self _setZoomFactor:_intermediateFactor forManagedCaptureDevice:self.currentDevice]; |
||||
|
|
||||
|
if (SCFloatEqual(_intermediateFactor, _targetFactor)) { |
||||
|
// finish smoothening |
||||
|
[self _invalidate]; |
||||
|
hasNext = NO; |
||||
|
} |
||||
|
|
||||
|
return hasNext; |
||||
|
} |
||||
|
|
||||
|
- (void)_invalidate |
||||
|
{ |
||||
|
[_displayLink invalidate]; |
||||
|
_displayLink = nil; |
||||
|
_trend = 1; |
||||
|
_stepLength = 0.0; |
||||
|
} |
||||
|
|
||||
|
- (void)_updateDataWithDiff:(CGFloat)diff |
||||
|
{ |
||||
|
_trend = diff < 0.0 ? -1 : 1; |
||||
|
_stepLength = |
||||
|
MAX(_stepLength, MAX([self _minimumStepLength], |
||||
|
ABS(diff) / ([self _delayTolerantTime] * [self _intermediateFactorFramesPerSecond]))); |
||||
|
} |
||||
|
|
||||
|
- (BOOL)_isDuringSmoothingProcess |
||||
|
{ |
||||
|
return (_displayLink ? YES : NO); |
||||
|
} |
||||
|
|
||||
|
@end |
@ -0,0 +1,20 @@ |
|||||
|
// |
||||
|
// SCManagedCaptureDeviceLockOnRecordExposureHandler.h |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by Derek Peirce on 3/24/17. |
||||
|
// Copyright © 2017 Snapchat, Inc. All rights reserved. |
||||
|
// |
||||
|
|
||||
|
#import "SCManagedCaptureDeviceExposureHandler.h" |
||||
|
|
||||
|
#import <AVFoundation/AVFoundation.h> |
||||
|
|
||||
|
// An exposure handler that prevents any changes in exposure as soon as recording begins |
||||
|
@interface SCManagedCaptureDeviceLockOnRecordExposureHandler : NSObject <SCManagedCaptureDeviceExposureHandler> |
||||
|
|
||||
|
- (instancetype)initWithDevice:(AVCaptureDevice *)device |
||||
|
pointOfInterest:(CGPoint)pointOfInterest |
||||
|
allowTap:(BOOL)allowTap; |
||||
|
|
||||
|
@end |
@ -0,0 +1,90 @@ |
|||||
|
// |
||||
|
// SCManagedCaptureDeviceLockOnRecordExposureHandler.m |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by Derek Peirce on 3/24/17. |
||||
|
// Copyright © 2017 Snapchat, Inc. All rights reserved. |
||||
|
// |
||||
|
|
||||
|
#import "SCManagedCaptureDeviceLockOnRecordExposureHandler.h" |
||||
|
|
||||
|
#import "AVCaptureDevice+ConfigurationLock.h" |
||||
|
#import "SCExposureState.h" |
||||
|
#import "SCManagedCaptureDeviceExposureHandler.h" |
||||
|
|
||||
|
#import <SCFoundation/SCTrace.h> |
||||
|
|
||||
|
@import AVFoundation; |
||||
|
|
||||
|
@implementation SCManagedCaptureDeviceLockOnRecordExposureHandler { |
||||
|
CGPoint _exposurePointOfInterest; |
||||
|
AVCaptureDevice *_device; |
||||
|
// allows the exposure to change when the user taps to refocus |
||||
|
BOOL _allowTap; |
||||
|
SCExposureState *_exposureState; |
||||
|
} |
||||
|
|
||||
|
- (instancetype)initWithDevice:(AVCaptureDevice *)device |
||||
|
pointOfInterest:(CGPoint)pointOfInterest |
||||
|
allowTap:(BOOL)allowTap |
||||
|
{ |
||||
|
if (self = [super init]) { |
||||
|
_device = device; |
||||
|
_exposurePointOfInterest = pointOfInterest; |
||||
|
_allowTap = allowTap; |
||||
|
} |
||||
|
return self; |
||||
|
} |
||||
|
|
||||
|
- (CGPoint)getExposurePointOfInterest |
||||
|
{ |
||||
|
return _exposurePointOfInterest; |
||||
|
} |
||||
|
|
||||
|
- (void)setExposurePointOfInterest:(CGPoint)pointOfInterest fromUser:(BOOL)fromUser |
||||
|
{ |
||||
|
SCTraceStart(); |
||||
|
BOOL locked = _device.exposureMode == AVCaptureExposureModeLocked || |
||||
|
_device.exposureMode == AVCaptureExposureModeCustom || |
||||
|
_device.exposureMode == AVCaptureExposureModeAutoExpose; |
||||
|
if (!locked || (fromUser && _allowTap)) { |
||||
|
AVCaptureExposureMode exposureMode = |
||||
|
(locked ? AVCaptureExposureModeAutoExpose : AVCaptureExposureModeContinuousAutoExposure); |
||||
|
if ([_device isExposureModeSupported:exposureMode] && [_device isExposurePointOfInterestSupported]) { |
||||
|
[_device runTask:@"set exposure point" |
||||
|
withLockedConfiguration:^() { |
||||
|
// Set exposure point before changing focus mode |
||||
|
// Be noticed that order does matter |
||||
|
_device.exposurePointOfInterest = pointOfInterest; |
||||
|
_device.exposureMode = exposureMode; |
||||
|
}]; |
||||
|
} |
||||
|
_exposurePointOfInterest = pointOfInterest; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
- (void)setStableExposure:(BOOL)stableExposure |
||||
|
{ |
||||
|
AVCaptureExposureMode exposureMode = |
||||
|
stableExposure ? AVCaptureExposureModeLocked : AVCaptureExposureModeContinuousAutoExposure; |
||||
|
if ([_device isExposureModeSupported:exposureMode]) { |
||||
|
[_device runTask:@"set stable exposure" |
||||
|
withLockedConfiguration:^() { |
||||
|
_device.exposureMode = exposureMode; |
||||
|
}]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
- (void)setVisible:(BOOL)visible |
||||
|
{ |
||||
|
if (visible) { |
||||
|
if (_device.exposureMode == AVCaptureExposureModeLocked || |
||||
|
_device.exposureMode == AVCaptureExposureModeCustom) { |
||||
|
[_exposureState applyISOAndExposureDurationToDevice:_device]; |
||||
|
} |
||||
|
} else { |
||||
|
_exposureState = [[SCExposureState alloc] initWithDevice:_device]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@end |
@ -0,0 +1,13 @@ |
|||||
|
// |
||||
|
// SCManagedCaptureDeviceSavitzkyGolayZoomHandler.h |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by Yu-Kuan Lai on 4/12/17. |
||||
|
// Copyright © 2017 Snapchat, Inc. All rights reserved. |
||||
|
// |
||||
|
|
||||
|
#import "SCManagedCaptureDeviceDefaultZoomHandler.h" |
||||
|
|
||||
|
@interface SCManagedCaptureDeviceSavitzkyGolayZoomHandler : SCManagedCaptureDeviceDefaultZoomHandler |
||||
|
|
||||
|
@end |
@ -0,0 +1,95 @@ |
|||||
|
// |
||||
|
// SCManagedCaptureDeviceSavitzkyGolayZoomHandler.m |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by Yu-Kuan Lai on 4/12/17. |
||||
|
// Copyright © 2017 Snapchat, Inc. All rights reserved. |
||||
|
// https://en.wikipedia.org/wiki/Savitzky%E2%80%93Golay_filter |
||||
|
// |
||||
|
|
||||
|
#import "SCManagedCaptureDeviceSavitzkyGolayZoomHandler.h" |
||||
|
|
||||
|
#import "SCManagedCaptureDevice.h" |
||||
|
#import "SCManagedCaptureDeviceDefaultZoomHandler_Private.h" |
||||
|
|
||||
|
#import <SCFoundation/SCQueuePerformer.h> |
||||
|
#import <SCFoundation/SCTraceODPCompatible.h> |
||||
|
|
||||
|
static NSUInteger const kSCSavitzkyGolayWindowSize = 9; |
||||
|
static CGFloat const kSCUpperSharpZoomThreshold = 1.15; |
||||
|
|
||||
|
@interface SCManagedCaptureDeviceSavitzkyGolayZoomHandler () |
||||
|
|
||||
|
@property (nonatomic, strong) NSMutableArray *zoomFactorHistoryArray; |
||||
|
|
||||
|
@end |
||||
|
|
||||
|
@implementation SCManagedCaptureDeviceSavitzkyGolayZoomHandler |
||||
|
|
||||
|
- (instancetype)initWithCaptureResource:(SCCaptureResource *)captureResource |
||||
|
{ |
||||
|
self = [super initWithCaptureResource:captureResource]; |
||||
|
if (self) { |
||||
|
_zoomFactorHistoryArray = [[NSMutableArray alloc] init]; |
||||
|
} |
||||
|
|
||||
|
return self; |
||||
|
} |
||||
|
|
||||
|
- (void)setZoomFactor:(CGFloat)zoomFactor forDevice:(SCManagedCaptureDevice *)device immediately:(BOOL)immediately |
||||
|
{ |
||||
|
if (self.currentDevice != device) { |
||||
|
// reset if device changed |
||||
|
self.currentDevice = device; |
||||
|
[self _resetZoomFactor:zoomFactor forDevice:self.currentDevice]; |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
if (immediately || zoomFactor == 1 || _zoomFactorHistoryArray.count == 0) { |
||||
|
// reset if zoomFactor is 1 or this is the first data point |
||||
|
[self _resetZoomFactor:zoomFactor forDevice:device]; |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
CGFloat lastVal = [[_zoomFactorHistoryArray lastObject] floatValue]; |
||||
|
CGFloat upperThreshold = lastVal * kSCUpperSharpZoomThreshold; |
||||
|
if (zoomFactor > upperThreshold) { |
||||
|
// sharp change in zoomFactor, reset |
||||
|
[self _resetZoomFactor:zoomFactor forDevice:device]; |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
[_zoomFactorHistoryArray addObject:@(zoomFactor)]; |
||||
|
if ([_zoomFactorHistoryArray count] > kSCSavitzkyGolayWindowSize) { |
||||
|
[_zoomFactorHistoryArray removeObjectAtIndex:0]; |
||||
|
} |
||||
|
|
||||
|
float filteredZoomFactor = |
||||
|
SC_CLAMP([self _savitzkyGolayFilteredZoomFactor], kSCMinVideoZoomFactor, kSCMaxVideoZoomFactor); |
||||
|
[self _setZoomFactor:filteredZoomFactor forManagedCaptureDevice:device]; |
||||
|
} |
||||
|
|
||||
|
- (CGFloat)_savitzkyGolayFilteredZoomFactor |
||||
|
{ |
||||
|
if ([_zoomFactorHistoryArray count] == kSCSavitzkyGolayWindowSize) { |
||||
|
CGFloat filteredZoomFactor = |
||||
|
59 * [_zoomFactorHistoryArray[4] floatValue] + |
||||
|
54 * ([_zoomFactorHistoryArray[3] floatValue] + [_zoomFactorHistoryArray[5] floatValue]) + |
||||
|
39 * ([_zoomFactorHistoryArray[2] floatValue] + [_zoomFactorHistoryArray[6] floatValue]) + |
||||
|
14 * ([_zoomFactorHistoryArray[1] floatValue] + [_zoomFactorHistoryArray[7] floatValue]) - |
||||
|
21 * ([_zoomFactorHistoryArray[0] floatValue] + [_zoomFactorHistoryArray[8] floatValue]); |
||||
|
filteredZoomFactor /= 231; |
||||
|
return filteredZoomFactor; |
||||
|
} else { |
||||
|
return [[_zoomFactorHistoryArray lastObject] floatValue]; // use zoomFactor directly if we have less than 9 |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
- (void)_resetZoomFactor:(CGFloat)zoomFactor forDevice:(SCManagedCaptureDevice *)device |
||||
|
{ |
||||
|
[_zoomFactorHistoryArray removeAllObjects]; |
||||
|
[_zoomFactorHistoryArray addObject:@(zoomFactor)]; |
||||
|
[self _setZoomFactor:zoomFactor forManagedCaptureDevice:device]; |
||||
|
} |
||||
|
|
||||
|
@end |
@ -0,0 +1,23 @@ |
|||||
|
// |
||||
|
// SCManagedCaptureDeviceSubjectAreaHandler.h |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by Xiaokang Liu on 19/03/2018. |
||||
|
// |
||||
|
// This class is used to handle the AVCaptureDeviceSubjectAreaDidChangeNotification notification for SCManagedCapturer. |
||||
|
// To reset device's settings when the subject area changed |
||||
|
|
||||
|
#import <SCBase/SCMacros.h> |
||||
|
|
||||
|
#import <Foundation/Foundation.h> |
||||
|
|
||||
|
@class SCCaptureResource; |
||||
|
@protocol SCCapturer; |
||||
|
|
||||
|
@interface SCManagedCaptureDeviceSubjectAreaHandler : NSObject |
||||
|
SC_INIT_AND_NEW_UNAVAILABLE |
||||
|
- (instancetype)initWithCaptureResource:(SCCaptureResource *)captureResource NS_DESIGNATED_INITIALIZER; |
||||
|
|
||||
|
- (void)stopObserving; |
||||
|
- (void)startObserving; |
||||
|
@end |
@ -0,0 +1,67 @@ |
|||||
|
// |
||||
|
// SCManagedCaptureDeviceSubjectAreaHandler.m |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by Xiaokang Liu on 19/03/2018. |
||||
|
// |
||||
|
|
||||
|
#import "SCManagedCaptureDeviceSubjectAreaHandler.h" |
||||
|
|
||||
|
#import "SCCameraTweaks.h" |
||||
|
#import "SCCaptureResource.h" |
||||
|
#import "SCCaptureWorker.h" |
||||
|
#import "SCManagedCaptureDevice+SCManagedCapturer.h" |
||||
|
#import "SCManagedCapturer.h" |
||||
|
#import "SCManagedCapturerState.h" |
||||
|
|
||||
|
#import <SCFoundation/SCAssertWrapper.h> |
||||
|
#import <SCFoundation/SCQueuePerformer.h> |
||||
|
|
||||
|
@interface SCManagedCaptureDeviceSubjectAreaHandler () { |
||||
|
__weak SCCaptureResource *_captureResource; |
||||
|
} |
||||
|
@end |
||||
|
|
||||
|
@implementation SCManagedCaptureDeviceSubjectAreaHandler |
||||
|
- (instancetype)initWithCaptureResource:(SCCaptureResource *)captureResource |
||||
|
{ |
||||
|
self = [super init]; |
||||
|
if (self) { |
||||
|
SCAssert(captureResource, @""); |
||||
|
_captureResource = captureResource; |
||||
|
} |
||||
|
return self; |
||||
|
} |
||||
|
|
||||
|
- (void)stopObserving |
||||
|
{ |
||||
|
[[NSNotificationCenter defaultCenter] removeObserver:self |
||||
|
name:AVCaptureDeviceSubjectAreaDidChangeNotification |
||||
|
object:nil]; |
||||
|
} |
||||
|
|
||||
|
- (void)startObserving |
||||
|
{ |
||||
|
[[NSNotificationCenter defaultCenter] addObserver:self |
||||
|
selector:@selector(_subjectAreaDidChange:) |
||||
|
name:AVCaptureDeviceSubjectAreaDidChangeNotification |
||||
|
object:nil]; |
||||
|
} |
||||
|
|
||||
|
#pragma mark - Private methods |
||||
|
- (void)_subjectAreaDidChange:(NSDictionary *)notification |
||||
|
{ |
||||
|
[_captureResource.queuePerformer perform:^{ |
||||
|
if (_captureResource.device.isConnected && !_captureResource.state.arSessionActive) { |
||||
|
// Reset to continuous autofocus when the subject area changed |
||||
|
[_captureResource.device continuousAutofocus]; |
||||
|
[_captureResource.device setExposurePointOfInterest:CGPointMake(0.5, 0.5) fromUser:NO]; |
||||
|
if (SCCameraTweaksEnablePortraitModeAutofocus()) { |
||||
|
[SCCaptureWorker setPortraitModePointOfInterestAsynchronously:CGPointMake(0.5, 0.5) |
||||
|
completionHandler:nil |
||||
|
resource:_captureResource]; |
||||
|
} |
||||
|
} |
||||
|
}]; |
||||
|
} |
||||
|
@end |
@ -0,0 +1,19 @@ |
|||||
|
// |
||||
|
// SCManagedCaptureDeviceThresholdExposureHandler.h |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by Derek Peirce on 4/11/17. |
||||
|
// Copyright © 2017 Snapchat, Inc. All rights reserved. |
||||
|
// |
||||
|
|
||||
|
#import "SCManagedCaptureDeviceExposureHandler.h" |
||||
|
|
||||
|
#import <AVFoundation/AVFoundation.h> |
||||
|
|
||||
|
@interface SCManagedCaptureDeviceThresholdExposureHandler : NSObject <SCManagedCaptureDeviceExposureHandler> |
||||
|
|
||||
|
- (instancetype)initWithDevice:(AVCaptureDevice *)device |
||||
|
pointOfInterest:(CGPoint)pointOfInterest |
||||
|
threshold:(CGFloat)threshold; |
||||
|
|
||||
|
@end |
@ -0,0 +1,133 @@ |
|||||
|
// |
||||
|
// SCManagedCaptureDeviceThresholdExposureHandler.m |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by Derek Peirce on 4/11/17. |
||||
|
// Copyright © 2017 Snapchat, Inc. All rights reserved. |
||||
|
// |
||||
|
|
||||
|
#import "SCManagedCaptureDeviceThresholdExposureHandler.h" |
||||
|
|
||||
|
#import "AVCaptureDevice+ConfigurationLock.h" |
||||
|
#import "SCCameraTweaks.h" |
||||
|
#import "SCExposureState.h" |
||||
|
#import "SCManagedCaptureDeviceExposureHandler.h" |
||||
|
|
||||
|
#import <SCFoundation/SCTrace.h> |
||||
|
|
||||
|
#import <FBKVOController/FBKVOController.h> |
||||
|
|
||||
|
@import AVFoundation; |
||||
|
|
||||
|
@implementation SCManagedCaptureDeviceThresholdExposureHandler { |
||||
|
AVCaptureDevice *_device; |
||||
|
CGPoint _exposurePointOfInterest; |
||||
|
CGFloat _threshold; |
||||
|
// allows the exposure to change when the user taps to refocus |
||||
|
SCExposureState *_exposureState; |
||||
|
FBKVOController *_kvoController; |
||||
|
} |
||||
|
|
||||
|
- (instancetype)initWithDevice:(AVCaptureDevice *)device |
||||
|
pointOfInterest:(CGPoint)pointOfInterest |
||||
|
threshold:(CGFloat)threshold |
||||
|
{ |
||||
|
if (self = [super init]) { |
||||
|
_device = device; |
||||
|
_exposurePointOfInterest = pointOfInterest; |
||||
|
_threshold = threshold; |
||||
|
_kvoController = [FBKVOController controllerWithObserver:self]; |
||||
|
@weakify(self); |
||||
|
[_kvoController observe:device |
||||
|
keyPath:NSStringFromSelector(@selector(exposureMode)) |
||||
|
options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew |
||||
|
block:^(id observer, id object, NSDictionary *change) { |
||||
|
@strongify(self); |
||||
|
AVCaptureExposureMode old = |
||||
|
(AVCaptureExposureMode)[(NSNumber *)change[NSKeyValueChangeOldKey] intValue]; |
||||
|
AVCaptureExposureMode new = |
||||
|
(AVCaptureExposureMode)[(NSNumber *)change[NSKeyValueChangeNewKey] intValue]; |
||||
|
if (old == AVCaptureExposureModeAutoExpose && new == AVCaptureExposureModeLocked) { |
||||
|
// auto expose is done, go back to custom |
||||
|
self->_exposureState = [[SCExposureState alloc] initWithDevice:self->_device]; |
||||
|
[self->_exposureState applyISOAndExposureDurationToDevice:self->_device]; |
||||
|
} |
||||
|
}]; |
||||
|
[_kvoController observe:device |
||||
|
keyPath:NSStringFromSelector(@selector(exposureTargetOffset)) |
||||
|
options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew |
||||
|
block:^(id observer, id object, NSDictionary *change) { |
||||
|
@strongify(self); |
||||
|
if (self->_device.exposureMode == AVCaptureExposureModeCustom) { |
||||
|
CGFloat offset = [(NSNumber *)change[NSKeyValueChangeOldKey] floatValue]; |
||||
|
if (fabs(offset) > self->_threshold) { |
||||
|
[self->_device runTask:@"set exposure point" |
||||
|
withLockedConfiguration:^() { |
||||
|
// Set exposure point before changing focus mode |
||||
|
// Be noticed that order does matter |
||||
|
self->_device.exposurePointOfInterest = CGPointMake(0.5, 0.5); |
||||
|
self->_device.exposureMode = AVCaptureExposureModeAutoExpose; |
||||
|
}]; |
||||
|
} |
||||
|
} |
||||
|
}]; |
||||
|
} |
||||
|
return self; |
||||
|
} |
||||
|
|
||||
|
- (CGPoint)getExposurePointOfInterest |
||||
|
{ |
||||
|
return _exposurePointOfInterest; |
||||
|
} |
||||
|
|
||||
|
- (void)setExposurePointOfInterest:(CGPoint)pointOfInterest fromUser:(BOOL)fromUser |
||||
|
{ |
||||
|
SCTraceStart(); |
||||
|
BOOL locked = _device.exposureMode == AVCaptureExposureModeLocked || |
||||
|
_device.exposureMode == AVCaptureExposureModeCustom || |
||||
|
_device.exposureMode == AVCaptureExposureModeAutoExpose; |
||||
|
if (!locked || fromUser) { |
||||
|
AVCaptureExposureMode exposureMode = |
||||
|
(locked ? AVCaptureExposureModeAutoExpose : AVCaptureExposureModeContinuousAutoExposure); |
||||
|
if ([_device isExposureModeSupported:exposureMode] && [_device isExposurePointOfInterestSupported]) { |
||||
|
[_device runTask:@"set exposure point" |
||||
|
withLockedConfiguration:^() { |
||||
|
// Set exposure point before changing focus mode |
||||
|
// Be noticed that order does matter |
||||
|
_device.exposurePointOfInterest = pointOfInterest; |
||||
|
_device.exposureMode = exposureMode; |
||||
|
}]; |
||||
|
} |
||||
|
_exposurePointOfInterest = pointOfInterest; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
- (void)setStableExposure:(BOOL)stableExposure |
||||
|
{ |
||||
|
if (stableExposure) { |
||||
|
_exposureState = [[SCExposureState alloc] initWithDevice:_device]; |
||||
|
[_exposureState applyISOAndExposureDurationToDevice:_device]; |
||||
|
} else { |
||||
|
AVCaptureExposureMode exposureMode = AVCaptureExposureModeContinuousAutoExposure; |
||||
|
if ([_device isExposureModeSupported:exposureMode]) { |
||||
|
[_device runTask:@"set exposure point" |
||||
|
withLockedConfiguration:^() { |
||||
|
_device.exposureMode = AVCaptureExposureModeContinuousAutoExposure; |
||||
|
}]; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
- (void)setVisible:(BOOL)visible |
||||
|
{ |
||||
|
if (visible) { |
||||
|
if (_device.exposureMode == AVCaptureExposureModeLocked || |
||||
|
_device.exposureMode == AVCaptureExposureModeCustom) { |
||||
|
[_exposureState applyISOAndExposureDurationToDevice:_device]; |
||||
|
} |
||||
|
} else { |
||||
|
_exposureState = [[SCExposureState alloc] initWithDevice:_device]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@end |
@ -0,0 +1,61 @@ |
|||||
|
// |
||||
|
// SCManagedCaptureFaceDetectionAdjustingPOIResource.h |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by Jiyang Zhu on 3/7/18. |
||||
|
// Copyright © 2018 Snapchat, Inc. All rights reserved. |
||||
|
// |
||||
|
// This class is used to keep several properties for face detection and focus/exposure. It provides methods to help |
||||
|
// FaceDetectionAutoFocusHandler and FaceDetectionAutoExposureHandler to deal with the point of interest setting events |
||||
|
// from user taps, subject area changes, and face detection, by updating itself and return the actual point of |
||||
|
// interest. |
||||
|
|
||||
|
#import <CoreGraphics/CoreGraphics.h> |
||||
|
#import <Foundation/Foundation.h> |
||||
|
|
||||
|
typedef NS_ENUM(NSInteger, SCManagedCaptureFaceDetectionAdjustingPOIMode) { |
||||
|
SCManagedCaptureFaceDetectionAdjustingPOIModeNone = 0, |
||||
|
SCManagedCaptureFaceDetectionAdjustingPOIModeFixedOnPointWithFace, |
||||
|
SCManagedCaptureFaceDetectionAdjustingPOIModeFixedOnPointWithoutFace, |
||||
|
}; |
||||
|
|
||||
|
@interface SCManagedCaptureFaceDetectionAdjustingPOIResource : NSObject |
||||
|
|
||||
|
@property (nonatomic, assign) CGPoint pointOfInterest; |
||||
|
|
||||
|
@property (nonatomic, strong) NSDictionary<NSNumber *, NSValue *> *faceBoundsByFaceID; |
||||
|
@property (nonatomic, assign) SCManagedCaptureFaceDetectionAdjustingPOIMode adjustingPOIMode; |
||||
|
@property (nonatomic, assign) BOOL shouldTargetOnFaceAutomatically; |
||||
|
@property (nonatomic, strong) NSNumber *targetingFaceID; |
||||
|
@property (nonatomic, assign) CGRect targetingFaceBounds; |
||||
|
|
||||
|
- (instancetype)initWithDefaultPointOfInterest:(CGPoint)pointOfInterest |
||||
|
shouldTargetOnFaceAutomatically:(BOOL)shouldTargetOnFaceAutomatically; |
||||
|
|
||||
|
- (void)reset; |
||||
|
|
||||
|
/** |
||||
|
Update SCManagedCaptureFaceDetectionAdjustingPOIResource when a new POI adjustment comes. It will find the face that |
||||
|
the proposedPoint belongs to, return the center of the face, if the adjustingPOIMode and fromUser meets the |
||||
|
requirements. |
||||
|
|
||||
|
@param proposedPoint |
||||
|
The point of interest that upper level wants to set. |
||||
|
@param fromUser |
||||
|
Whether the setting is from user's tap or not. |
||||
|
@return |
||||
|
The actual point of interest that should be applied. |
||||
|
*/ |
||||
|
- (CGPoint)updateWithNewProposedPointOfInterest:(CGPoint)proposedPoint fromUser:(BOOL)fromUser; |
||||
|
|
||||
|
/** |
||||
|
Update SCManagedCaptureFaceDetectionAdjustingPOIResource when new detected face bounds comes. |
||||
|
|
||||
|
@param faceBoundsByFaceID |
||||
|
A dictionary. Key: FaceID as NSNumber. Value: FaceBounds as CGRect. |
||||
|
@return |
||||
|
The actual point of interest that should be applied. |
||||
|
*/ |
||||
|
- (CGPoint)updateWithNewDetectedFaceBounds:(NSDictionary<NSNumber *, NSValue *> *)faceBoundsByFaceID; |
||||
|
|
||||
|
@end |
@ -0,0 +1,232 @@ |
|||||
|
// |
||||
|
// SCManagedCaptureFaceDetectionAdjustingPOIResource.m |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by Jiyang Zhu on 3/7/18. |
||||
|
// Copyright © 2018 Snapchat, Inc. All rights reserved. |
||||
|
// |
||||
|
|
||||
|
#import "SCManagedCaptureFaceDetectionAdjustingPOIResource.h" |
||||
|
|
||||
|
#import <SCFoundation/SCLog.h> |
||||
|
#import <SCFoundation/SCTrace.h> |
||||
|
#import <SCFoundation/SCTraceODPCompatible.h> |
||||
|
|
||||
|
@implementation SCManagedCaptureFaceDetectionAdjustingPOIResource { |
||||
|
CGPoint _defaultPointOfInterest; |
||||
|
} |
||||
|
|
||||
|
#pragma mark - Public Methods |
||||
|
|
||||
|
- (instancetype)initWithDefaultPointOfInterest:(CGPoint)pointOfInterest |
||||
|
shouldTargetOnFaceAutomatically:(BOOL)shouldTargetOnFaceAutomatically |
||||
|
{ |
||||
|
if (self = [super init]) { |
||||
|
_pointOfInterest = pointOfInterest; |
||||
|
_defaultPointOfInterest = pointOfInterest; |
||||
|
_shouldTargetOnFaceAutomatically = shouldTargetOnFaceAutomatically; |
||||
|
} |
||||
|
return self; |
||||
|
} |
||||
|
|
||||
|
- (void)reset |
||||
|
{ |
||||
|
SCTraceODPCompatibleStart(2); |
||||
|
self.adjustingPOIMode = SCManagedCaptureFaceDetectionAdjustingPOIModeNone; |
||||
|
self.targetingFaceID = nil; |
||||
|
self.targetingFaceBounds = CGRectZero; |
||||
|
self.faceBoundsByFaceID = nil; |
||||
|
self.pointOfInterest = _defaultPointOfInterest; |
||||
|
} |
||||
|
|
||||
|
- (CGPoint)updateWithNewProposedPointOfInterest:(CGPoint)proposedPoint fromUser:(BOOL)fromUser |
||||
|
{ |
||||
|
SCTraceODPCompatibleStart(2); |
||||
|
if (fromUser) { |
||||
|
NSNumber *faceID = |
||||
|
[self _getFaceIDOfFaceBoundsContainingPoint:proposedPoint fromFaceBounds:self.faceBoundsByFaceID]; |
||||
|
if (faceID && [faceID integerValue] >= 0) { |
||||
|
CGPoint point = [self _getPointOfInterestWithFaceID:faceID fromFaceBounds:self.faceBoundsByFaceID]; |
||||
|
if ([self _isPointOfInterestValid:point]) { |
||||
|
[self _setPointOfInterest:point |
||||
|
targetingFaceID:faceID |
||||
|
adjustingPOIMode:SCManagedCaptureFaceDetectionAdjustingPOIModeFixedOnPointWithFace]; |
||||
|
} else { |
||||
|
[self _setPointOfInterest:proposedPoint |
||||
|
targetingFaceID:nil |
||||
|
adjustingPOIMode:SCManagedCaptureFaceDetectionAdjustingPOIModeFixedOnPointWithoutFace]; |
||||
|
} |
||||
|
} else { |
||||
|
[self _setPointOfInterest:proposedPoint |
||||
|
targetingFaceID:nil |
||||
|
adjustingPOIMode:SCManagedCaptureFaceDetectionAdjustingPOIModeFixedOnPointWithoutFace]; |
||||
|
} |
||||
|
} else { |
||||
|
[self _setPointOfInterest:proposedPoint |
||||
|
targetingFaceID:nil |
||||
|
adjustingPOIMode:SCManagedCaptureFaceDetectionAdjustingPOIModeNone]; |
||||
|
} |
||||
|
return self.pointOfInterest; |
||||
|
} |
||||
|
|
||||
|
- (CGPoint)updateWithNewDetectedFaceBounds:(NSDictionary<NSNumber *, NSValue *> *)faceBoundsByFaceID |
||||
|
{ |
||||
|
SCTraceODPCompatibleStart(2); |
||||
|
self.faceBoundsByFaceID = faceBoundsByFaceID; |
||||
|
switch (self.adjustingPOIMode) { |
||||
|
case SCManagedCaptureFaceDetectionAdjustingPOIModeNone: { |
||||
|
if (self.shouldTargetOnFaceAutomatically) { |
||||
|
[self _focusOnPreferredFaceInFaceBounds:self.faceBoundsByFaceID]; |
||||
|
} |
||||
|
} break; |
||||
|
case SCManagedCaptureFaceDetectionAdjustingPOIModeFixedOnPointWithFace: { |
||||
|
BOOL isFocusingOnCurrentTargetingFaceSuccess = |
||||
|
[self _focusOnFaceWithTargetFaceID:self.targetingFaceID inFaceBounds:self.faceBoundsByFaceID]; |
||||
|
if (!isFocusingOnCurrentTargetingFaceSuccess && self.shouldTargetOnFaceAutomatically) { |
||||
|
// If the targeted face has disappeared, and shouldTargetOnFaceAutomatically is YES, automatically target on |
||||
|
// the next preferred face. |
||||
|
[self _focusOnPreferredFaceInFaceBounds:self.faceBoundsByFaceID]; |
||||
|
} |
||||
|
} break; |
||||
|
case SCManagedCaptureFaceDetectionAdjustingPOIModeFixedOnPointWithoutFace: |
||||
|
// The point of interest should be fixed at a non-face point where user tapped before. |
||||
|
break; |
||||
|
} |
||||
|
return self.pointOfInterest; |
||||
|
} |
||||
|
|
||||
|
#pragma mark - Internal Methods |
||||
|
|
||||
|
- (BOOL)_focusOnPreferredFaceInFaceBounds:(NSDictionary<NSNumber *, NSValue *> *)faceBoundsByFaceID |
||||
|
{ |
||||
|
SCTraceODPCompatibleStart(2); |
||||
|
NSNumber *preferredFaceID = [self _getPreferredFaceIDFromFaceBounds:faceBoundsByFaceID]; |
||||
|
return [self _focusOnFaceWithTargetFaceID:preferredFaceID inFaceBounds:faceBoundsByFaceID]; |
||||
|
} |
||||
|
|
||||
|
- (BOOL)_focusOnFaceWithTargetFaceID:(NSNumber *)preferredFaceID |
||||
|
inFaceBounds:(NSDictionary<NSNumber *, NSValue *> *)faceBoundsByFaceID |
||||
|
{ |
||||
|
SCTraceODPCompatibleStart(2); |
||||
|
SC_GUARD_ELSE_RETURN_VALUE(preferredFaceID, NO); |
||||
|
NSValue *faceBoundsValue = [faceBoundsByFaceID objectForKey:preferredFaceID]; |
||||
|
if (faceBoundsValue) { |
||||
|
CGRect faceBounds = [faceBoundsValue CGRectValue]; |
||||
|
CGPoint proposedPoint = CGPointMake(CGRectGetMidX(faceBounds), CGRectGetMidY(faceBounds)); |
||||
|
if ([self _isPointOfInterestValid:proposedPoint]) { |
||||
|
if ([self _shouldChangeToNewPoint:proposedPoint withNewFaceID:preferredFaceID newFaceBounds:faceBounds]) { |
||||
|
[self _setPointOfInterest:proposedPoint |
||||
|
targetingFaceID:preferredFaceID |
||||
|
adjustingPOIMode:SCManagedCaptureFaceDetectionAdjustingPOIModeFixedOnPointWithFace]; |
||||
|
} |
||||
|
return YES; |
||||
|
} |
||||
|
} |
||||
|
[self reset]; |
||||
|
return NO; |
||||
|
} |
||||
|
|
||||
|
- (void)_setPointOfInterest:(CGPoint)pointOfInterest |
||||
|
targetingFaceID:(NSNumber *)targetingFaceID |
||||
|
adjustingPOIMode:(SCManagedCaptureFaceDetectionAdjustingPOIMode)adjustingPOIMode |
||||
|
{ |
||||
|
SCTraceODPCompatibleStart(2); |
||||
|
self.pointOfInterest = pointOfInterest; |
||||
|
self.targetingFaceID = targetingFaceID; |
||||
|
if (targetingFaceID) { // If targetingFaceID exists, record the current face bounds. |
||||
|
self.targetingFaceBounds = [[self.faceBoundsByFaceID objectForKey:targetingFaceID] CGRectValue]; |
||||
|
} else { // Otherwise, reset targetingFaceBounds to zero. |
||||
|
self.targetingFaceBounds = CGRectZero; |
||||
|
} |
||||
|
self.adjustingPOIMode = adjustingPOIMode; |
||||
|
} |
||||
|
|
||||
|
- (BOOL)_isPointOfInterestValid:(CGPoint)pointOfInterest |
||||
|
{ |
||||
|
return (pointOfInterest.x >= 0 && pointOfInterest.x <= 1 && pointOfInterest.y >= 0 && pointOfInterest.y <= 1); |
||||
|
} |
||||
|
|
||||
|
- (NSNumber *)_getPreferredFaceIDFromFaceBounds:(NSDictionary<NSNumber *, NSValue *> *)faceBoundsByFaceID |
||||
|
{ |
||||
|
SCTraceODPCompatibleStart(2); |
||||
|
SC_GUARD_ELSE_RETURN_VALUE(faceBoundsByFaceID.count > 0, nil); |
||||
|
|
||||
|
// Find out the bounds with the max area. |
||||
|
__block NSNumber *preferredFaceID = nil; |
||||
|
__block CGFloat maxArea = 0; |
||||
|
[faceBoundsByFaceID |
||||
|
enumerateKeysAndObjectsUsingBlock:^(NSNumber *_Nonnull key, NSValue *_Nonnull obj, BOOL *_Nonnull stop) { |
||||
|
CGRect faceBounds = [obj CGRectValue]; |
||||
|
CGFloat area = CGRectGetWidth(faceBounds) * CGRectGetHeight(faceBounds); |
||||
|
if (area > maxArea) { |
||||
|
preferredFaceID = key; |
||||
|
maxArea = area; |
||||
|
} |
||||
|
}]; |
||||
|
|
||||
|
return preferredFaceID; |
||||
|
} |
||||
|
|
||||
|
- (CGPoint)_getPointOfInterestWithFaceID:(NSNumber *)faceID |
||||
|
fromFaceBounds:(NSDictionary<NSNumber *, NSValue *> *)faceBoundsByFaceID |
||||
|
{ |
||||
|
SCTraceODPCompatibleStart(2); |
||||
|
NSValue *faceBoundsValue = [faceBoundsByFaceID objectForKey:faceID]; |
||||
|
if (faceBoundsValue) { |
||||
|
CGRect faceBounds = [faceBoundsValue CGRectValue]; |
||||
|
CGPoint point = CGPointMake(CGRectGetMidX(faceBounds), CGRectGetMidY(faceBounds)); |
||||
|
return point; |
||||
|
} else { |
||||
|
return CGPointMake(-1, -1); // An invalid point. |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
Setting a new focus/exposure point needs high CPU usage, so we only set a new POI when we have to. This method is to |
||||
|
return whether setting this new point if necessary. |
||||
|
If not, there is no need to change the POI. |
||||
|
*/ |
||||
|
- (BOOL)_shouldChangeToNewPoint:(CGPoint)newPoint |
||||
|
withNewFaceID:(NSNumber *)newFaceID |
||||
|
newFaceBounds:(CGRect)newFaceBounds |
||||
|
{ |
||||
|
SCTraceODPCompatibleStart(2); |
||||
|
BOOL shouldChange = NO; |
||||
|
if (!newFaceID || !self.targetingFaceID || |
||||
|
![newFaceID isEqualToNumber:self.targetingFaceID]) { // Return YES if it is a new face. |
||||
|
shouldChange = YES; |
||||
|
} else if (CGRectEqualToRect(self.targetingFaceBounds, CGRectZero) || |
||||
|
!CGRectContainsPoint(self.targetingFaceBounds, |
||||
|
newPoint)) { // Return YES if the new point if out of the current face bounds. |
||||
|
shouldChange = YES; |
||||
|
} else { |
||||
|
CGFloat currentBoundsArea = |
||||
|
CGRectGetWidth(self.targetingFaceBounds) * CGRectGetHeight(self.targetingFaceBounds); |
||||
|
CGFloat newBoundsArea = CGRectGetWidth(newFaceBounds) * CGRectGetHeight(newFaceBounds); |
||||
|
if (newBoundsArea >= currentBoundsArea * 1.2 || |
||||
|
newBoundsArea <= |
||||
|
currentBoundsArea * |
||||
|
0.8) { // Return YES if the area of new bounds if over 20% more or 20% less than the current one. |
||||
|
shouldChange = YES; |
||||
|
} |
||||
|
} |
||||
|
return shouldChange; |
||||
|
} |
||||
|
|
||||
|
- (NSNumber *)_getFaceIDOfFaceBoundsContainingPoint:(CGPoint)point |
||||
|
fromFaceBounds:(NSDictionary<NSNumber *, NSValue *> *)faceBoundsByFaceID |
||||
|
{ |
||||
|
SC_GUARD_ELSE_RETURN_VALUE(faceBoundsByFaceID.count > 0, nil); |
||||
|
__block NSNumber *faceID = nil; |
||||
|
[faceBoundsByFaceID |
||||
|
enumerateKeysAndObjectsUsingBlock:^(NSNumber *_Nonnull key, NSValue *_Nonnull obj, BOOL *_Nonnull stop) { |
||||
|
CGRect faceBounds = [obj CGRectValue]; |
||||
|
if (CGRectContainsPoint(faceBounds, point)) { |
||||
|
faceID = key; |
||||
|
*stop = YES; |
||||
|
} |
||||
|
}]; |
||||
|
return faceID; |
||||
|
} |
||||
|
|
||||
|
@end |
@ -0,0 +1,80 @@ |
|||||
|
// |
||||
|
// SCManagedCapturePreviewLayerController.h |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by Liu Liu on 5/5/15. |
||||
|
// Copyright (c) 2015 Snapchat, Inc. All rights reserved. |
||||
|
// |
||||
|
|
||||
|
#import <SCCameraFoundation/SCManagedVideoDataSource.h> |
||||
|
#import <SCFoundation/SCAssertWrapper.h> |
||||
|
#import <SCGhostToSnappable/SCGhostToSnappableSignal.h> |
||||
|
|
||||
|
#import <AVFoundation/AVFoundation.h> |
||||
|
#import <Foundation/Foundation.h> |
||||
|
#import <Metal/Metal.h> |
||||
|
#import <UIKit/UIKit.h> |
||||
|
|
||||
|
@protocol SCCapturer; |
||||
|
@class LSAGLView, SCBlackCameraDetector, SCManagedCapturePreviewLayerController; |
||||
|
|
||||
|
@protocol SCManagedCapturePreviewLayerControllerDelegate |
||||
|
|
||||
|
- (SCBlackCameraDetector *)blackCameraDetectorForManagedCapturePreviewLayerController: |
||||
|
(SCManagedCapturePreviewLayerController *)controller; |
||||
|
- (sc_create_g2s_ticket_f)g2sTicketForManagedCapturePreviewLayerController: |
||||
|
(SCManagedCapturePreviewLayerController *)controller; |
||||
|
|
||||
|
@end |
||||
|
|
||||
|
/** |
||||
|
* SCManagedCapturePreviewLayerController controls display of frame in a view. The controller has 3 |
||||
|
* different methods for this. |
||||
|
* AVCaptureVideoPreviewLayer: This is a feed coming straight from the camera and does not allow any |
||||
|
* image processing or modification of the frames displayed. |
||||
|
* LSAGLView: OpenGL based video for displaying video that is being processed (Lenses etc.) |
||||
|
* CAMetalLayer: Metal layer drawing textures on a vertex quad for display on screen. |
||||
|
*/ |
||||
|
@interface SCManagedCapturePreviewLayerController : NSObject <SCManagedSampleBufferDisplayController> |
||||
|
|
||||
|
@property (nonatomic, strong, readonly) UIView *view; |
||||
|
|
||||
|
@property (nonatomic, strong, readonly) AVCaptureVideoPreviewLayer *videoPreviewLayer; |
||||
|
|
||||
|
@property (nonatomic, strong, readonly) LSAGLView *videoPreviewGLView; |
||||
|
|
||||
|
@property (nonatomic, weak) id<SCManagedCapturePreviewLayerControllerDelegate> delegate; |
||||
|
|
||||
|
+ (instancetype)sharedInstance; |
||||
|
|
||||
|
- (void)pause; |
||||
|
|
||||
|
- (void)resume; |
||||
|
|
||||
|
- (UIView *)newStandInViewWithRect:(CGRect)rect; |
||||
|
|
||||
|
- (void)setManagedCapturer:(id<SCCapturer>)managedCapturer; |
||||
|
|
||||
|
// This method returns a token that you can hold on to. As long as the token is hold, |
||||
|
// an outdated view will be hold unless the app backgrounded. |
||||
|
- (NSString *)keepDisplayingOutdatedPreview; |
||||
|
|
||||
|
// End displaying the outdated frame with an issued keep token. If there is no one holds |
||||
|
// any token any more, this outdated view will be flushed. |
||||
|
- (void)endDisplayingOutdatedPreview:(NSString *)keepToken; |
||||
|
|
||||
|
// Create views for Metal, this method need to be called on the main thread. |
||||
|
- (void)setupPreviewLayer; |
||||
|
|
||||
|
// Create render pipeline state, setup shaders for Metal, this need to be called off the main thread. |
||||
|
- (void)setupRenderPipeline; |
||||
|
|
||||
|
- (void)applicationDidEnterBackground; |
||||
|
|
||||
|
- (void)applicationWillEnterForeground; |
||||
|
|
||||
|
- (void)applicationWillResignActive; |
||||
|
|
||||
|
- (void)applicationDidBecomeActive; |
||||
|
|
||||
|
@end |
@ -0,0 +1,563 @@ |
|||||
|
// |
||||
|
// SCManagedCapturePreviewLayerController.m |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by Liu Liu on 5/5/15. |
||||
|
// Copyright (c) 2015 Snapchat, Inc. All rights reserved. |
||||
|
// |
||||
|
|
||||
|
#import "SCManagedCapturePreviewLayerController.h" |
||||
|
|
||||
|
#import "SCBlackCameraDetector.h" |
||||
|
#import "SCCameraTweaks.h" |
||||
|
#import "SCManagedCapturePreviewView.h" |
||||
|
#import "SCManagedCapturer.h" |
||||
|
#import "SCManagedCapturerListener.h" |
||||
|
#import "SCManagedCapturerUtils.h" |
||||
|
#import "SCMetalUtils.h" |
||||
|
|
||||
|
#import <SCFoundation/NSData+Random.h> |
||||
|
#import <SCFoundation/SCCoreGraphicsUtils.h> |
||||
|
#import <SCFoundation/SCDeviceName.h> |
||||
|
#import <SCFoundation/SCLog.h> |
||||
|
#import <SCFoundation/SCQueuePerformer.h> |
||||
|
#import <SCFoundation/SCTrace.h> |
||||
|
#import <SCFoundation/SCTraceODPCompatible.h> |
||||
|
#import <SCFoundation/UIScreen+SCSafeAreaInsets.h> |
||||
|
#import <SCGhostToSnappable/SCGhostToSnappableSignal.h> |
||||
|
|
||||
|
#import <FBKVOController/FBKVOController.h> |
||||
|
|
||||
|
#define SCLogPreviewLayerInfo(fmt, ...) SCLogCoreCameraInfo(@"[PreviewLayerController] " fmt, ##__VA_ARGS__) |
||||
|
#define SCLogPreviewLayerWarning(fmt, ...) SCLogCoreCameraWarning(@"[PreviewLayerController] " fmt, ##__VA_ARGS__) |
||||
|
#define SCLogPreviewLayerError(fmt, ...) SCLogCoreCameraError(@"[PreviewLayerController] " fmt, ##__VA_ARGS__) |
||||
|
|
||||
|
const static CGSize kSCManagedCapturePreviewDefaultRenderSize = { |
||||
|
.width = 720, .height = 1280, |
||||
|
}; |
||||
|
|
||||
|
const static CGSize kSCManagedCapturePreviewRenderSize1080p = { |
||||
|
.width = 1080, .height = 1920, |
||||
|
}; |
||||
|
|
||||
|
#if !TARGET_IPHONE_SIMULATOR |
||||
|
|
||||
|
static NSInteger const kSCMetalCannotAcquireDrawableLimit = 2; |
||||
|
|
||||
|
@interface CAMetalLayer (SCSecretFature) |
||||
|
|
||||
|
// Call discardContents. |
||||
|
- (void)sc_secretFeature; |
||||
|
|
||||
|
@end |
||||
|
|
||||
|
@implementation CAMetalLayer (SCSecretFature) |
||||
|
|
||||
|
- (void)sc_secretFeature |
||||
|
{ |
||||
|
// "discardContents" |
||||
|
char buffer[] = {0x9b, 0x96, 0x8c, 0x9c, 0x9e, 0x8d, 0x9b, 0xbc, 0x90, 0x91, 0x8b, 0x9a, 0x91, 0x8b, 0x8c, 0}; |
||||
|
unsigned long len = strlen(buffer); |
||||
|
for (unsigned idx = 0; idx < len; ++idx) { |
||||
|
buffer[idx] = ~buffer[idx]; |
||||
|
} |
||||
|
SEL selector = NSSelectorFromString([NSString stringWithUTF8String:buffer]); |
||||
|
if ([self respondsToSelector:selector]) { |
||||
|
NSMethodSignature *signature = [self methodSignatureForSelector:selector]; |
||||
|
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; |
||||
|
[invocation setTarget:self]; |
||||
|
[invocation setSelector:selector]; |
||||
|
[invocation invoke]; |
||||
|
} |
||||
|
// For anyone curious, here is the actual implementation for discardContents in 10.3 (With Hopper v4, arm64) |
||||
|
// From glance, this seems pretty safe to call. |
||||
|
// void -[CAMetalLayer(CAMetalLayerPrivate) discardContents](int arg0) |
||||
|
// { |
||||
|
// *(r31 + 0xffffffffffffffe0) = r20; |
||||
|
// *(0xfffffffffffffff0 + r31) = r19; |
||||
|
// r31 = r31 + 0xffffffffffffffe0; |
||||
|
// *(r31 + 0x10) = r29; |
||||
|
// *(0x20 + r31) = r30; |
||||
|
// r29 = r31 + 0x10; |
||||
|
// r19 = *(arg0 + sign_extend_64(*(int32_t *)0x1a6300510)); |
||||
|
// if (r19 != 0x0) { |
||||
|
// r0 = loc_1807079dc(*0x1a7811fc8, r19); |
||||
|
// r0 = _CAImageQueueConsumeUnconsumed(*(r19 + 0x10)); |
||||
|
// r0 = _CAImageQueueFlush(*(r19 + 0x10)); |
||||
|
// r29 = *(r31 + 0x10); |
||||
|
// r30 = *(0x20 + r31); |
||||
|
// r20 = *r31; |
||||
|
// r19 = *(r31 + 0x10); |
||||
|
// r31 = r31 + 0x20; |
||||
|
// r0 = loc_1807079dc(*0x1a7811fc8, zero_extend_64(0x0)); |
||||
|
// } else { |
||||
|
// r29 = *(r31 + 0x10); |
||||
|
// r30 = *(0x20 + r31); |
||||
|
// r20 = *r31; |
||||
|
// r19 = *(r31 + 0x10); |
||||
|
// r31 = r31 + 0x20; |
||||
|
// } |
||||
|
// return; |
||||
|
// } |
||||
|
} |
||||
|
|
||||
|
@end |
||||
|
|
||||
|
#endif |
||||
|
|
||||
|
@interface SCManagedCapturePreviewLayerController () <SCManagedCapturerListener> |
||||
|
|
||||
|
@property (nonatomic) BOOL renderSuspended; |
||||
|
|
||||
|
@end |
||||
|
|
||||
|
@implementation SCManagedCapturePreviewLayerController { |
||||
|
SCManagedCapturePreviewView *_view; |
||||
|
CGSize _drawableSize; |
||||
|
SCQueuePerformer *_performer; |
||||
|
FBKVOController *_renderingKVO; |
||||
|
#if !TARGET_IPHONE_SIMULATOR |
||||
|
CAMetalLayer *_metalLayer; |
||||
|
id<MTLCommandQueue> _commandQueue; |
||||
|
id<MTLRenderPipelineState> _renderPipelineState; |
||||
|
CVMetalTextureCacheRef _textureCache; |
||||
|
dispatch_semaphore_t _commandBufferSemaphore; |
||||
|
// If the current view contains an outdated display (or any display) |
||||
|
BOOL _containOutdatedPreview; |
||||
|
// If we called empty outdated display already, but for some reason, hasn't emptied it yet. |
||||
|
BOOL _requireToFlushOutdatedPreview; |
||||
|
NSMutableSet *_tokenSet; |
||||
|
NSUInteger _cannotAcquireDrawable; |
||||
|
#endif |
||||
|
} |
||||
|
|
||||
|
+ (instancetype)sharedInstance |
||||
|
{ |
||||
|
static dispatch_once_t onceToken; |
||||
|
static SCManagedCapturePreviewLayerController *managedCapturePreviewLayerController; |
||||
|
dispatch_once(&onceToken, ^{ |
||||
|
managedCapturePreviewLayerController = [[SCManagedCapturePreviewLayerController alloc] init]; |
||||
|
}); |
||||
|
return managedCapturePreviewLayerController; |
||||
|
} |
||||
|
|
||||
|
- (instancetype)init |
||||
|
{ |
||||
|
self = [super init]; |
||||
|
if (self) { |
||||
|
#if !TARGET_IPHONE_SIMULATOR |
||||
|
// We only allow one renders at a time (Sorry, no double / triple buffering). |
||||
|
// It has to be created early here, otherwise integrity of other parts of the code is not |
||||
|
// guaranteed. |
||||
|
// TODO: I need to reason more about the initialization sequence. |
||||
|
_commandBufferSemaphore = dispatch_semaphore_create(1); |
||||
|
// Set _renderSuspended to be YES so that we won't render until it is fully setup. |
||||
|
_renderSuspended = YES; |
||||
|
_tokenSet = [NSMutableSet set]; |
||||
|
#endif |
||||
|
// If the screen is less than default size, we should fallback. |
||||
|
CGFloat nativeScale = [UIScreen mainScreen].nativeScale; |
||||
|
CGSize screenSize = [UIScreen mainScreen].fixedCoordinateSpace.bounds.size; |
||||
|
CGSize renderSize = [SCDeviceName isIphoneX] ? kSCManagedCapturePreviewRenderSize1080p |
||||
|
: kSCManagedCapturePreviewDefaultRenderSize; |
||||
|
if (screenSize.width * nativeScale < renderSize.width) { |
||||
|
_drawableSize = CGSizeMake(screenSize.width * nativeScale, screenSize.height * nativeScale); |
||||
|
} else { |
||||
|
_drawableSize = SCSizeIntegral( |
||||
|
SCSizeCropToAspectRatio(renderSize, SCSizeGetAspectRatio(SCManagedCapturerAllScreenSize()))); |
||||
|
} |
||||
|
_performer = [[SCQueuePerformer alloc] initWithLabel:"SCManagedCapturePreviewLayerController" |
||||
|
qualityOfService:QOS_CLASS_USER_INITIATED |
||||
|
queueType:DISPATCH_QUEUE_SERIAL |
||||
|
context:SCQueuePerformerContextCoreCamera]; |
||||
|
|
||||
|
_renderingKVO = [[FBKVOController alloc] initWithObserver:self]; |
||||
|
[_renderingKVO observe:self |
||||
|
keyPath:@keypath(self, renderSuspended) |
||||
|
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld |
||||
|
block:^(id observer, id object, NSDictionary *change) { |
||||
|
BOOL oldValue = [change[NSKeyValueChangeOldKey] boolValue]; |
||||
|
BOOL newValue = [change[NSKeyValueChangeNewKey] boolValue]; |
||||
|
if (oldValue != newValue) { |
||||
|
[[_delegate blackCameraDetectorForManagedCapturePreviewLayerController:self] |
||||
|
capturePreviewDidBecomeVisible:!newValue]; |
||||
|
} |
||||
|
}]; |
||||
|
} |
||||
|
return self; |
||||
|
} |
||||
|
|
||||
|
- (void)pause |
||||
|
{ |
||||
|
#if !TARGET_IPHONE_SIMULATOR |
||||
|
SCTraceStart(); |
||||
|
SCLogPreviewLayerInfo(@"pause Metal rendering performer waiting"); |
||||
|
[_performer performAndWait:^() { |
||||
|
self.renderSuspended = YES; |
||||
|
}]; |
||||
|
SCLogPreviewLayerInfo(@"pause Metal rendering performer finished"); |
||||
|
#endif |
||||
|
} |
||||
|
|
||||
|
- (void)resume |
||||
|
{ |
||||
|
#if !TARGET_IPHONE_SIMULATOR |
||||
|
SCTraceStart(); |
||||
|
SCLogPreviewLayerInfo(@"resume Metal rendering performer waiting"); |
||||
|
[_performer performAndWait:^() { |
||||
|
self.renderSuspended = NO; |
||||
|
}]; |
||||
|
SCLogPreviewLayerInfo(@"resume Metal rendering performer finished"); |
||||
|
#endif |
||||
|
} |
||||
|
|
||||
|
- (void)setupPreviewLayer |
||||
|
{ |
||||
|
#if !TARGET_IPHONE_SIMULATOR |
||||
|
SCTraceStart(); |
||||
|
SCAssertMainThread(); |
||||
|
SC_GUARD_ELSE_RETURN(SCDeviceSupportsMetal()); |
||||
|
|
||||
|
if (!_metalLayer) { |
||||
|
_metalLayer = [CAMetalLayer new]; |
||||
|
SCLogPreviewLayerInfo(@"setup metalLayer:%@", _metalLayer); |
||||
|
|
||||
|
if (!_view) { |
||||
|
// Create capture preview view and setup the metal layer |
||||
|
[self view]; |
||||
|
} else { |
||||
|
[_view setupMetalLayer:_metalLayer]; |
||||
|
} |
||||
|
} |
||||
|
#endif |
||||
|
} |
||||
|
|
||||
|
- (UIView *)newStandInViewWithRect:(CGRect)rect |
||||
|
{ |
||||
|
return [self.view resizableSnapshotViewFromRect:rect afterScreenUpdates:YES withCapInsets:UIEdgeInsetsZero]; |
||||
|
} |
||||
|
|
||||
|
- (void)setupRenderPipeline |
||||
|
{ |
||||
|
#if !TARGET_IPHONE_SIMULATOR |
||||
|
SCTraceStart(); |
||||
|
SC_GUARD_ELSE_RETURN(SCDeviceSupportsMetal()); |
||||
|
SCAssertNotMainThread(); |
||||
|
id<MTLDevice> device = SCGetManagedCaptureMetalDevice(); |
||||
|
id<MTLLibrary> shaderLibrary = [device newDefaultLibrary]; |
||||
|
_commandQueue = [device newCommandQueue]; |
||||
|
MTLRenderPipelineDescriptor *renderPipelineDescriptor = [MTLRenderPipelineDescriptor new]; |
||||
|
renderPipelineDescriptor.colorAttachments[0].pixelFormat = MTLPixelFormatBGRA8Unorm; |
||||
|
renderPipelineDescriptor.vertexFunction = [shaderLibrary newFunctionWithName:@"yuv_vertex_reshape"]; |
||||
|
renderPipelineDescriptor.fragmentFunction = [shaderLibrary newFunctionWithName:@"yuv_fragment_texture"]; |
||||
|
MTLVertexDescriptor *vertexDescriptor = [MTLVertexDescriptor vertexDescriptor]; |
||||
|
vertexDescriptor.attributes[0].format = MTLVertexFormatFloat2; // position |
||||
|
vertexDescriptor.attributes[0].offset = 0; |
||||
|
vertexDescriptor.attributes[0].bufferIndex = 0; |
||||
|
vertexDescriptor.attributes[1].format = MTLVertexFormatFloat2; // texCoords |
||||
|
vertexDescriptor.attributes[1].offset = 2 * sizeof(float); |
||||
|
vertexDescriptor.attributes[1].bufferIndex = 0; |
||||
|
vertexDescriptor.layouts[0].stepRate = 1; |
||||
|
vertexDescriptor.layouts[0].stepFunction = MTLVertexStepFunctionPerVertex; |
||||
|
vertexDescriptor.layouts[0].stride = 4 * sizeof(float); |
||||
|
renderPipelineDescriptor.vertexDescriptor = vertexDescriptor; |
||||
|
_renderPipelineState = [device newRenderPipelineStateWithDescriptor:renderPipelineDescriptor error:nil]; |
||||
|
CVMetalTextureCacheCreate(kCFAllocatorDefault, nil, device, nil, &_textureCache); |
||||
|
_metalLayer.device = device; |
||||
|
_metalLayer.drawableSize = _drawableSize; |
||||
|
_metalLayer.pixelFormat = MTLPixelFormatBGRA8Unorm; |
||||
|
_metalLayer.framebufferOnly = YES; // It is default to Yes. |
||||
|
[_performer performAndWait:^() { |
||||
|
self.renderSuspended = NO; |
||||
|
}]; |
||||
|
SCLogPreviewLayerInfo(@"did setup render pipeline"); |
||||
|
#endif |
||||
|
} |
||||
|
|
||||
|
- (UIView *)view |
||||
|
{ |
||||
|
SCTraceStart(); |
||||
|
SCAssertMainThread(); |
||||
|
if (!_view) { |
||||
|
#if TARGET_IPHONE_SIMULATOR |
||||
|
_view = [[SCManagedCapturePreviewView alloc] initWithFrame:[UIScreen mainScreen].fixedCoordinateSpace.bounds |
||||
|
aspectRatio:SCSizeGetAspectRatio(_drawableSize) |
||||
|
metalLayer:nil]; |
||||
|
#else |
||||
|
_view = [[SCManagedCapturePreviewView alloc] initWithFrame:[UIScreen mainScreen].fixedCoordinateSpace.bounds |
||||
|
aspectRatio:SCSizeGetAspectRatio(_drawableSize) |
||||
|
metalLayer:_metalLayer]; |
||||
|
SCLogPreviewLayerInfo(@"created SCManagedCapturePreviewView:%@", _view); |
||||
|
#endif |
||||
|
} |
||||
|
return _view; |
||||
|
} |
||||
|
|
||||
|
- (void)setManagedCapturer:(id<SCCapturer>)managedCapturer |
||||
|
{ |
||||
|
SCTraceStart(); |
||||
|
SCLogPreviewLayerInfo(@"setManagedCapturer:%@", managedCapturer); |
||||
|
if (SCDeviceSupportsMetal()) { |
||||
|
[managedCapturer addSampleBufferDisplayController:self context:SCCapturerContext]; |
||||
|
} |
||||
|
[managedCapturer addListener:self]; |
||||
|
} |
||||
|
|
||||
|
- (void)applicationDidEnterBackground |
||||
|
{ |
||||
|
#if !TARGET_IPHONE_SIMULATOR |
||||
|
SCTraceStart(); |
||||
|
SCAssertMainThread(); |
||||
|
SC_GUARD_ELSE_RETURN(SCDeviceSupportsMetal()); |
||||
|
SCLogPreviewLayerInfo(@"applicationDidEnterBackground waiting for performer"); |
||||
|
[_performer performAndWait:^() { |
||||
|
CVMetalTextureCacheFlush(_textureCache, 0); |
||||
|
[_tokenSet removeAllObjects]; |
||||
|
self.renderSuspended = YES; |
||||
|
}]; |
||||
|
SCLogPreviewLayerInfo(@"applicationDidEnterBackground signal performer finishes"); |
||||
|
#endif |
||||
|
} |
||||
|
|
||||
|
- (void)applicationWillResignActive |
||||
|
{ |
||||
|
SC_GUARD_ELSE_RETURN(SCDeviceSupportsMetal()); |
||||
|
SCTraceStart(); |
||||
|
SCAssertMainThread(); |
||||
|
#if !TARGET_IPHONE_SIMULATOR |
||||
|
SCLogPreviewLayerInfo(@"pause Metal rendering"); |
||||
|
[_performer performAndWait:^() { |
||||
|
self.renderSuspended = YES; |
||||
|
}]; |
||||
|
#endif |
||||
|
} |
||||
|
|
||||
|
- (void)applicationDidBecomeActive |
||||
|
{ |
||||
|
SC_GUARD_ELSE_RETURN(SCDeviceSupportsMetal()); |
||||
|
SCTraceStart(); |
||||
|
SCAssertMainThread(); |
||||
|
#if !TARGET_IPHONE_SIMULATOR |
||||
|
SCLogPreviewLayerInfo(@"resume Metal rendering waiting for performer"); |
||||
|
[_performer performAndWait:^() { |
||||
|
self.renderSuspended = NO; |
||||
|
}]; |
||||
|
SCLogPreviewLayerInfo(@"resume Metal rendering performer finished"); |
||||
|
#endif |
||||
|
} |
||||
|
|
||||
|
- (void)applicationWillEnterForeground |
||||
|
{ |
||||
|
#if !TARGET_IPHONE_SIMULATOR |
||||
|
SCTraceStart(); |
||||
|
SCAssertMainThread(); |
||||
|
SC_GUARD_ELSE_RETURN(SCDeviceSupportsMetal()); |
||||
|
SCLogPreviewLayerInfo(@"applicationWillEnterForeground waiting for performer"); |
||||
|
[_performer performAndWait:^() { |
||||
|
self.renderSuspended = NO; |
||||
|
if (_containOutdatedPreview && _tokenSet.count == 0) { |
||||
|
[self _flushOutdatedPreview]; |
||||
|
} |
||||
|
}]; |
||||
|
SCLogPreviewLayerInfo(@"applicationWillEnterForeground performer finished"); |
||||
|
#endif |
||||
|
} |
||||
|
|
||||
|
- (NSString *)keepDisplayingOutdatedPreview |
||||
|
{ |
||||
|
SCTraceStart(); |
||||
|
NSString *token = [NSData randomBase64EncodedStringOfLength:8]; |
||||
|
#if !TARGET_IPHONE_SIMULATOR |
||||
|
SCLogPreviewLayerInfo(@"keepDisplayingOutdatedPreview waiting for performer"); |
||||
|
[_performer performAndWait:^() { |
||||
|
[_tokenSet addObject:token]; |
||||
|
}]; |
||||
|
SCLogPreviewLayerInfo(@"keepDisplayingOutdatedPreview performer finished"); |
||||
|
#endif |
||||
|
return token; |
||||
|
} |
||||
|
|
||||
|
- (void)endDisplayingOutdatedPreview:(NSString *)keepToken |
||||
|
{ |
||||
|
#if !TARGET_IPHONE_SIMULATOR |
||||
|
SC_GUARD_ELSE_RETURN(SCDeviceSupportsMetal()); |
||||
|
// I simply use a lock for this. If it becomes a bottleneck, I can figure something else out. |
||||
|
SCTraceStart(); |
||||
|
SCLogPreviewLayerInfo(@"endDisplayingOutdatedPreview waiting for performer"); |
||||
|
[_performer performAndWait:^() { |
||||
|
[_tokenSet removeObject:keepToken]; |
||||
|
if (_tokenSet.count == 0 && _requireToFlushOutdatedPreview && _containOutdatedPreview && !_renderSuspended) { |
||||
|
[self _flushOutdatedPreview]; |
||||
|
} |
||||
|
}]; |
||||
|
SCLogPreviewLayerInfo(@"endDisplayingOutdatedPreview performer finished"); |
||||
|
#endif |
||||
|
} |
||||
|
|
||||
|
#pragma mark - SCManagedSampleBufferDisplayController |
||||
|
|
||||
|
- (void)enqueueSampleBuffer:(CMSampleBufferRef)sampleBuffer |
||||
|
{ |
||||
|
#if !TARGET_IPHONE_SIMULATOR |
||||
|
// Just drop the frame if it is rendering. |
||||
|
SC_GUARD_ELSE_RUN_AND_RETURN_VALUE(dispatch_semaphore_wait(_commandBufferSemaphore, DISPATCH_TIME_NOW) == 0, |
||||
|
SCLogPreviewLayerInfo(@"waiting for commandBufferSemaphore signaled"), ); |
||||
|
// Just drop the frame, simple. |
||||
|
[_performer performAndWait:^() { |
||||
|
if (_renderSuspended) { |
||||
|
SCLogGeneralInfo(@"Preview rendering suspends and current sample buffer is dropped"); |
||||
|
dispatch_semaphore_signal(_commandBufferSemaphore); |
||||
|
return; |
||||
|
} |
||||
|
@autoreleasepool { |
||||
|
const BOOL isFirstPreviewFrame = !_containOutdatedPreview; |
||||
|
if (isFirstPreviewFrame) { |
||||
|
// Signal that we receieved the first frame (otherwise this will be YES already). |
||||
|
SCGhostToSnappableSignalDidReceiveFirstPreviewFrame(); |
||||
|
sc_create_g2s_ticket_f func = [_delegate g2sTicketForManagedCapturePreviewLayerController:self]; |
||||
|
SCG2SActivateManiphestTicketQueueWithTicketCreationFunction(func); |
||||
|
} |
||||
|
CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer); |
||||
|
|
||||
|
CVPixelBufferLockBaseAddress(imageBuffer, kCVPixelBufferLock_ReadOnly); |
||||
|
size_t pixelWidth = CVPixelBufferGetWidth(imageBuffer); |
||||
|
size_t pixelHeight = CVPixelBufferGetHeight(imageBuffer); |
||||
|
id<MTLTexture> yTexture = |
||||
|
SCMetalTextureFromPixelBuffer(imageBuffer, 0, MTLPixelFormatR8Unorm, _textureCache); |
||||
|
id<MTLTexture> cbCrTexture = |
||||
|
SCMetalTextureFromPixelBuffer(imageBuffer, 1, MTLPixelFormatRG8Unorm, _textureCache); |
||||
|
CVPixelBufferUnlockBaseAddress(imageBuffer, kCVPixelBufferLock_ReadOnly); |
||||
|
|
||||
|
SC_GUARD_ELSE_RUN_AND_RETURN(yTexture && cbCrTexture, dispatch_semaphore_signal(_commandBufferSemaphore)); |
||||
|
id<MTLCommandBuffer> commandBuffer = _commandQueue.commandBuffer; |
||||
|
id<CAMetalDrawable> drawable = _metalLayer.nextDrawable; |
||||
|
if (!drawable) { |
||||
|
// Count how many times I cannot acquire drawable. |
||||
|
++_cannotAcquireDrawable; |
||||
|
if (_cannotAcquireDrawable >= kSCMetalCannotAcquireDrawableLimit) { |
||||
|
// Calling [_metalLayer discardContents] to flush the CAImageQueue |
||||
|
SCLogGeneralInfo(@"Cannot acquire drawable, reboot Metal .."); |
||||
|
[_metalLayer sc_secretFeature]; |
||||
|
} |
||||
|
dispatch_semaphore_signal(_commandBufferSemaphore); |
||||
|
return; |
||||
|
} |
||||
|
_cannotAcquireDrawable = 0; // Reset to 0 in case we can acquire drawable. |
||||
|
MTLRenderPassDescriptor *renderPassDescriptor = [MTLRenderPassDescriptor new]; |
||||
|
renderPassDescriptor.colorAttachments[0].texture = drawable.texture; |
||||
|
id<MTLRenderCommandEncoder> renderEncoder = |
||||
|
[commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor]; |
||||
|
[renderEncoder setRenderPipelineState:_renderPipelineState]; |
||||
|
[renderEncoder setFragmentTexture:yTexture atIndex:0]; |
||||
|
[renderEncoder setFragmentTexture:cbCrTexture atIndex:1]; |
||||
|
// TODO: Prob this out of the image buffer. |
||||
|
// 90 clock-wise rotated texture coordinate. |
||||
|
// Also do aspect fill. |
||||
|
float normalizedHeight, normalizedWidth; |
||||
|
if (pixelWidth * _drawableSize.width > _drawableSize.height * pixelHeight) { |
||||
|
normalizedHeight = 1.0; |
||||
|
normalizedWidth = pixelWidth * (_drawableSize.width / pixelHeight) / _drawableSize.height; |
||||
|
} else { |
||||
|
normalizedHeight = pixelHeight * (_drawableSize.height / pixelWidth) / _drawableSize.width; |
||||
|
normalizedWidth = 1.0; |
||||
|
} |
||||
|
const float vertices[] = { |
||||
|
-normalizedHeight, -normalizedWidth, 1, 1, // lower left -> upper right |
||||
|
normalizedHeight, -normalizedWidth, 1, 0, // lower right -> lower right |
||||
|
-normalizedHeight, normalizedWidth, 0, 1, // upper left -> upper left |
||||
|
normalizedHeight, normalizedWidth, 0, 0, // upper right -> lower left |
||||
|
}; |
||||
|
[renderEncoder setVertexBytes:vertices length:sizeof(vertices) atIndex:0]; |
||||
|
[renderEncoder drawPrimitives:MTLPrimitiveTypeTriangleStrip vertexStart:0 vertexCount:4]; |
||||
|
[renderEncoder endEncoding]; |
||||
|
// I need to set a minimum duration for the drawable. |
||||
|
// There is a bug on iOS 10.3, if I present as soon as I can, I am keeping the GPU |
||||
|
// at 30fps even you swipe between views, that causes undesirable visual jarring. |
||||
|
// By set a minimum duration, even it is incrediably small (I tried 10ms, and here 60fps works), |
||||
|
// the OS seems can adjust the frame rate much better when swiping. |
||||
|
// This is an iOS 10.3 new method. |
||||
|
if ([commandBuffer respondsToSelector:@selector(presentDrawable:afterMinimumDuration:)]) { |
||||
|
[(id)commandBuffer presentDrawable:drawable afterMinimumDuration:(1.0 / 60)]; |
||||
|
} else { |
||||
|
[commandBuffer presentDrawable:drawable]; |
||||
|
} |
||||
|
[commandBuffer addCompletedHandler:^(id<MTLCommandBuffer> commandBuffer) { |
||||
|
dispatch_semaphore_signal(_commandBufferSemaphore); |
||||
|
}]; |
||||
|
if (isFirstPreviewFrame) { |
||||
|
if ([drawable respondsToSelector:@selector(addPresentedHandler:)] && |
||||
|
[drawable respondsToSelector:@selector(presentedTime)]) { |
||||
|
[(id)drawable addPresentedHandler:^(id<MTLDrawable> presentedDrawable) { |
||||
|
SCGhostToSnappableSignalDidRenderFirstPreviewFrame([(id)presentedDrawable presentedTime]); |
||||
|
}]; |
||||
|
} else { |
||||
|
[commandBuffer addCompletedHandler:^(id<MTLCommandBuffer> commandBuffer) { |
||||
|
// Using CACurrentMediaTime to approximate. |
||||
|
SCGhostToSnappableSignalDidRenderFirstPreviewFrame(CACurrentMediaTime()); |
||||
|
}]; |
||||
|
} |
||||
|
} |
||||
|
// We enqueued an sample buffer to display, therefore, it contains an outdated display (to be clean up). |
||||
|
_containOutdatedPreview = YES; |
||||
|
[commandBuffer commit]; |
||||
|
} |
||||
|
}]; |
||||
|
#endif |
||||
|
} |
||||
|
|
||||
|
- (void)flushOutdatedPreview |
||||
|
{ |
||||
|
SCTraceStart(); |
||||
|
#if !TARGET_IPHONE_SIMULATOR |
||||
|
// This method cannot drop frames (otherwise we will have residual on the screen). |
||||
|
SCLogPreviewLayerInfo(@"flushOutdatedPreview waiting for performer"); |
||||
|
[_performer performAndWait:^() { |
||||
|
_requireToFlushOutdatedPreview = YES; |
||||
|
SC_GUARD_ELSE_RETURN(!_renderSuspended); |
||||
|
// Have to make sure we have no token left before return. |
||||
|
SC_GUARD_ELSE_RETURN(_tokenSet.count == 0); |
||||
|
[self _flushOutdatedPreview]; |
||||
|
}]; |
||||
|
SCLogPreviewLayerInfo(@"flushOutdatedPreview performer finished"); |
||||
|
#endif |
||||
|
} |
||||
|
|
||||
|
- (void)_flushOutdatedPreview |
||||
|
{ |
||||
|
SCTraceStart(); |
||||
|
SCAssertPerformer(_performer); |
||||
|
#if !TARGET_IPHONE_SIMULATOR |
||||
|
SCLogPreviewLayerInfo(@"flushOutdatedPreview containOutdatedPreview:%d", _containOutdatedPreview); |
||||
|
// I don't care if this has renderSuspended or not, assuming I did the right thing. |
||||
|
// Emptied, no need to do this any more on foregrounding. |
||||
|
SC_GUARD_ELSE_RETURN(_containOutdatedPreview); |
||||
|
_containOutdatedPreview = NO; |
||||
|
_requireToFlushOutdatedPreview = NO; |
||||
|
[_metalLayer sc_secretFeature]; |
||||
|
#endif |
||||
|
} |
||||
|
|
||||
|
#pragma mark - SCManagedCapturerListener |
||||
|
|
||||
|
- (void)managedCapturer:(id<SCCapturer>)managedCapturer |
||||
|
didChangeVideoPreviewLayer:(AVCaptureVideoPreviewLayer *)videoPreviewLayer |
||||
|
{ |
||||
|
SCTraceStart(); |
||||
|
SCAssertMainThread(); |
||||
|
// Force to load the view |
||||
|
[self view]; |
||||
|
_view.videoPreviewLayer = videoPreviewLayer; |
||||
|
SCLogPreviewLayerInfo(@"didChangeVideoPreviewLayer:%@", videoPreviewLayer); |
||||
|
} |
||||
|
|
||||
|
- (void)managedCapturer:(id<SCCapturer>)managedCapturer didChangeVideoPreviewGLView:(LSAGLView *)videoPreviewGLView |
||||
|
{ |
||||
|
SCTraceStart(); |
||||
|
SCAssertMainThread(); |
||||
|
// Force to load the view |
||||
|
[self view]; |
||||
|
_view.videoPreviewGLView = videoPreviewGLView; |
||||
|
SCLogPreviewLayerInfo(@"didChangeVideoPreviewGLView:%@", videoPreviewGLView); |
||||
|
} |
||||
|
|
||||
|
@end |
@ -0,0 +1,25 @@ |
|||||
|
// |
||||
|
// SCManagedCapturePreviewView.h |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by Liu Liu on 5/5/15. |
||||
|
// Copyright (c) 2015 Snapchat, Inc. All rights reserved. |
||||
|
// |
||||
|
|
||||
|
#import <AVFoundation/AVFoundation.h> |
||||
|
#import <UIKit/UIKit.h> |
||||
|
|
||||
|
@class LSAGLView; |
||||
|
|
||||
|
@interface SCManagedCapturePreviewView : UIView |
||||
|
|
||||
|
- (instancetype)initWithFrame:(CGRect)frame NS_UNAVAILABLE; |
||||
|
|
||||
|
- (instancetype)initWithFrame:(CGRect)frame aspectRatio:(CGFloat)aspectRatio metalLayer:(CALayer *)metalLayer; |
||||
|
// This method is called only once in case the metalLayer is nil previously. |
||||
|
- (void)setupMetalLayer:(CALayer *)metalLayer; |
||||
|
|
||||
|
@property (nonatomic, strong) AVCaptureVideoPreviewLayer *videoPreviewLayer; |
||||
|
@property (nonatomic, strong) LSAGLView *videoPreviewGLView; |
||||
|
|
||||
|
@end |
@ -0,0 +1,173 @@ |
|||||
|
// |
||||
|
// SCManagedCapturePreviewView.m |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by Liu Liu on 5/5/15. |
||||
|
// Copyright (c) 2015 Snapchat, Inc. All rights reserved. |
||||
|
// |
||||
|
|
||||
|
#import "SCManagedCapturePreviewView.h" |
||||
|
|
||||
|
#import "SCCameraTweaks.h" |
||||
|
#import "SCManagedCapturePreviewLayerController.h" |
||||
|
#import "SCManagedCapturePreviewViewDebugView.h" |
||||
|
#import "SCMetalUtils.h" |
||||
|
|
||||
|
#import <SCFoundation/SCCoreGraphicsUtils.h> |
||||
|
#import <SCFoundation/SCLog.h> |
||||
|
#import <SCFoundation/SCTrace.h> |
||||
|
|
||||
|
#import <Looksery/LSAGLView.h> |
||||
|
|
||||
|
@implementation SCManagedCapturePreviewView { |
||||
|
CGFloat _aspectRatio; |
||||
|
CALayer *_containerLayer; |
||||
|
CALayer *_metalLayer; |
||||
|
SCManagedCapturePreviewViewDebugView *_debugView; |
||||
|
} |
||||
|
|
||||
|
- (instancetype)initWithFrame:(CGRect)frame aspectRatio:(CGFloat)aspectRatio metalLayer:(CALayer *)metalLayer |
||||
|
{ |
||||
|
SCTraceStart(); |
||||
|
SCAssertMainThread(); |
||||
|
self = [super initWithFrame:frame]; |
||||
|
if (self) { |
||||
|
_aspectRatio = aspectRatio; |
||||
|
if (SCDeviceSupportsMetal()) { |
||||
|
[CATransaction begin]; |
||||
|
[CATransaction setDisableActions:YES]; |
||||
|
_metalLayer = metalLayer; |
||||
|
_metalLayer.frame = [self _layerFrame]; |
||||
|
[self.layer insertSublayer:_metalLayer below:[self.layer sublayers][0]]; |
||||
|
[CATransaction commit]; |
||||
|
} else { |
||||
|
_containerLayer = [[CALayer alloc] init]; |
||||
|
_containerLayer.frame = [self _layerFrame]; |
||||
|
// Using a container layer such that the software zooming is happening on this layer |
||||
|
[self.layer insertSublayer:_containerLayer below:[self.layer sublayers][0]]; |
||||
|
} |
||||
|
if ([self _shouldShowDebugView]) { |
||||
|
_debugView = [[SCManagedCapturePreviewViewDebugView alloc] init]; |
||||
|
[self addSubview:_debugView]; |
||||
|
} |
||||
|
} |
||||
|
return self; |
||||
|
} |
||||
|
|
||||
|
- (void)_layoutVideoPreviewLayer |
||||
|
{ |
||||
|
SCAssertMainThread(); |
||||
|
[CATransaction begin]; |
||||
|
[CATransaction setDisableActions:YES]; |
||||
|
if (SCDeviceSupportsMetal()) { |
||||
|
_metalLayer.frame = [self _layerFrame]; |
||||
|
} else { |
||||
|
if (_videoPreviewLayer) { |
||||
|
SCLogGeneralInfo(@"container layer frame %@, video preview layer frame %@", |
||||
|
NSStringFromCGRect(_containerLayer.frame), NSStringFromCGRect(_videoPreviewLayer.frame)); |
||||
|
} |
||||
|
// Using bounds because we don't really care about the position at this point. |
||||
|
_containerLayer.frame = [self _layerFrame]; |
||||
|
_videoPreviewLayer.frame = _containerLayer.bounds; |
||||
|
_videoPreviewLayer.position = |
||||
|
CGPointMake(CGRectGetWidth(_containerLayer.bounds) * 0.5, CGRectGetHeight(_containerLayer.bounds) * 0.5); |
||||
|
} |
||||
|
[CATransaction commit]; |
||||
|
} |
||||
|
|
||||
|
- (void)_layoutVideoPreviewGLView |
||||
|
{ |
||||
|
SCCAssertMainThread(); |
||||
|
_videoPreviewGLView.frame = [self _layerFrame]; |
||||
|
} |
||||
|
|
||||
|
- (CGRect)_layerFrame |
||||
|
{ |
||||
|
CGRect frame = SCRectMakeWithCenterAndSize( |
||||
|
SCRectGetMid(self.bounds), SCSizeIntegral(SCSizeExpandToAspectRatio(self.bounds.size, _aspectRatio))); |
||||
|
|
||||
|
CGFloat x = frame.origin.x; |
||||
|
x = isnan(x) ? 0.0 : (isfinite(x) ? x : INFINITY); |
||||
|
|
||||
|
CGFloat y = frame.origin.y; |
||||
|
y = isnan(y) ? 0.0 : (isfinite(y) ? y : INFINITY); |
||||
|
|
||||
|
CGFloat width = frame.size.width; |
||||
|
width = isnan(width) ? 0.0 : (isfinite(width) ? width : INFINITY); |
||||
|
|
||||
|
CGFloat height = frame.size.height; |
||||
|
height = isnan(height) ? 0.0 : (isfinite(height) ? height : INFINITY); |
||||
|
|
||||
|
return CGRectMake(x, y, width, height); |
||||
|
} |
||||
|
|
||||
|
- (void)setVideoPreviewLayer:(AVCaptureVideoPreviewLayer *)videoPreviewLayer |
||||
|
{ |
||||
|
SCAssertMainThread(); |
||||
|
if (_videoPreviewLayer != videoPreviewLayer) { |
||||
|
[_videoPreviewLayer removeFromSuperlayer]; |
||||
|
_videoPreviewLayer = videoPreviewLayer; |
||||
|
[_containerLayer addSublayer:_videoPreviewLayer]; |
||||
|
[self _layoutVideoPreviewLayer]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
- (void)setupMetalLayer:(CALayer *)metalLayer |
||||
|
{ |
||||
|
SCAssert(!_metalLayer, @"_metalLayer should be nil."); |
||||
|
SCAssert(metalLayer, @"metalLayer must exists."); |
||||
|
SCAssertMainThread(); |
||||
|
_metalLayer = metalLayer; |
||||
|
[self.layer insertSublayer:_metalLayer below:[self.layer sublayers][0]]; |
||||
|
[self _layoutVideoPreviewLayer]; |
||||
|
} |
||||
|
|
||||
|
- (void)setVideoPreviewGLView:(LSAGLView *)videoPreviewGLView |
||||
|
{ |
||||
|
SCAssertMainThread(); |
||||
|
if (_videoPreviewGLView != videoPreviewGLView) { |
||||
|
[_videoPreviewGLView removeFromSuperview]; |
||||
|
_videoPreviewGLView = videoPreviewGLView; |
||||
|
[self addSubview:_videoPreviewGLView]; |
||||
|
[self _layoutVideoPreviewGLView]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
#pragma mark - Overridden methods |
||||
|
|
||||
|
- (void)layoutSubviews |
||||
|
{ |
||||
|
SCAssertMainThread(); |
||||
|
[super layoutSubviews]; |
||||
|
[self _layoutVideoPreviewLayer]; |
||||
|
[self _layoutVideoPreviewGLView]; |
||||
|
[self _layoutDebugViewIfNeeded]; |
||||
|
} |
||||
|
|
||||
|
- (void)setHidden:(BOOL)hidden |
||||
|
{ |
||||
|
SCAssertMainThread(); |
||||
|
[super setHidden:hidden]; |
||||
|
if (hidden) { |
||||
|
SCLogGeneralInfo(@"[SCManagedCapturePreviewView] - isHidden is being set to YES"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
#pragma mark - Debug View |
||||
|
|
||||
|
- (BOOL)_shouldShowDebugView |
||||
|
{ |
||||
|
// Only show debug view in internal builds and tweak settings are turned on. |
||||
|
return SCIsInternalBuild() && |
||||
|
(SCCameraTweaksEnableFocusPointObservation() || SCCameraTweaksEnableExposurePointObservation()); |
||||
|
} |
||||
|
|
||||
|
- (void)_layoutDebugViewIfNeeded |
||||
|
{ |
||||
|
SCAssertMainThread(); |
||||
|
SC_GUARD_ELSE_RETURN([self _shouldShowDebugView]); |
||||
|
_debugView.frame = self.bounds; |
||||
|
[self bringSubviewToFront:_debugView]; |
||||
|
} |
||||
|
|
||||
|
@end |
@ -0,0 +1,14 @@ |
|||||
|
// |
||||
|
// SCManagedCapturePreviewViewDebugView.h |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by Jiyang Zhu on 1/19/18. |
||||
|
// Copyright © 2018 Snapchat, Inc. All rights reserved. |
||||
|
// |
||||
|
|
||||
|
#import <AVFoundation/AVFoundation.h> |
||||
|
#import <UIKit/UIKit.h> |
||||
|
|
||||
|
@interface SCManagedCapturePreviewViewDebugView : UIView |
||||
|
|
||||
|
@end |
@ -0,0 +1,204 @@ |
|||||
|
// |
||||
|
// SCManagedCapturePreviewViewDebugView.m |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by Jiyang Zhu on 1/19/18. |
||||
|
// Copyright © 2018 Snapchat, Inc. All rights reserved. |
||||
|
// |
||||
|
|
||||
|
#import "SCManagedCapturePreviewViewDebugView.h" |
||||
|
|
||||
|
#import "SCManagedCapturer.h" |
||||
|
#import "SCManagedCapturerListener.h" |
||||
|
|
||||
|
#import <SCFoundation/SCAssertWrapper.h> |
||||
|
#import <SCFoundation/SCThreadHelpers.h> |
||||
|
#import <SCFoundation/UIFont+AvenirNext.h> |
||||
|
|
||||
|
@import CoreText; |
||||
|
|
||||
|
static CGFloat const kSCManagedCapturePreviewViewDebugViewCrossHairLineWidth = 1.0; |
||||
|
static CGFloat const kSCManagedCapturePreviewViewDebugViewCrossHairWidth = 20.0; |
||||
|
|
||||
|
@interface SCManagedCapturePreviewViewDebugView () <SCManagedCapturerListener> |
||||
|
|
||||
|
@property (assign, nonatomic) CGPoint focusPoint; |
||||
|
@property (assign, nonatomic) CGPoint exposurePoint; |
||||
|
@property (strong, nonatomic) NSDictionary<NSNumber *, NSValue *> *faceBoundsByFaceID; |
||||
|
|
||||
|
@end |
||||
|
|
||||
|
@implementation SCManagedCapturePreviewViewDebugView |
||||
|
|
||||
|
- (instancetype)initWithFrame:(CGRect)frame |
||||
|
{ |
||||
|
self = [super initWithFrame:frame]; |
||||
|
if (self) { |
||||
|
self.userInteractionEnabled = NO; |
||||
|
self.backgroundColor = [UIColor clearColor]; |
||||
|
_focusPoint = [self _convertPointOfInterest:CGPointMake(0.5, 0.5)]; |
||||
|
_exposurePoint = [self _convertPointOfInterest:CGPointMake(0.5, 0.5)]; |
||||
|
[[SCManagedCapturer sharedInstance] addListener:self]; |
||||
|
} |
||||
|
return self; |
||||
|
} |
||||
|
|
||||
|
- (void)drawRect:(CGRect)rect |
||||
|
{ |
||||
|
CGContextRef context = UIGraphicsGetCurrentContext(); |
||||
|
|
||||
|
if (self.focusPoint.x > 0 || self.focusPoint.y > 0) { |
||||
|
[self _drawCrossHairAtPoint:self.focusPoint inContext:context withColor:[UIColor greenColor] isXShaped:YES]; |
||||
|
} |
||||
|
|
||||
|
if (self.exposurePoint.x > 0 || self.exposurePoint.y > 0) { |
||||
|
[self _drawCrossHairAtPoint:self.exposurePoint inContext:context withColor:[UIColor yellowColor] isXShaped:NO]; |
||||
|
} |
||||
|
|
||||
|
if (self.faceBoundsByFaceID.count > 0) { |
||||
|
[self.faceBoundsByFaceID |
||||
|
enumerateKeysAndObjectsUsingBlock:^(NSNumber *_Nonnull key, NSValue *_Nonnull obj, BOOL *_Nonnull stop) { |
||||
|
CGRect faceRect = [obj CGRectValue]; |
||||
|
NSInteger faceID = [key integerValue]; |
||||
|
[self _drawRectangle:faceRect |
||||
|
text:[NSString sc_stringWithFormat:@"ID: %@", key] |
||||
|
inContext:context |
||||
|
withColor:[UIColor colorWithRed:((faceID % 3) == 0) |
||||
|
green:((faceID % 3) == 1) |
||||
|
blue:((faceID % 3) == 2) |
||||
|
alpha:1.0]]; |
||||
|
}]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
- (void)dealloc |
||||
|
{ |
||||
|
[[SCManagedCapturer sharedInstance] removeListener:self]; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
Draw a crosshair with center point, context, color and shape. |
||||
|
|
||||
|
@param isXShaped "X" or "+" |
||||
|
*/ |
||||
|
- (void)_drawCrossHairAtPoint:(CGPoint)center |
||||
|
inContext:(CGContextRef)context |
||||
|
withColor:(UIColor *)color |
||||
|
isXShaped:(BOOL)isXShaped |
||||
|
{ |
||||
|
CGFloat width = kSCManagedCapturePreviewViewDebugViewCrossHairWidth; |
||||
|
|
||||
|
CGContextSetStrokeColorWithColor(context, color.CGColor); |
||||
|
CGContextSetLineWidth(context, kSCManagedCapturePreviewViewDebugViewCrossHairLineWidth); |
||||
|
CGContextBeginPath(context); |
||||
|
|
||||
|
if (isXShaped) { |
||||
|
CGContextMoveToPoint(context, center.x - width / 2, center.y - width / 2); |
||||
|
CGContextAddLineToPoint(context, center.x + width / 2, center.y + width / 2); |
||||
|
CGContextMoveToPoint(context, center.x + width / 2, center.y - width / 2); |
||||
|
CGContextAddLineToPoint(context, center.x - width / 2, center.y + width / 2); |
||||
|
} else { |
||||
|
CGContextMoveToPoint(context, center.x - width / 2, center.y); |
||||
|
CGContextAddLineToPoint(context, center.x + width / 2, center.y); |
||||
|
CGContextMoveToPoint(context, center.x, center.y - width / 2); |
||||
|
CGContextAddLineToPoint(context, center.x, center.y + width / 2); |
||||
|
} |
||||
|
|
||||
|
CGContextStrokePath(context); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
Draw a rectangle, with a text on the top left. |
||||
|
*/ |
||||
|
- (void)_drawRectangle:(CGRect)rect text:(NSString *)text inContext:(CGContextRef)context withColor:(UIColor *)color |
||||
|
{ |
||||
|
CGContextSetStrokeColorWithColor(context, color.CGColor); |
||||
|
CGContextSetLineWidth(context, kSCManagedCapturePreviewViewDebugViewCrossHairLineWidth); |
||||
|
CGContextBeginPath(context); |
||||
|
|
||||
|
CGContextMoveToPoint(context, CGRectGetMinX(rect), CGRectGetMinY(rect)); |
||||
|
CGContextAddLineToPoint(context, CGRectGetMinX(rect), CGRectGetMaxY(rect)); |
||||
|
CGContextAddLineToPoint(context, CGRectGetMaxX(rect), CGRectGetMaxY(rect)); |
||||
|
CGContextAddLineToPoint(context, CGRectGetMaxX(rect), CGRectGetMinY(rect)); |
||||
|
CGContextAddLineToPoint(context, CGRectGetMinX(rect), CGRectGetMinY(rect)); |
||||
|
|
||||
|
NSMutableParagraphStyle *textStyle = [[NSMutableParagraphStyle alloc] init]; |
||||
|
textStyle.alignment = NSTextAlignmentLeft; |
||||
|
NSDictionary *attributes = @{ |
||||
|
NSFontAttributeName : [UIFont boldSystemFontOfSize:16], |
||||
|
NSForegroundColorAttributeName : color, |
||||
|
NSParagraphStyleAttributeName : textStyle |
||||
|
}; |
||||
|
|
||||
|
[text drawInRect:rect withAttributes:attributes]; |
||||
|
|
||||
|
CGContextStrokePath(context); |
||||
|
} |
||||
|
|
||||
|
- (CGPoint)_convertPointOfInterest:(CGPoint)point |
||||
|
{ |
||||
|
SCAssertMainThread(); |
||||
|
CGPoint convertedPoint = |
||||
|
CGPointMake((1 - point.y) * CGRectGetWidth(self.bounds), point.x * CGRectGetHeight(self.bounds)); |
||||
|
if ([[SCManagedCapturer sharedInstance] isVideoMirrored]) { |
||||
|
convertedPoint.x = CGRectGetWidth(self.bounds) - convertedPoint.x; |
||||
|
} |
||||
|
return convertedPoint; |
||||
|
} |
||||
|
|
||||
|
- (NSDictionary<NSNumber *, NSValue *> *)_convertFaceBounds:(NSDictionary<NSNumber *, NSValue *> *)faceBoundsByFaceID |
||||
|
{ |
||||
|
SCAssertMainThread(); |
||||
|
NSMutableDictionary<NSNumber *, NSValue *> *convertedFaceBoundsByFaceID = |
||||
|
[NSMutableDictionary dictionaryWithCapacity:faceBoundsByFaceID.count]; |
||||
|
for (NSNumber *key in faceBoundsByFaceID.allKeys) { |
||||
|
CGRect faceBounds = [[faceBoundsByFaceID objectForKey:key] CGRectValue]; |
||||
|
CGRect convertedBounds = CGRectMake(CGRectGetMinY(faceBounds) * CGRectGetWidth(self.bounds), |
||||
|
CGRectGetMinX(faceBounds) * CGRectGetHeight(self.bounds), |
||||
|
CGRectGetHeight(faceBounds) * CGRectGetWidth(self.bounds), |
||||
|
CGRectGetWidth(faceBounds) * CGRectGetHeight(self.bounds)); |
||||
|
if (![[SCManagedCapturer sharedInstance] isVideoMirrored]) { |
||||
|
convertedBounds.origin.x = CGRectGetWidth(self.bounds) - CGRectGetMaxX(convertedBounds); |
||||
|
} |
||||
|
[convertedFaceBoundsByFaceID setObject:[NSValue valueWithCGRect:convertedBounds] forKey:key]; |
||||
|
} |
||||
|
return convertedFaceBoundsByFaceID; |
||||
|
} |
||||
|
|
||||
|
#pragma mark - SCManagedCapturerListener |
||||
|
- (void)managedCapturer:(id<SCCapturer>)managedCapturer didChangeExposurePoint:(CGPoint)exposurePoint |
||||
|
{ |
||||
|
runOnMainThreadAsynchronouslyIfNecessary(^{ |
||||
|
self.exposurePoint = [self _convertPointOfInterest:exposurePoint]; |
||||
|
[self setNeedsDisplay]; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
- (void)managedCapturer:(id<SCCapturer>)managedCapturer didChangeFocusPoint:(CGPoint)focusPoint |
||||
|
{ |
||||
|
runOnMainThreadAsynchronouslyIfNecessary(^{ |
||||
|
self.focusPoint = [self _convertPointOfInterest:focusPoint]; |
||||
|
[self setNeedsDisplay]; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
- (void)managedCapturer:(id<SCCapturer>)managedCapturer |
||||
|
didDetectFaceBounds:(NSDictionary<NSNumber *, NSValue *> *)faceBoundsByFaceID |
||||
|
{ |
||||
|
runOnMainThreadAsynchronouslyIfNecessary(^{ |
||||
|
self.faceBoundsByFaceID = [self _convertFaceBounds:faceBoundsByFaceID]; |
||||
|
[self setNeedsDisplay]; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
- (void)managedCapturer:(id<SCCapturer>)managedCapturer didChangeCaptureDevicePosition:(SCManagedCapturerState *)state |
||||
|
{ |
||||
|
runOnMainThreadAsynchronouslyIfNecessary(^{ |
||||
|
self.faceBoundsByFaceID = nil; |
||||
|
self.focusPoint = [self _convertPointOfInterest:CGPointMake(0.5, 0.5)]; |
||||
|
self.exposurePoint = [self _convertPointOfInterest:CGPointMake(0.5, 0.5)]; |
||||
|
[self setNeedsDisplay]; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
@end |
@ -0,0 +1,23 @@ |
|||||
|
// SCManagedCapturer.h |
||||
|
// Snapchat |
||||
|
// |
||||
|
// Created by Liu Liu on 4/20/15. |
||||
|
|
||||
|
#import "SCCapturer.h" |
||||
|
#import "SCManagedCapturerListener.h" |
||||
|
#import "SCManagedCapturerUtils.h" |
||||
|
|
||||
|
#import <Foundation/Foundation.h> |
||||
|
|
||||
|
/* |
||||
|
SCManagedCapturer is a shell class. Its job is to provide an singleton instance which follows protocol of |
||||
|
SCManagedCapturerImpl. The reason we use this pattern is because we are building SCManagedCapturerV2. This setup |
||||
|
offers |
||||
|
possbility for us to code V2 without breaking the existing app, and can test the new implementation via Tweak. |
||||
|
*/ |
||||
|
|
||||
|
@interface SCManagedCapturer : NSObject |
||||
|
|
||||
|
+ (id<SCCapturer>)sharedInstance; |
||||
|
|
||||
|
@end |
Write
Preview
Loading…
Cancel
Save
Reference in new issue