diff --git a/Features/Core/SCFeature.h b/Features/Core/SCFeature.h new file mode 100644 index 0000000..8720103 --- /dev/null +++ b/Features/Core/SCFeature.h @@ -0,0 +1,26 @@ +// +// SCFeature.h +// SCCamera +// +// Created by Kristian Bauer on 1/4/18. +// + +#import + +/** + * Top level protocol for UI features + */ +#define SCLogCameraFeatureInfo(fmt, ...) SCLogCoreCameraInfo(@"[SCFeature] " fmt, ##__VA_ARGS__) +@protocol SCFeatureContainerView; +@protocol SCFeature + +@optional +- (void)configureWithView:(UIView *)view; +- (void)forwardCameraTimerGesture:(UIGestureRecognizer *)gestureRecognizer; +- (void)forwardCameraOverlayTapGesture:(UIGestureRecognizer *)gestureRecognizer; +- (void)forwardLongPressGesture:(UIGestureRecognizer *)gestureRecognizer; +- (void)forwardPinchGesture:(UIPinchGestureRecognizer *)gestureRecognizer; +- (void)forwardPanGesture:(UIPanGestureRecognizer *)gestureRecognizer; +- (BOOL)shouldBlockTouchAtPoint:(CGPoint)point; + +@end diff --git a/Features/Core/SCFeatureContainerView.h b/Features/Core/SCFeatureContainerView.h new file mode 100644 index 0000000..fe5befa --- /dev/null +++ b/Features/Core/SCFeatureContainerView.h @@ -0,0 +1,13 @@ +// +// SCFeatureContainerView.h +// SCCamera +// +// Created by Kristian Bauer on 4/17/18. +// + +#import + +@protocol SCFeatureContainerView +- (BOOL)isTapGestureRecognizer:(UIGestureRecognizer *)gestureRecognizer; +- (CGRect)initialCameraTimerFrame; +@end diff --git a/Features/Core/SCFeatureCoordinator.h b/Features/Core/SCFeatureCoordinator.h new file mode 100644 index 0000000..db13f26 --- /dev/null +++ b/Features/Core/SCFeatureCoordinator.h @@ -0,0 +1,44 @@ +// +// SCFeatureCoordinator.h +// SCCamera +// +// Created by Kristian Bauer on 1/4/18. +// + +#import "SCFeature.h" + +#import + +@protocol SCFeatureProvider; +@class SCCameraOverlayView; + +/** + * Handles creation of SCFeatures and communication between owner and features. + */ +@interface SCFeatureCoordinator : NSObject + +SC_INIT_AND_NEW_UNAVAILABLE; +- (instancetype)initWithFeatureContainerView:(SCCameraOverlayView *)containerView + provider:(id)provider; + +/** + * Asks provider for features with given featureTypes specified in initializer. + */ +- (void)reloadFeatures; + +/** + * Eventually won't need this, but in order to use new framework w/ existing architecture, need a way to forward + * gestures to individual features. + */ +- (void)forwardCameraTimerGesture:(UIGestureRecognizer *)gestureRecognizer; +- (void)forwardCameraOverlayTapGesture:(UIGestureRecognizer *)gestureRecognizer; +- (void)forwardLongPressGesture:(UIGestureRecognizer *)gestureRecognizer; +- (void)forwardPinchGesture:(UIPinchGestureRecognizer *)recognizer; +- (void)forwardPanGesture:(UIPanGestureRecognizer *)recognizer; +/** + * To prevent gestures on AVCameraViewController from triggering at the same time as feature controls, need to provide a + * way for features to indicate that they will block a touch with given point. + */ +- (BOOL)shouldBlockTouchAtPoint:(CGPoint)point; + +@end diff --git a/Features/Core/SCFeatureCoordinator.m b/Features/Core/SCFeatureCoordinator.m new file mode 100644 index 0000000..971e11f --- /dev/null +++ b/Features/Core/SCFeatureCoordinator.m @@ -0,0 +1,117 @@ +// +// SCFeatureCoordinator.m +// SCCamera +// +// Created by Kristian Bauer on 1/4/18. +// + +#import "SCFeatureCoordinator.h" + +#import "SCFeature.h" +#import "SCFeatureProvider.h" + +#import +#import + +typedef NSString SCFeatureDictionaryKey; + +@interface SCFeatureCoordinator () +@property (nonatomic, weak) UIView *containerView; +@property (nonatomic, strong) id provider; +@end + +@implementation SCFeatureCoordinator + +- (instancetype)initWithFeatureContainerView:(UIView *)containerView + provider:(id)provider +{ + SCTraceODPCompatibleStart(2); + SCAssert(containerView, @"SCFeatureCoordinator containerView must be non-nil"); + SCAssert(provider, @"SCFeatureCoordinator provider must be non-nil"); + self = [super init]; + if (self) { + _containerView = containerView; + _provider = provider; + [self reloadFeatures]; + } + return self; +} + +- (void)reloadFeatures +{ + SCTraceODPCompatibleStart(2); + [_provider resetInstances]; + NSMutableArray *features = [NSMutableArray array]; + for (id feature in _provider.supportedFeatures) { + if ([feature respondsToSelector:@selector(configureWithView:)]) { + [feature configureWithView:_containerView]; + } + if (feature) { + [features addObject:feature]; + } + } +} + +- (void)forwardCameraTimerGesture:(UIGestureRecognizer *)gestureRecognizer +{ + SCTraceODPCompatibleStart(2); + for (id feature in _provider.supportedFeatures) { + if ([feature respondsToSelector:@selector(forwardCameraTimerGesture:)]) { + [feature forwardCameraTimerGesture:gestureRecognizer]; + } + } +} + +- (void)forwardCameraOverlayTapGesture:(UIGestureRecognizer *)gestureRecognizer +{ + SCTraceODPCompatibleStart(2); + for (id feature in _provider.supportedFeatures) { + if ([feature respondsToSelector:@selector(forwardCameraOverlayTapGesture:)]) { + [feature forwardCameraOverlayTapGesture:gestureRecognizer]; + } + } +} + +- (void)forwardLongPressGesture:(UIGestureRecognizer *)gestureRecognizer +{ + SCTraceODPCompatibleStart(2); + for (id feature in _provider.supportedFeatures) { + if ([feature respondsToSelector:@selector(forwardLongPressGesture:)]) { + [feature forwardLongPressGesture:gestureRecognizer]; + } + } +} + +- (void)forwardPinchGesture:(UIPinchGestureRecognizer *)gestureRecognizer +{ + SCTraceODPCompatibleStart(2); + for (id feature in _provider.supportedFeatures) { + if ([feature respondsToSelector:@selector(forwardPinchGesture:)]) { + [feature forwardPinchGesture:gestureRecognizer]; + } + } +} + +- (void)forwardPanGesture:(UIPanGestureRecognizer *)gestureRecognizer +{ + SCTraceODPCompatibleStart(2); + for (id feature in _provider.supportedFeatures) { + if ([feature respondsToSelector:@selector(forwardPanGesture:)]) { + [feature forwardPanGesture:gestureRecognizer]; + } + } +} + +- (BOOL)shouldBlockTouchAtPoint:(CGPoint)point +{ + SCTraceODPCompatibleStart(2); + for (id feature in _provider.supportedFeatures) { + if ([feature respondsToSelector:@selector(shouldBlockTouchAtPoint:)] && + [feature shouldBlockTouchAtPoint:point]) { + return YES; + } + } + return NO; +} + +@end diff --git a/Features/Core/SCFeatureProvider.h b/Features/Core/SCFeatureProvider.h new file mode 100644 index 0000000..e60474f --- /dev/null +++ b/Features/Core/SCFeatureProvider.h @@ -0,0 +1,50 @@ +// +// SCFeatureProvider.h +// SCCamera +// +// Created by Kristian Bauer on 1/4/18. +// + +#import + +#import + +@class SCFeatureSettingsManager, SCCapturerToken, SCUserSession; + +@protocol SCFeature +, SCCapturer, SCFeatureFlash, SCFeatureHandsFree, SCFeatureLensSideButton, SCFeatureLensButtonZ, SCFeatureMemories, + SCFeatureNightMode, SCFeatureSnapKit, SCFeatureTapToFocusAndExposure, SCFeatureToggleCamera, SCFeatureShazam, + SCFeatureImageCapture, SCFeatureScanning, SCFeatureZooming; + +/** + * Provides single location for creating and configuring SCFeatures. + */ +@protocol SCFeatureProvider + +@property (nonatomic) AVCameraViewType cameraViewType; + +@property (nonatomic, readonly) id capturer; +@property (nonatomic, strong, readwrite) SCCapturerToken *token; +@property (nonatomic, readonly) SCUserSession *userSession; +// TODO: We should not be reusing AVCameraViewController so eventually the +// context should be removed. +@property (nonatomic, readonly) AVCameraViewControllerContext context; +@property (nonatomic) id handsFreeRecording; +@property (nonatomic) id snapKit; +@property (nonatomic) id tapToFocusAndExposure; +@property (nonatomic) id memories; +@property (nonatomic) id flash; +@property (nonatomic) id lensSideButton; +@property (nonatomic) id lensZButton; +@property (nonatomic) id nightMode; +@property (nonatomic) id toggleCamera; +@property (nonatomic) id shazam; +@property (nonatomic) id scanning; +@property (nonatomic) id imageCapture; +@property (nonatomic) id zooming; + +@property (nonatomic, readonly) NSArray> *supportedFeatures; + +- (void)resetInstances; + +@end diff --git a/Features/Flash/SCFeatureFlash.h b/Features/Flash/SCFeatureFlash.h new file mode 100644 index 0000000..c579de1 --- /dev/null +++ b/Features/Flash/SCFeatureFlash.h @@ -0,0 +1,20 @@ +// +// SCFeatureFlash.h +// SCCamera +// +// Created by Kristian Bauer on 3/27/18. +// + +#import "SCFeature.h" + +@class SCNavigationBarButtonItem; + +/** + * Public interface for interacting with camera flash feature. + */ +@protocol SCFeatureFlash +@property (nonatomic, readonly) SCNavigationBarButtonItem *navigationBarButtonItem; + +- (void)interruptGestures; + +@end diff --git a/Features/Flash/SCFeatureFlashImpl.h b/Features/Flash/SCFeatureFlashImpl.h new file mode 100644 index 0000000..95f9c3d --- /dev/null +++ b/Features/Flash/SCFeatureFlashImpl.h @@ -0,0 +1,23 @@ +// +// SCFeatureFlashImpl.h +// SCCamera +// +// Created by Kristian Bauer on 3/27/18. +// + +#import "SCFeatureFlash.h" + +#import + +@class SCLogger; +@protocol SCCapturer; + +/** + * Interface for camera flash feature. Handles enabling/disabling of camera flash via SCCapturer and UI for displaying + * flash button. + * Should only expose initializer. All other vars and methods should be declared in SCFeatureFlash protocol. + */ +@interface SCFeatureFlashImpl : NSObject +SC_INIT_AND_NEW_UNAVAILABLE +- (instancetype)initWithCapturer:(id)capturer logger:(SCLogger *)logger NS_DESIGNATED_INITIALIZER; +@end diff --git a/Features/Flash/SCFeatureFlashImpl.m b/Features/Flash/SCFeatureFlashImpl.m new file mode 100644 index 0000000..5cfdb8b --- /dev/null +++ b/Features/Flash/SCFeatureFlashImpl.m @@ -0,0 +1,226 @@ +// +// SCFeatureFlashImpl.m +// SCCamera +// +// Created by Kristian Bauer on 3/27/18. +// + +#import "SCFeatureFlashImpl.h" + +#import "SCCapturer.h" +#import "SCFlashButton.h" +#import "SCManagedCapturerListener.h" +#import "SCManagedCapturerState.h" + +#import +#import +#import +#import +#import + +static CGFloat const kSCFlashButtonInsets = -2.f; +static CGRect const kSCFlashButtonFrame = {0, 0, 36, 44}; + +static NSString *const kSCFlashEventName = @"TOGGLE_CAMERA_FLASH_BUTTON"; +static NSString *const kSCFlashEventParameterFlashName = @"flash_on"; +static NSString *const kSCFlashEventParameterCameraName = @"front_facing_camera_on"; + +@interface SCFeatureFlashImpl () +@property (nonatomic, strong, readwrite) id capturer; +@property (nonatomic, strong, readwrite) SCLogger *logger; +@property (nonatomic, strong, readwrite) SCFlashButton *flashButton; +@property (nonatomic, weak, readwrite) UIView *containerView; +@property (nonatomic, strong, readwrite) SCManagedCapturerState *managedCapturerState; +@property (nonatomic, assign, readwrite) BOOL canEnable; +@end + +@interface SCFeatureFlashImpl (SCManagedCapturerListener) +@end + +@implementation SCFeatureFlashImpl +@synthesize navigationBarButtonItem = _navigationBarButtonItem; + +- (instancetype)initWithCapturer:(id)capturer logger:(SCLogger *)logger +{ + SCTraceODPCompatibleStart(2); + self = [super init]; + if (self) { + _capturer = capturer; + [_capturer addListener:self]; + _logger = logger; + } + return self; +} + +- (void)dealloc +{ + SCTraceODPCompatibleStart(2); + [_capturer removeListener:self]; +} + +#pragma mark - SCFeature + +- (void)configureWithView:(UIView *)view +{ + SCTraceODPCompatibleStart(2); + _containerView = view; +} + +- (BOOL)shouldBlockTouchAtPoint:(CGPoint)point +{ + SCTraceODPCompatibleStart(2); + SC_GUARD_ELSE_RETURN_VALUE(_flashButton.userInteractionEnabled && !_flashButton.hidden, NO); + CGPoint convertedPoint = [_flashButton convertPoint:point fromView:_containerView]; + return [_flashButton pointInside:convertedPoint withEvent:nil]; +} + +#pragma mark - SCFeatureFlash + +- (void)interruptGestures +{ + SCTraceODPCompatibleStart(2); + [_flashButton interruptGestures]; +} + +- (SCNavigationBarButtonItem *)navigationBarButtonItem +{ + SCTraceODPCompatibleStart(2); + SC_GUARD_ELSE_RETURN_VALUE(!_navigationBarButtonItem, _navigationBarButtonItem); + _navigationBarButtonItem = [[SCNavigationBarButtonItem alloc] initWithCustomView:self.flashButton]; + return _navigationBarButtonItem; +} + +#pragma mark - Getters + +- (SCFlashButton *)flashButton +{ + SCTraceODPCompatibleStart(2); + SC_GUARD_ELSE_RETURN_VALUE(!_flashButton, _flashButton); + _flashButton = [[SCFlashButton alloc] initWithFrame:kSCFlashButtonFrame]; + _flashButton.layer.sublayerTransform = CATransform3DMakeTranslation(kSCFlashButtonInsets, 0, 0); + _flashButton.buttonState = SCFlashButtonStateOff; + _flashButton.maximumScale = 1.1111f; + [_flashButton addTarget:self action:@selector(_flashTapped)]; + + _flashButton.accessibilityIdentifier = @"flash"; + _flashButton.accessibilityLabel = SCLocalizedString(@"flash", 0); + return _flashButton; +} + +#pragma mark - Setters + +- (void)setCanEnable:(BOOL)canEnable +{ + SCTraceODPCompatibleStart(2); + SCLogCameraFeatureInfo(@"[%@] setCanEnable new: %@ old: %@", NSStringFromClass([self class]), + canEnable ? @"YES" : @"NO", _canEnable ? @"YES" : @"NO"); + self.flashButton.userInteractionEnabled = canEnable; +} + +#pragma mark - Internal Helpers + +- (void)_flashTapped +{ + SCTraceODPCompatibleStart(2); + BOOL flashActive = !_managedCapturerState.flashActive; + + SCLogCameraFeatureInfo(@"[%@] _flashTapped flashActive new: %@ old: %@", NSStringFromClass([self class]), + flashActive ? @"YES" : @"NO", !flashActive ? @"YES" : @"NO"); + _containerView.userInteractionEnabled = NO; + @weakify(self); + [_capturer setFlashActive:flashActive + completionHandler:^{ + @strongify(self); + SCLogCameraFeatureInfo(@"[%@] _flashTapped setFlashActive completion", NSStringFromClass([self class])); + self.containerView.userInteractionEnabled = YES; + } + context:SCCapturerContext]; + + NSDictionary *loggingParameters = @{ + kSCFlashEventParameterFlashName : @(flashActive), + kSCFlashEventParameterCameraName : + @(_managedCapturerState.devicePosition == SCManagedCaptureDevicePositionFront) + }; + [_logger logEvent:kSCFlashEventName parameters:loggingParameters]; +} + +- (BOOL)_shouldHideForState:(SCManagedCapturerState *)state +{ + SCTraceODPCompatibleStart(2); + return (!state.flashSupported && !state.torchSupported && + state.devicePosition != SCManagedCaptureDevicePositionFront) || + state.arSessionActive; +} + +@end + +@implementation SCFeatureFlashImpl (SCManagedCapturerListener) + +- (void)managedCapturer:(id)managedCapturer didChangeFlashActive:(SCManagedCapturerState *)state +{ + SCTraceODPCompatibleStart(2); + SCLogCameraFeatureInfo(@"[%@] didChangeFlashActive flashActive: %@", NSStringFromClass([self class]), + state.flashActive ? @"YES" : @"NO"); + self.flashButton.buttonState = state.flashActive ? SCFlashButtonStateOn : SCFlashButtonStateOff; +} + +- (void)managedCapturer:(id)managedCapturer + didChangeFlashSupportedAndTorchSupported:(SCManagedCapturerState *)state +{ + SCTraceODPCompatibleStart(2); + SCLogCameraFeatureInfo( + @"[%@] didChangeFlashSupportedAndTorchSupported flashSupported: %@ torchSupported: %@ devicePosition: %@", + NSStringFromClass([self class]), state.flashSupported ? @"YES" : @"NO", state.torchSupported ? @"YES" : @"NO", + state.devicePosition == SCManagedCaptureDevicePositionFront ? @"front" : @"back"); + self.flashButton.hidden = [self _shouldHideForState:state]; +} + +- (void)managedCapturer:(id)managedCapturer didChangeState:(SCManagedCapturerState *)state +{ + SCTraceODPCompatibleStart(2); + _managedCapturerState = [state copy]; +} + +- (void)managedCapturer:(id)managedCapturer didChangeARSessionActive:(SCManagedCapturerState *)state +{ + SCTraceODPCompatibleStart(2); + SCLogCameraFeatureInfo(@"[%@] didChangeARSessionActive: %@", NSStringFromClass([self class]), + state.arSessionActive ? @"YES" : @"NO"); + self.flashButton.hidden = [self _shouldHideForState:state]; +} + +- (void)managedCapturer:(id)managedCapturer + didBeginVideoRecording:(SCManagedCapturerState *)state + session:(SCVideoCaptureSessionInfo)session +{ + SCTraceODPCompatibleStart(2); + self.canEnable = NO; +} + +- (void)managedCapturer:(id)managedCapturer + didFinishRecording:(SCManagedCapturerState *)state + session:(SCVideoCaptureSessionInfo)session + recordedVideo:(SCManagedRecordedVideo *)recordedVideo +{ + SCTraceODPCompatibleStart(2); + self.canEnable = YES; +} + +- (void)managedCapturer:(id)managedCapturer + didFailRecording:(SCManagedCapturerState *)state + session:(SCVideoCaptureSessionInfo)session + error:(NSError *)error +{ + SCTraceODPCompatibleStart(2); + self.canEnable = YES; +} + +- (void)managedCapturer:(id)managedCapturer + didCancelRecording:(SCManagedCapturerState *)state + session:(SCVideoCaptureSessionInfo)session +{ + SCTraceODPCompatibleStart(2); + self.canEnable = YES; +} + +@end diff --git a/Features/Flash/SCFlashButton.h b/Features/Flash/SCFlashButton.h new file mode 100644 index 0000000..b287930 --- /dev/null +++ b/Features/Flash/SCFlashButton.h @@ -0,0 +1,15 @@ +// +// SCFlashButton.h +// SCCamera +// +// Created by Will Wu on 2/13/14. +// Copyright (c) 2014 Snapchat, Inc. All rights reserved. +// + +#import + +typedef NS_ENUM(NSInteger, SCFlashButtonState) { SCFlashButtonStateOn = 0, SCFlashButtonStateOff = 1 }; + +@interface SCFlashButton : SCGrowingButton +@property (nonatomic, assign) SCFlashButtonState buttonState; +@end diff --git a/Features/Flash/SCFlashButton.m b/Features/Flash/SCFlashButton.m new file mode 100644 index 0000000..daeca88 --- /dev/null +++ b/Features/Flash/SCFlashButton.m @@ -0,0 +1,35 @@ +// +// SCFlashButton.m +// SCCamera +// +// Created by Will Wu on 2/13/14. +// Copyright (c) 2014 Snapchat, Inc. All rights reserved. +// + +#import "SCFlashButton.h" + +#import + +@implementation SCFlashButton + +- (void)setButtonState:(SCFlashButtonState)buttonState +{ + // Don't reset flash button state if it doesn't change. + if (_buttonState == buttonState) { + return; + } + _buttonState = buttonState; + + if (buttonState == SCFlashButtonStateOn) { + self.image = [UIImage imageNamed:@"camera_flash_on_v10"]; + self.accessibilityValue = @"on"; + } else { + self.image = [UIImage imageNamed:@"camera_flash_off_v10"]; + self.accessibilityValue = @"off"; + } + + self.imageInset = SCRoundSizeToPixels(CGSizeMake((CGRectGetWidth(self.bounds) - self.image.size.width) / 2, + (CGRectGetHeight(self.bounds) - self.image.size.height) / 2)); +} + +@end diff --git a/Features/HandsFree/SCFeatureHandsFree.h b/Features/HandsFree/SCFeatureHandsFree.h new file mode 100644 index 0000000..f981241 --- /dev/null +++ b/Features/HandsFree/SCFeatureHandsFree.h @@ -0,0 +1,30 @@ +// +// SCFeatureHandsFree.h +// SCCamera +// +// Created by Kristian Bauer on 2/26/18. +// + +#import "SCFeature.h" + +#import + +@class SCLongPressGestureRecognizer, SCPreviewPresenter; + +@protocol SCFeatureHandsFree +@property (nonatomic, weak) SCPreviewPresenter *previewPresenter; +@property (nonatomic, strong, readonly) SCLongPressGestureRecognizer *longPressGestureRecognizer; + +/** + * Whether the feature is enabled or not. + */ +@property (nonatomic) BOOL enabled; +- (void)setupRecordLifecycleEventsWithMethod:(SCCameraRecordingMethod)method; +- (BOOL)shouldDisplayMultiSnapTooltip; + +/** + * Block called when user cancels hands-free recording via X button. + */ +- (void)setCancelBlock:(dispatch_block_t)cancelBlock; + +@end diff --git a/Features/ImageCapture/SCFeatureImageCapture.h b/Features/ImageCapture/SCFeatureImageCapture.h new file mode 100644 index 0000000..fc53bff --- /dev/null +++ b/Features/ImageCapture/SCFeatureImageCapture.h @@ -0,0 +1,27 @@ +// +// SCFeatureImageCapture.h +// SCCamera +// +// Created by Kristian Bauer on 4/18/18. +// + +#import "SCFeature.h" + +#import + +@protocol SCFeatureImageCapture; + +@protocol SCFeatureImageCaptureDelegate +- (void)featureImageCapture:(id)featureImageCapture willCompleteWithImage:(UIImage *)image; +- (void)featureImageCapture:(id)featureImageCapture didCompleteWithError:(NSError *)error; +- (void)featureImageCapturedDidComplete:(id)featureImageCapture; +@end + +/** + SCFeature protocol for capturing an image. + */ +@protocol SCFeatureImageCapture +@property (nonatomic, weak, readwrite) id delegate; +@property (nonatomic, strong, readonly) SCPromise *imagePromise; +- (void)captureImage:(NSString *)captureSessionID; +@end diff --git a/Features/ImageCapture/SCFeatureImageCaptureImpl.h b/Features/ImageCapture/SCFeatureImageCaptureImpl.h new file mode 100644 index 0000000..9ad1b50 --- /dev/null +++ b/Features/ImageCapture/SCFeatureImageCaptureImpl.h @@ -0,0 +1,21 @@ +// +// SCFeatureImageCaptureImpl.h +// SCCamera +// +// Created by Kristian Bauer on 4/18/18. +// + +#import "AVCameraViewEnums.h" +#import "SCFeatureImageCapture.h" + +#import + +@protocol SCCapturer; +@class SCLogger; + +@interface SCFeatureImageCaptureImpl : NSObject +SC_INIT_AND_NEW_UNAVAILABLE +- (instancetype)initWithCapturer:(id)capturer + logger:(SCLogger *)logger + cameraViewType:(AVCameraViewType)cameraViewType NS_DESIGNATED_INITIALIZER; +@end diff --git a/Features/ImageCapture/SCFeatureImageCaptureImpl.m b/Features/ImageCapture/SCFeatureImageCaptureImpl.m new file mode 100644 index 0000000..1762b37 --- /dev/null +++ b/Features/ImageCapture/SCFeatureImageCaptureImpl.m @@ -0,0 +1,184 @@ +// +// SCFeatureImageCaptureImpl.m +// SCCamera +// +// Created by Kristian Bauer on 4/18/18. +// + +#import "SCFeatureImageCaptureImpl.h" + +#import "SCLogger+Camera.h" +#import "SCManagedCapturePreviewLayerController.h" +#import "SCManagedCapturerLensAPI.h" +#import "SCManagedCapturerListener.h" +#import "SCManagedCapturerUtils.h" +#import "SCManagedStillImageCapturer.h" + +#import +#import +#import +#import +#import +#import +#import + +@interface SCFeatureImageCaptureImpl () +@property (nonatomic, strong, readwrite) id capturer; +@property (nonatomic, strong, readwrite) SCLogger *logger; +@property (nonatomic, assign) AVCameraViewType cameraViewType; +@property (nonatomic, strong, readwrite) SCManagedCapturerState *managedCapturerState; + +/** + * Whether user has attempted image capture in current session. Reset on foreground of app. + */ +@property (nonatomic, assign) BOOL hasTriedCapturing; +@end + +@interface SCFeatureImageCaptureImpl (SCManagedCapturerListener) +@end + +@implementation SCFeatureImageCaptureImpl +@synthesize delegate = _delegate; +@synthesize imagePromise = _imagePromise; + +- (instancetype)initWithCapturer:(id)capturer + logger:(SCLogger *)logger + cameraViewType:(AVCameraViewType)cameraViewType +{ + SCTraceODPCompatibleStart(2); + self = [super init]; + if (self) { + _capturer = capturer; + [_capturer addListener:self]; + _logger = logger; + _cameraViewType = cameraViewType; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(_viewWillEnterForeground) + name:UIApplicationWillEnterForegroundNotification + object:nil]; + } + return self; +} + +- (void)dealloc +{ + [_capturer removeListener:self]; +} + +#pragma mark - SCFeatureImageCapture + +- (void)captureImage:(NSString *)captureSessionID +{ + SCTraceODPCompatibleStart(2); + [_logger logTimedEventStart:kSCCameraMetricsRecordingDelay uniqueId:@"IMAGE" isUniqueEvent:NO]; + BOOL asyncCaptureEnabled = [self _asynchronousCaptureEnabled:_managedCapturerState]; + SCLogCameraFeatureInfo(@"[%@] takeImage begin async: %@", NSStringFromClass([self class]), + asyncCaptureEnabled ? @"YES" : @"NO"); + + if (asyncCaptureEnabled) { + SCQueuePerformer *performer = [[SCQueuePerformer alloc] initWithLabel:"com.snapchat.image-capture-promise" + qualityOfService:QOS_CLASS_USER_INTERACTIVE + queueType:DISPATCH_QUEUE_SERIAL + context:SCQueuePerformerContextCoreCamera]; + _imagePromise = [[SCPromise alloc] initWithPerformer:performer]; + } + + @weakify(self); + [_capturer captureStillImageAsynchronouslyWithAspectRatio:SCManagedCapturedImageAndVideoAspectRatio() + captureSessionID:captureSessionID + completionHandler:^(UIImage *fullScreenImage, NSDictionary *metadata, + NSError *error, SCManagedCapturerState *state) { + @strongify(self); + SC_GUARD_ELSE_RETURN(self); + [self _takeImageCallback:fullScreenImage + metadata:metadata + error:error + state:state]; + } + context:SCCapturerContext]; + [_logger logCameraCaptureFinishedWithDuration:0]; +} + +#pragma mark - Private + +- (void)_viewWillEnterForeground +{ + SCTraceODPCompatibleStart(2); + _hasTriedCapturing = NO; +} + +- (void)_takeImageCallback:(UIImage *)image + metadata:(NSDictionary *)metadata + error:(NSError *)error + state:(SCManagedCapturerState *)state +{ + SCTraceODPCompatibleStart(2); + [self _logCaptureComplete:state]; + + if (image) { + [_delegate featureImageCapture:self willCompleteWithImage:image]; + if (_imagePromise) { + [_imagePromise completeWithValue:image]; + } + } else { + if (_imagePromise) { + [_imagePromise completeWithError:[NSError errorWithDomain:@"" code:-1 userInfo:nil]]; + } + [_delegate featureImageCapture:self didCompleteWithError:error]; + } + _imagePromise = nil; + [_delegate featureImageCapturedDidComplete:self]; +} + +- (BOOL)_asynchronousCaptureEnabled:(SCManagedCapturerState *)state +{ + SCTraceODPCompatibleStart(2); + BOOL shouldCaptureImageFromVideoBuffer = + [SCDeviceName isSimilarToIphone5orNewer] && ![SCDeviceName isSimilarToIphone6orNewer]; + // Fast image capture is disabled in following cases + // (1) flash is on; + // (2) lenses are active; + // (3) SCPhotoCapturer is not supported; + // (4) not main camera for iPhoneX; + return !state.flashActive && !state.lensesActive && !_capturer.lensProcessingCore.appliedLens && + (SCPhotoCapturerIsEnabled() || shouldCaptureImageFromVideoBuffer) && + (![SCDeviceName isIphoneX] || (_cameraViewType == AVCameraViewNoReply)); +} + +- (void)_logCaptureComplete:(SCManagedCapturerState *)state +{ + SCTraceODPCompatibleStart(2); + NSDictionary *params = @{ + @"type" : @"image", + @"lenses_active" : @(state.lensesActive), + @"is_back_camera" : @(state.devicePosition != SCManagedCaptureDevicePositionFront), + @"is_main_camera" : @(_cameraViewType == AVCameraViewNoReply), + @"is_first_attempt_after_app_startup" : @(!_hasTriedCapturing), + @"app_startup_type" : SCLaunchType(), + @"app_startup_time" : @(SCAppStartupTimeMicros() / 1000.0), + @"time_elapse_after_app_startup" : @(SCTimeElapseAfterAppStartupMicros() / 1000.0), + }; + [_logger logTimedEventEnd:kSCCameraMetricsRecordingDelay uniqueId:@"IMAGE" parameters:params]; + _hasTriedCapturing = YES; +} + +@end + +@implementation SCFeatureImageCaptureImpl (SCManagedCapturerListener) + +- (void)managedCapturer:(id)managedCapturer didChangeState:(SCManagedCapturerState *)state +{ + SCTraceODPCompatibleStart(2); + _managedCapturerState = [state copy]; +} + +- (void)managedCapturer:(id)managedCapturer didCapturePhoto:(SCManagedCapturerState *)state +{ + SCTraceODPCompatibleStart(2); + if (_imagePromise) { + [[SCManagedCapturePreviewLayerController sharedInstance] pause]; + } +} + +@end diff --git a/Features/NightMode/SCFeatureNightMode.h b/Features/NightMode/SCFeatureNightMode.h new file mode 100644 index 0000000..e0bc488 --- /dev/null +++ b/Features/NightMode/SCFeatureNightMode.h @@ -0,0 +1,22 @@ +// +// SCFeatureNightMode.h +// SCCamera +// +// Created by Kristian Bauer on 4/9/18. +// + +#import "SCFeature.h" + +@class SCNavigationBarButtonItem, SCPreviewPresenter; + +/** + * Public interface for interacting with camera night mode feature. + * User spec: https://snapchat.quip.com/w4h4ArzcmXCS + */ +@protocol SCFeatureNightMode +@property (nonatomic, weak, readwrite) SCPreviewPresenter *previewPresenter; +@property (nonatomic, readonly) SCNavigationBarButtonItem *navigationBarButtonItem; + +- (void)interruptGestures; +- (void)hideWithDelayIfNeeded; +@end diff --git a/Features/NightMode/SCNightModeButton.h b/Features/NightMode/SCNightModeButton.h new file mode 100644 index 0000000..2fe1398 --- /dev/null +++ b/Features/NightMode/SCNightModeButton.h @@ -0,0 +1,18 @@ +// +// SCNightModeButton.h +// SCCamera +// +// Created by Liu Liu on 3/19/15. +// Copyright (c) 2015 Snapchat, Inc. All rights reserved. +// + +#import +#import + +@interface SCNightModeButton : SCGrowingButton +@property (nonatomic, assign, getter=isSelected) BOOL selected; +SC_INIT_AND_NEW_UNAVAILABLE +- (void)show; +- (void)hideWithDelay:(BOOL)delay; +- (BOOL)willHideAfterDelay; +@end diff --git a/Features/NightMode/SCNightModeButton.m b/Features/NightMode/SCNightModeButton.m new file mode 100644 index 0000000..4606a16 --- /dev/null +++ b/Features/NightMode/SCNightModeButton.m @@ -0,0 +1,95 @@ +// +// SCNightModeButton.m +// SCCamera +// +// Created by Liu Liu on 3/19/15. +// Copyright (c) 2015 Snapchat, Inc. All rights reserved. +// + +#import "SCNightModeButton.h" + +#import + +static NSTimeInterval const kSCNightModeButtonHiddenDelay = 2.5; + +@implementation SCNightModeButton { + dispatch_block_t _delayedHideBlock; +} + +- (instancetype)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + if (self) { + self.image = [UIImage imageNamed:@"camera_nightmode_off_v10"]; + self.imageInset = CGSizeMake((CGRectGetWidth(self.bounds) - self.image.size.width) / 2, + (CGRectGetHeight(self.bounds) - self.image.size.height) / 2); + } + return self; +} + +- (void)setSelected:(BOOL)selected +{ + SC_GUARD_ELSE_RETURN(_selected != selected); + if (selected) { + [self _cancelDelayedHideAnimation]; + self.image = [UIImage imageNamed:@"camera_nightmode_on_v10"]; + } else { + self.image = [UIImage imageNamed:@"camera_nightmode_off_v10"]; + } + self.imageInset = CGSizeMake((CGRectGetWidth(self.bounds) - self.image.size.width) / 2, + (CGRectGetHeight(self.bounds) - self.image.size.height) / 2); + _selected = selected; +} + +- (void)show +{ + SC_GUARD_ELSE_RETURN(self.hidden); + SCAssertMainThread(); + [self _cancelDelayedHideAnimation]; + self.hidden = NO; + [self animate]; +} + +- (void)hideWithDelay:(BOOL)delay +{ + SC_GUARD_ELSE_RETURN(!self.hidden); + SCAssertMainThread(); + [self _cancelDelayedHideAnimation]; + if (delay) { + @weakify(self); + _delayedHideBlock = dispatch_block_create(0, ^{ + @strongify(self); + SC_GUARD_ELSE_RETURN(self); + [UIView animateWithDuration:0.3 + animations:^{ + self.alpha = 0; + } + completion:^(BOOL finished) { + self.alpha = 1; + self.hidden = YES; + _delayedHideBlock = nil; + }]; + }); + dispatch_time_t delayTime = + dispatch_time(DISPATCH_TIME_NOW, (int64_t)(kSCNightModeButtonHiddenDelay * NSEC_PER_SEC)); + dispatch_after(delayTime, dispatch_get_main_queue(), _delayedHideBlock); + } else { + self.hidden = YES; + } +} + +- (BOOL)willHideAfterDelay +{ + return _delayedHideBlock != nil; +} + +#pragma mark - Private + +- (void)_cancelDelayedHideAnimation +{ + SC_GUARD_ELSE_RETURN(_delayedHideBlock); + dispatch_cancel(_delayedHideBlock); + _delayedHideBlock = nil; +} + +@end diff --git a/Features/Scanning/SCFeatureScanning.h b/Features/Scanning/SCFeatureScanning.h new file mode 100644 index 0000000..8dc5ab5 --- /dev/null +++ b/Features/Scanning/SCFeatureScanning.h @@ -0,0 +1,26 @@ +// +// SCFeatureScanning.h +// Snapchat +// +// Created by Xiaokang Liu on 2018/4/19. +// + +#import "SCFeature.h" + +@protocol SCFeatureScanning; + +@protocol SCFeatureScanningDelegate +- (void)featureScanning:(id)featureScanning didFinishWithResult:(NSObject *)resultObject; +@end + +/** + This SCFeature allows the user to long press on the screen to scan a snapcode. + */ +@protocol SCFeatureScanning +@property (nonatomic, weak) id delegate; +@property (nonatomic, assign) NSTimeInterval lastSuccessfulScanTime; +- (void)startScanning; +- (void)stopScanning; + +- (void)stopSearch; +@end diff --git a/Features/Shazam/SCFeatureShazam.h b/Features/Shazam/SCFeatureShazam.h new file mode 100644 index 0000000..4a19cf2 --- /dev/null +++ b/Features/Shazam/SCFeatureShazam.h @@ -0,0 +1,23 @@ +// +// SCFeatureShazam.h +// SCCamera +// +// Created by Xiaokang Liu on 2018/4/18. +// + +#import "SCFeature.h" + +@class SCLens; +@protocol SCFeatureShazam; + +@protocol SCFeatureShazamDelegate +- (void)featureShazam:(id)featureShazam didFinishWithResult:(NSObject *)result; +- (void)featureShazamDidSubmitSearchRequest:(id)featureShazam; +- (SCLens *)filterLensForFeatureShazam:(id)featureShazam; +@end + +@protocol SCFeatureShazam +@property (nonatomic, weak) id delegate; +- (void)stopAudioRecordingAsynchronously; +- (void)resetInfo; +@end diff --git a/Features/SnapKit/SCFeatureSnapKit.h b/Features/SnapKit/SCFeatureSnapKit.h new file mode 100644 index 0000000..64ff824 --- /dev/null +++ b/Features/SnapKit/SCFeatureSnapKit.h @@ -0,0 +1,14 @@ +// +// SCFeatureSnapKit.h +// SCCamera +// +// Created by Michel Loenngren on 3/19/18. +// + +#import "SCFeature.h" + +@class SCCameraDeepLinkMetadata; + +@protocol SCFeatureSnapKit +- (void)setDeepLinkMetadata:(SCCameraDeepLinkMetadata *)metadata; +@end diff --git a/Features/TapToFocus/SCFeatureTapToFocusAndExposure.h b/Features/TapToFocus/SCFeatureTapToFocusAndExposure.h new file mode 100644 index 0000000..30ef02d --- /dev/null +++ b/Features/TapToFocus/SCFeatureTapToFocusAndExposure.h @@ -0,0 +1,17 @@ +// +// SCFeatureTapToFocusAndExposure.h +// SCCamera +// +// Created by Michel Loenngren on 4/5/18. +// + +#import "SCFeature.h" + +/** + This SCFeature allows the user to tap on the screen to adjust focus and exposure. + */ +@protocol SCFeatureTapToFocusAndExposure + +- (void)reset; + +@end diff --git a/Features/TapToFocus/SCFeatureTapToFocusAndExposureImpl.h b/Features/TapToFocus/SCFeatureTapToFocusAndExposureImpl.h new file mode 100644 index 0000000..3a9efad --- /dev/null +++ b/Features/TapToFocus/SCFeatureTapToFocusAndExposureImpl.h @@ -0,0 +1,49 @@ +// +// SCFeatureTapToFocusAndExposureImpl.h +// SCCamera +// +// Created by Michel Loenngren on 4/5/18. +// + +#import "SCFeatureTapToFocusAndExposure.h" + +#import + +#import + +@protocol SCCapturer; + +/** + Protocol describing unique camera commands to run when the user taps on screen. These could be focus, exposure or tap + to portrait mode. + */ +@protocol SCFeatureCameraTapCommand +- (void)execute:(CGPoint)pointOfInterest capturer:(id)capturer; +@end + +/** + This is the default implementation of SCFeatureTapToFocusAndExposure allowing the user to tap on the camera overlay + view in order to adjust focus and exposure. + */ +@interface SCFeatureTapToFocusAndExposureImpl : NSObject +SC_INIT_AND_NEW_UNAVAILABLE +- (instancetype)initWithCapturer:(id)capturer commands:(NSArray> *)commands; +@end + +/** + Adjust focus on tap. + */ +@interface SCFeatureCameraFocusTapCommand : NSObject +@end + +/** + Adjust exposure on tap. + */ +@interface SCFeatureCameraExposureTapCommand : NSObject +@end + +/** + Adjust portrait mode point of interest on tap. + */ +@interface SCFeatureCameraPortraitTapCommand : NSObject +@end diff --git a/Features/TapToFocus/SCFeatureTapToFocusAndExposureImpl.m b/Features/TapToFocus/SCFeatureTapToFocusAndExposureImpl.m new file mode 100644 index 0000000..b11bc5a --- /dev/null +++ b/Features/TapToFocus/SCFeatureTapToFocusAndExposureImpl.m @@ -0,0 +1,118 @@ +// +// SCFeatureTapToFocusImpl.m +// SCCamera +// +// Created by Michel Loenngren on 4/5/18. +// + +#import "SCFeatureTapToFocusAndExposureImpl.h" + +#import "SCCameraTweaks.h" +#import "SCCapturer.h" +#import "SCFeatureContainerView.h" +#import "SCTapAnimationView.h" + +#import +#import + +@interface SCFeatureTapToFocusAndExposureImpl () +@property (nonatomic, weak) id capturer; +@property (nonatomic, weak) UIView *containerView; +@property (nonatomic) BOOL userTappedToFocusAndExposure; +@property (nonatomic) NSArray> *commands; +@end + +@implementation SCFeatureTapToFocusAndExposureImpl + +- (instancetype)initWithCapturer:(id)capturer commands:(NSArray> *)commands +{ + if (self = [super init]) { + _capturer = capturer; + _commands = commands; + } + return self; +} + +- (void)reset +{ + SC_GUARD_ELSE_RETURN(_userTappedToFocusAndExposure); + _userTappedToFocusAndExposure = NO; + [_capturer continuousAutofocusAndExposureAsynchronouslyWithCompletionHandler:nil context:SCCapturerContext]; +} + +#pragma mark - SCFeature + +- (void)configureWithView:(UIView *)view +{ + SCTraceODPCompatibleStart(2); + _containerView = view; +} + +- (void)forwardCameraOverlayTapGesture:(UIGestureRecognizer *)gestureRecognizer +{ + SCTraceODPCompatibleStart(2); + CGPoint point = [gestureRecognizer locationInView:gestureRecognizer.view]; + @weakify(self); + [_capturer convertViewCoordinates:[gestureRecognizer locationInView:_containerView] + completionHandler:^(CGPoint pointOfInterest) { + @strongify(self); + SC_GUARD_ELSE_RETURN(self); + SCLogCameraFeatureInfo(@"Tapped to focus: %@", NSStringFromCGPoint(pointOfInterest)); + [self _applyTapCommands:pointOfInterest]; + [self _showTapAnimationAtPoint:point forGesture:gestureRecognizer]; + } + context:SCCapturerContext]; +} + +#pragma mark - Private helpers + +- (void)_applyTapCommands:(CGPoint)pointOfInterest +{ + SCTraceODPCompatibleStart(2); + for (id command in _commands) { + [command execute:pointOfInterest capturer:_capturer]; + } + self.userTappedToFocusAndExposure = YES; +} + +- (void)_showTapAnimationAtPoint:(CGPoint)point forGesture:(UIGestureRecognizer *)gestureRecognizer +{ + SCTraceODPCompatibleStart(2); + SC_GUARD_ELSE_RETURN([self.containerView isTapGestureRecognizer:gestureRecognizer]) + SCTapAnimationView *tapAnimationView = [SCTapAnimationView tapAnimationView]; + [_containerView addSubview:tapAnimationView]; + tapAnimationView.center = point; + [tapAnimationView showWithCompletion:^(SCTapAnimationView *view) { + [view removeFromSuperview]; + }]; +} + +@end + +@implementation SCFeatureCameraFocusTapCommand +- (void)execute:(CGPoint)pointOfInterest capturer:(id)capturer +{ + [capturer setAutofocusPointOfInterestAsynchronously:pointOfInterest + completionHandler:nil + context:SCCapturerContext]; +} +@end + +@implementation SCFeatureCameraExposureTapCommand +- (void)execute:(CGPoint)pointOfInterest capturer:(id)capturer +{ + [capturer setExposurePointOfInterestAsynchronously:pointOfInterest + fromUser:YES + completionHandler:nil + context:SCCapturerContext]; +} +@end + +@implementation SCFeatureCameraPortraitTapCommand +- (void)execute:(CGPoint)pointOfInterest capturer:(id)capturer +{ + [capturer setPortraitModePointOfInterestAsynchronously:pointOfInterest + completionHandler:nil + context:SCCapturerContext]; +} +@end diff --git a/Features/TapToFocus/SCTapAnimationView.h b/Features/TapToFocus/SCTapAnimationView.h new file mode 100644 index 0000000..4e1a903 --- /dev/null +++ b/Features/TapToFocus/SCTapAnimationView.h @@ -0,0 +1,21 @@ +// +// SCTapAnimationView.h +// SCCamera +// +// Created by Alexander Grytsiuk on 8/26/15. +// Copyright (c) 2015 Snapchat, Inc. All rights reserved. +// + +#import + +@class SCTapAnimationView; + +typedef void (^SCTapAnimationViewCompletion)(SCTapAnimationView *); + +@interface SCTapAnimationView : UIView + ++ (instancetype)tapAnimationView; + +- (void)showWithCompletion:(SCTapAnimationViewCompletion)completion; + +@end diff --git a/Features/TapToFocus/SCTapAnimationView.m b/Features/TapToFocus/SCTapAnimationView.m new file mode 100644 index 0000000..477c496 --- /dev/null +++ b/Features/TapToFocus/SCTapAnimationView.m @@ -0,0 +1,178 @@ +// +// SCTapAnimationView.m +// SCCamera +// +// Created by Alexander Grytsiuk on 8/26/15. +// Copyright (c) 2015 Snapchat, Inc. All rights reserved. +// + +#import "SCTapAnimationView.h" + +#import + +@import QuartzCore; + +static const CGFloat kSCAnimationStep = 0.167; +static const CGFloat kSCInnerCirclePadding = 2.5; +static const CGFloat kSCTapAnimationViewWidth = 55; +static const CGFloat kSCOuterRingBorderWidth = 1; + +static NSString *const kSCOpacityAnimationKey = @"opacity"; +static NSString *const kSCScaleAnimationKey = @"scale"; + +@implementation SCTapAnimationView { + CALayer *_outerRing; + CALayer *_innerCircle; +} + +#pragma mark Class Methods + ++ (instancetype)tapAnimationView +{ + return [[self alloc] initWithFrame:CGRectMake(0, 0, kSCTapAnimationViewWidth, kSCTapAnimationViewWidth)]; +} + +#pragma mark Life Cycle + +- (instancetype)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + if (self) { + self.userInteractionEnabled = NO; + _outerRing = [CALayer layer]; + _outerRing.backgroundColor = [UIColor clearColor].CGColor; + _outerRing.borderColor = [UIColor whiteColor].CGColor; + _outerRing.borderWidth = kSCOuterRingBorderWidth; + _outerRing.shadowColor = [UIColor blackColor].CGColor; + _outerRing.shadowOpacity = 0.4; + _outerRing.shadowOffset = CGSizeMake(0.5, 0.5); + _outerRing.opacity = 0.0; + _outerRing.frame = self.bounds; + _outerRing.cornerRadius = CGRectGetMidX(_outerRing.bounds); + [self.layer addSublayer:_outerRing]; + + _innerCircle = [CALayer layer]; + _innerCircle.backgroundColor = [UIColor whiteColor].CGColor; + _innerCircle.opacity = 0.0; + _innerCircle.frame = CGRectInset(self.bounds, kSCInnerCirclePadding, kSCInnerCirclePadding); + _innerCircle.cornerRadius = CGRectGetMidX(_innerCircle.bounds); + [self.layer addSublayer:_innerCircle]; + } + return self; +} + +#pragma mark Public + +- (void)showWithCompletion:(SCTapAnimationViewCompletion)completion +{ + [_outerRing removeAllAnimations]; + [_innerCircle removeAllAnimations]; + + [CATransaction begin]; + [CATransaction setCompletionBlock:^{ + if (completion) { + completion(self); + } + }]; + [self addOuterRingOpacityAnimation]; + [self addOuterRingScaleAnimation]; + [self addInnerCircleOpacityAnimation]; + [self addInnerCircleScaleAnimation]; + [CATransaction commit]; +} + +#pragma mark Private + +- (CAKeyframeAnimation *)keyFrameAnimationWithKeyPath:(NSString *)keyPath + duration:(CGFloat)duration + values:(NSArray *)values + keyTimes:(NSArray *)keyTimes + timingFunctions:(NSArray *)timingFunctions +{ + CAKeyframeAnimation *keyframeAnimation = [CAKeyframeAnimation animationWithKeyPath:keyPath]; + keyframeAnimation.duration = duration; + keyframeAnimation.values = values; + keyframeAnimation.keyTimes = keyTimes; + keyframeAnimation.timingFunctions = timingFunctions; + keyframeAnimation.fillMode = kCAFillModeForwards; + keyframeAnimation.removedOnCompletion = NO; + + return keyframeAnimation; +} + +- (CABasicAnimation *)animationWithKeyPath:(NSString *)keyPath + duration:(CGFloat)duration + fromValue:(NSValue *)fromValue + toValue:(NSValue *)toValue + timingFunction:(CAMediaTimingFunction *)timingFunction +{ + CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:keyPath]; + animation.duration = duration; + animation.fromValue = fromValue; + animation.toValue = toValue; + animation.timingFunction = timingFunction; + animation.fillMode = kCAFillModeForwards; + animation.removedOnCompletion = NO; + + return animation; +} + +- (void)addOuterRingOpacityAnimation +{ + CAKeyframeAnimation *animation = + [self keyFrameAnimationWithKeyPath:@keypath(_outerRing, opacity) + duration:kSCAnimationStep * 5 + values:@[ @0.0, @1.0, @1.0, @0.0 ] + keyTimes:@[ @0.0, @0.2, @0.8, @1.0 ] + timingFunctions:@[ + [CAMediaTimingFunction functionWithControlPoints:0.0:0.0:0.0:1.0], + [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear], + [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut], + ]]; + [_outerRing addAnimation:animation forKey:kSCOpacityAnimationKey]; +} + +- (void)addOuterRingScaleAnimation +{ + CAKeyframeAnimation *animation = + [self keyFrameAnimationWithKeyPath:@keypath(_innerCircle, transform) + duration:kSCAnimationStep * 3 + values:@[ + [NSValue valueWithCATransform3D:CATransform3DMakeScale(0.50, 0.50, 1.0)], + [NSValue valueWithCATransform3D:CATransform3DIdentity], + [NSValue valueWithCATransform3D:CATransform3DMakeScale(0.83, 0.83, 1.0)], + ] + keyTimes:@[ @0.0, @0.66, @1.0 ] + timingFunctions:@[ + [CAMediaTimingFunction functionWithControlPoints:0.0:0.0:0.0:1.0], + [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut], + ]]; + [_outerRing addAnimation:animation forKey:kSCScaleAnimationKey]; +} + +- (void)addInnerCircleOpacityAnimation +{ + CAKeyframeAnimation *animation = + [self keyFrameAnimationWithKeyPath:@keypath(_innerCircle, opacity) + duration:kSCAnimationStep * 3 + values:@[ @0.0, @0.40, @0.0 ] + keyTimes:@[ @0.0, @0.33, @1.0 ] + timingFunctions:@[ + [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn], + [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut], + ]]; + [_innerCircle addAnimation:animation forKey:kSCOpacityAnimationKey]; +} + +- (void)addInnerCircleScaleAnimation +{ + CABasicAnimation *animation = + [self animationWithKeyPath:@keypath(_innerCircle, transform) + duration:kSCAnimationStep * 2 + fromValue:[NSValue valueWithCATransform3D:CATransform3DMakeScale(0.0, 0.0, 1.0)] + toValue:[NSValue valueWithCATransform3D:CATransform3DIdentity] + timingFunction:[CAMediaTimingFunction functionWithControlPoints:0.0:0.0:0.0:1.0]]; + [_innerCircle addAnimation:animation forKey:kSCScaleAnimationKey]; +} + +@end diff --git a/Features/ToggleCamera/SCFeatureToggleCamera.h b/Features/ToggleCamera/SCFeatureToggleCamera.h new file mode 100644 index 0000000..539d2c9 --- /dev/null +++ b/Features/ToggleCamera/SCFeatureToggleCamera.h @@ -0,0 +1,37 @@ +// +// SCFeatureToggleCamera.h +// SCCamera +// +// Created by Michel Loenngren on 4/17/18. +// + +#import +#import + +@protocol SCCapturer +, SCFeatureToggleCamera, SCLensCameraScreenDataProviderProtocol; + +@protocol SCFeatureToggleCameraDelegate + +- (void)featureToggleCamera:(id)feature + willToggleToDevicePosition:(SCManagedCaptureDevicePosition)devicePosition; +- (void)featureToggleCamera:(id)feature + didToggleToDevicePosition:(SCManagedCaptureDevicePosition)devicePosition; + +@end + +/** + SCFeature protocol for toggling the camera. + */ +@protocol SCFeatureToggleCamera + +@property (nonatomic, weak) id delegate; + +- (void)toggleCameraWithRecording:(BOOL)isRecording + takingPicture:(BOOL)isTakingPicture + lensDataProvider:(id)lensDataProvider + completion:(void (^)(BOOL success))completion; + +- (void)reset; + +@end diff --git a/Features/Zooming/SCFeatureZooming.h b/Features/Zooming/SCFeatureZooming.h new file mode 100644 index 0000000..b7de823 --- /dev/null +++ b/Features/Zooming/SCFeatureZooming.h @@ -0,0 +1,34 @@ +// +// SCFeatureZooming.h +// SCCamera +// +// Created by Xiaokang Liu on 2018/4/19. +// + +#import "SCFeature.h" + +#import +#import + +@class SCPreviewPresenter; +@protocol SCFeatureZooming; + +@protocol SCFeatureZoomingDelegate +- (void)featureZoomingForceTouchedWhileRecording:(id)featureZooming; +- (BOOL)featureZoomingIsInitiatedRecording:(id)featureZooming; +@end + +@protocol SCFeatureZooming +@property (nonatomic, weak) id delegate; +@property (nonatomic, weak) SCPreviewPresenter *previewPresenter; + +- (void)resetOffset; +- (void)resetScale; + +- (void)cancelPreview; +- (void)flipOffset; + +- (void)resetBeginningScale; +- (void)toggleCameraForReset:(SCManagedCaptureDevicePosition)devicePosition; +- (void)recordCurrentZoomStateForReset; +@end diff --git a/Logging/SCCoreCameraLogger.h b/Logging/SCCoreCameraLogger.h new file mode 100644 index 0000000..36944cb --- /dev/null +++ b/Logging/SCCoreCameraLogger.h @@ -0,0 +1,66 @@ +// +// SCCoreCameraLogger.h +// Snapchat +// +// Created by Chao Pang on 3/6/18. +// + +#import + +/** + * CAMERA_CREATION_DELAY event + */ +extern NSString *const kSCCameraCreationDelayEventStartTimeKey; +extern NSString *const kSCCameraCreationDelayEventStartTimeAdjustmentKey; +extern NSString *const kSCCameraCreationDelayEventEndTimeKey; +extern NSString *const kSCCameraCreationDelayEventCaptureSessionIdKey; +extern NSString *const kSCCameraCreationDelayEventFilterLensIdKey; +extern NSString *const kSCCameraCreationDelayEventNightModeDetectedKey; +extern NSString *const kSCCameraCreationDelayEventNightModeActiveKey; +extern NSString *const kSCCameraCreationDelayEventCameraApiKey; +extern NSString *const kSCCameraCreationDelayEventCameraLevelKey; +extern NSString *const kSCCameraCreationDelayEventCameraPositionKey; +extern NSString *const kSCCameraCreationDelayEventCameraOpenSourceKey; +extern NSString *const kSCCameraCreationDelayEventContentDurationKey; +extern NSString *const kSCCameraCreationDelayEventMediaTypeKey; +extern NSString *const kSCCameraCreationDelayEventStartTypeKey; +extern NSString *const kSCCameraCreationDelayEventStartSubTypeKey; +extern NSString *const kSCCameraCreationDelayEventAnalyticsVersion; + +@interface SCCoreCameraLogger : NSObject + ++ (instancetype)sharedInstance; + +/** + * CAMERA_CREATION_DELAY event + */ +- (void)logCameraCreationDelayEventStartWithCaptureSessionId:(NSString *)captureSessionId + filterLensId:(NSString *)filterLensId + underLowLightCondition:(BOOL)underLowLightCondition + isNightModeActive:(BOOL)isNightModeActive + isBackCamera:(BOOL)isBackCamera + isMainCamera:(BOOL)isMainCamera; + +- (void)logCameraCreationDelaySplitPointRecordingGestureFinished; + +- (void)logCameraCreationDelaySplitPointStillImageCaptureApi:(NSString *)api; + +- (void)logCameraCreationDelaySplitPointPreCaptureOperationRequested; + +- (void)logCameraCreationDelaySplitPointPreCaptureOperationFinishedAt:(CFTimeInterval)time; + +- (void)updatedCameraCreationDelayWithContentDuration:(CFTimeInterval)duration; + +- (void)logCameraCreationDelaySplitPointCameraCaptureContentReady; + +- (void)logCameraCreationDelaySplitPointPreviewFinishedPreparation; + +- (void)logCameraCreationDelaySplitPointPreviewDisplayedForImage:(BOOL)isImage; + +- (void)logCameraCreationDelaySplitPointPreviewAnimationComplete:(BOOL)isImage; + +- (void)logCameraCreationDelaySplitPointPreviewFirstFramePlayed:(BOOL)isImage; + +- (void)cancelCameraCreationDelayEvent; + +@end diff --git a/Logging/SCCoreCameraLogger.m b/Logging/SCCoreCameraLogger.m new file mode 100644 index 0000000..2981f65 --- /dev/null +++ b/Logging/SCCoreCameraLogger.m @@ -0,0 +1,304 @@ +// +// SCCoreCameraLogger.m +// Snapchat +// +// Created by Chao Pang on 3/6/18. +// + +#import "SCCoreCameraLogger.h" + +#import +#import +#import +#import + +static const char *kSCCoreCameraLoggerQueueLabel = "com.snapchat.core-camera-logger-queue"; + +NSString *const kSCCameraCreationDelayEventStartTimeKey = @"start_time"; +NSString *const kSCCameraCreationDelayEventStartTimeAdjustmentKey = @"start_time_adjustment"; +NSString *const kSCCameraCreationDelayEventEndTimeKey = @"end_time"; +NSString *const kSCCameraCreationDelayEventCaptureSessionIdKey = @"capture_session_id"; +NSString *const kSCCameraCreationDelayEventFilterLensIdKey = @"filter_lens_id"; +NSString *const kSCCameraCreationDelayEventNightModeDetectedKey = @"night_mode_detected"; +NSString *const kSCCameraCreationDelayEventNightModeActiveKey = @"night_mode_active"; +NSString *const kSCCameraCreationDelayEventCameraApiKey = @"camera_api"; +NSString *const kSCCameraCreationDelayEventCameraLevelKey = @"camera_level"; +NSString *const kSCCameraCreationDelayEventCameraPositionKey = @"camera_position"; +NSString *const kSCCameraCreationDelayEventCameraOpenSourceKey = @"camera_open_source"; +NSString *const kSCCameraCreationDelayEventContentDurationKey = @"content_duration"; +NSString *const kSCCameraCreationDelayEventMediaTypeKey = @"media_type"; +NSString *const kSCCameraCreationDelayEventStartTypeKey = @"start_type"; +NSString *const kSCCameraCreationDelayEventStartSubTypeKey = @"start_sub_type"; +NSString *const kSCCameraCreationDelayEventAnalyticsVersion = @"ios_v1"; + +static inline NSUInteger SCTimeToMS(CFTimeInterval time) +{ + return (NSUInteger)(time * 1000); +} + +static NSString *SCDictionaryToJSONString(NSDictionary *dictionary) +{ + NSData *dictData = [NSJSONSerialization dataWithJSONObject:dictionary options:0 error:nil]; + return [[NSString alloc] initWithData:dictData encoding:NSUTF8StringEncoding]; +} + +@implementation SCCoreCameraLogger { + SCQueuePerformer *_performer; + NSMutableDictionary *_cameraCreationDelayParameters; + NSMutableDictionary *_cameraCreationDelaySplits; +} + +- (instancetype)init +{ + self = [super init]; + if (self) { + _cameraCreationDelayParameters = [NSMutableDictionary dictionary]; + _cameraCreationDelaySplits = [NSMutableDictionary dictionary]; + _performer = [[SCQueuePerformer alloc] initWithLabel:kSCCoreCameraLoggerQueueLabel + qualityOfService:QOS_CLASS_UNSPECIFIED + queueType:DISPATCH_QUEUE_SERIAL + context:SCQueuePerformerContextCoreCamera]; + } + return self; +} + ++ (instancetype)sharedInstance +{ + static SCCoreCameraLogger *sharedInstance; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sharedInstance = [[SCCoreCameraLogger alloc] init]; + }); + return sharedInstance; +} + +// Camera creation delay metrics + +- (void)logCameraCreationDelayEventStartWithCaptureSessionId:(NSString *)captureSessionId + filterLensId:(NSString *)filterLensId + underLowLightCondition:(BOOL)underLowLightCondition + isNightModeActive:(BOOL)isNightModeActive + isBackCamera:(BOOL)isBackCamera + isMainCamera:(BOOL)isMainCamera +{ + CFTimeInterval startTime = CACurrentMediaTime(); + [_performer perform:^{ + [_cameraCreationDelayParameters removeAllObjects]; + [_cameraCreationDelaySplits removeAllObjects]; + _cameraCreationDelayParameters[kSCCameraCreationDelayEventStartTimeKey] = @(startTime); + _cameraCreationDelayParameters[kSCCameraCreationDelayEventCaptureSessionIdKey] = captureSessionId ?: @"null"; + _cameraCreationDelayParameters[kSCCameraCreationDelayEventFilterLensIdKey] = filterLensId ?: @"null"; + _cameraCreationDelayParameters[kSCCameraCreationDelayEventNightModeDetectedKey] = @(underLowLightCondition); + _cameraCreationDelayParameters[kSCCameraCreationDelayEventNightModeActiveKey] = @(isNightModeActive); + _cameraCreationDelayParameters[kSCCameraCreationDelayEventCameraPositionKey] = + isBackCamera ? @"back" : @"front"; + _cameraCreationDelayParameters[kSCCameraCreationDelayEventCameraOpenSourceKey] = + isMainCamera ? @"main_camera" : @"reply_camera"; + _cameraCreationDelayParameters[kSCCameraCreationDelayEventStartTypeKey] = SCLaunchType() ?: @"null"; + _cameraCreationDelayParameters[kSCCameraCreationDelayEventStartSubTypeKey] = SCLaunchSubType() ?: @"null"; + }]; +} + +- (void)logCameraCreationDelaySplitPointRecordingGestureFinished +{ + CFTimeInterval time = CACurrentMediaTime(); + [_performer perform:^{ + CFTimeInterval endRecordingTimeOffset = + time - [_cameraCreationDelayParameters[kSCCameraCreationDelayEventStartTimeKey] doubleValue]; + NSNumber *recordStartTimeMillis = + (NSNumber *)_cameraCreationDelaySplits[kSCCameraSubmetricsPreCaptureOperationFinished]; + if (recordStartTimeMillis) { + CFTimeInterval timeDisplacement = ([recordStartTimeMillis doubleValue] / 1000.0) - endRecordingTimeOffset; + _cameraCreationDelayParameters[kSCCameraCreationDelayEventStartTimeAdjustmentKey] = @(timeDisplacement); + } + [self _addSplitPointForKey:kSCCameraSubmetricsRecordingGestureFinished atTime:time]; + }]; +} + +- (void)logCameraCreationDelaySplitPointStillImageCaptureApi:(NSString *)api +{ + CFTimeInterval time = CACurrentMediaTime(); + [_performer perform:^{ + if (api) { + _cameraCreationDelayParameters[kSCCameraCreationDelayEventCameraApiKey] = api; + } + [self _addSplitPointForKey:kSCCameraSubmetricsPreCaptureOperationRequested atTime:time]; + }]; +} + +- (void)logCameraCreationDelaySplitPointPreCaptureOperationRequested +{ + CFTimeInterval time = CACurrentMediaTime(); + [_performer perform:^{ + [self _addSplitPointForKey:kSCCameraSubmetricsPreCaptureOperationRequested atTime:time]; + }]; +} + +- (void)logCameraCreationDelaySplitPointPreCaptureOperationFinishedAt:(CFTimeInterval)time +{ + [_performer perform:^{ + [self _addSplitPointForKey:kSCCameraSubmetricsPreCaptureOperationFinished atTime:time]; + }]; +} + +- (void)updatedCameraCreationDelayWithContentDuration:(CFTimeInterval)duration +{ + [_performer perform:^{ + _cameraCreationDelayParameters[kSCCameraCreationDelayEventContentDurationKey] = @(SCTimeToMS(duration)); + }]; +} + +- (void)logCameraCreationDelaySplitPointCameraCaptureContentReady +{ + CFTimeInterval time = CACurrentMediaTime(); + [_performer perform:^{ + [self _addSplitPointForKey:kSCCameraSubmetricsCameraCaptureContentReady atTime:time]; + }]; +} + +- (void)logCameraCreationDelaySplitPointPreviewFinishedPreparation +{ + CFTimeInterval time = CACurrentMediaTime(); + [_performer perform:^{ + [self _addSplitPointForKey:kSCCameraSubmetricsCameraCaptureContentReady atTime:time]; + }]; +} + +- (void)logCameraCreationDelaySplitPointPreviewDisplayedForImage:(BOOL)isImage +{ + CFTimeInterval time = CACurrentMediaTime(); + [_performer perform:^{ + [self _addSplitPointForKey:kSCCameraSubmetricsPreviewLayoutReady atTime:time]; + }]; +} + +- (void)logCameraCreationDelaySplitPointPreviewAnimationComplete:(BOOL)isImage +{ + CFTimeInterval time = CACurrentMediaTime(); + [_performer perform:^{ + [self _addSplitPointForKey:kSCCameraSubmetricsPreviewAnimationFinish atTime:time]; + if (_cameraCreationDelaySplits[kSCCameraSubmetricsPreviewPlayerReady]) { + [self _completeLogCameraCreationDelayEventWithIsImage:isImage atTime:time]; + } + }]; +} + +- (void)logCameraCreationDelaySplitPointPreviewFirstFramePlayed:(BOOL)isImage +{ + CFTimeInterval time = CACurrentMediaTime(); + [_performer perform:^{ + [self _addSplitPointForKey:kSCCameraSubmetricsPreviewPlayerReady atTime:time]; + if (_cameraCreationDelaySplits[kSCCameraSubmetricsPreviewAnimationFinish]) { + [self _completeLogCameraCreationDelayEventWithIsImage:isImage atTime:time]; + } + }]; +} + +- (void)cancelCameraCreationDelayEvent +{ + [_performer perform:^{ + [_cameraCreationDelayParameters removeAllObjects]; + [_cameraCreationDelaySplits removeAllObjects]; + }]; +} + +#pragma - Private methods + +- (void)_completeLogCameraCreationDelayEventWithIsImage:(BOOL)isImage atTime:(CFTimeInterval)time +{ + SCAssertPerformer(_performer); + if (_cameraCreationDelayParameters[kSCCameraCreationDelayEventCaptureSessionIdKey]) { + _cameraCreationDelayParameters[kSCCameraCreationDelayEventMediaTypeKey] = isImage ? @"image" : @"video"; + _cameraCreationDelayParameters[kSCCameraCreationDelayEventEndTimeKey] = @(time); + [self _logCameraCreationDelayBlizzardEvent]; + } + [_cameraCreationDelayParameters removeAllObjects]; + [_cameraCreationDelaySplits removeAllObjects]; +} + +- (void)_addSplitPointForKey:(NSString *)key atTime:(CFTimeInterval)time +{ + SCAssertPerformer(_performer); + if (key) { + CFTimeInterval timeOffset = + time - [_cameraCreationDelayParameters[kSCCameraCreationDelayEventStartTimeKey] doubleValue]; + NSNumber *timeAdjustment = + _cameraCreationDelayParameters[kSCCameraCreationDelayEventStartTimeAdjustmentKey] ?: @(0); + _cameraCreationDelaySplits[key] = @(SCTimeToMS(timeOffset + [timeAdjustment doubleValue])); + } +} + +- (void)_logCameraCreationDelayBlizzardEvent +{ + SCAssertPerformer(_performer); + SCASharedCameraMetricParams *sharedCameraMetricsParams = [[SCASharedCameraMetricParams alloc] init]; + [sharedCameraMetricsParams setAnalyticsVersion:kSCCameraCreationDelayEventAnalyticsVersion]; + NSString *mediaType = _cameraCreationDelayParameters[kSCCameraCreationDelayEventMediaTypeKey]; + if (mediaType) { + if ([mediaType isEqualToString:@"image"]) { + [sharedCameraMetricsParams setMediaType:SCAMediaType_IMAGE]; + } else if ([mediaType isEqualToString:@"video"]) { + [sharedCameraMetricsParams setMediaType:SCAMediaType_VIDEO]; + } + } + if (_cameraCreationDelayParameters[kSCCameraCreationDelayEventNightModeDetectedKey] && + _cameraCreationDelayParameters[kSCCameraCreationDelayEventNightModeActiveKey]) { + BOOL isNightModeDetected = + [_cameraCreationDelayParameters[kSCCameraCreationDelayEventNightModeDetectedKey] boolValue]; + BOOL isNightModeActive = + [_cameraCreationDelayParameters[kSCCameraCreationDelayEventNightModeActiveKey] boolValue]; + if (!isNightModeDetected) { + [sharedCameraMetricsParams setLowLightStatus:SCALowLightStatus_NOT_DETECTED]; + } else if (!isNightModeActive) { + [sharedCameraMetricsParams setLowLightStatus:SCALowLightStatus_DETECTED]; + } else if (isNightModeActive) { + [sharedCameraMetricsParams setLowLightStatus:SCALowLightStatus_ENABLED]; + } + } + + [sharedCameraMetricsParams setPowerMode:[[NSProcessInfo processInfo] isLowPowerModeEnabled] + ? @"LOW_POWER_MODE_ENABLED" + : @"LOW_POWER_MODE_DISABLED"]; + [sharedCameraMetricsParams + setFilterLensId:_cameraCreationDelayParameters[kSCCameraCreationDelayEventFilterLensIdKey] ?: @"null"]; + [sharedCameraMetricsParams + setCaptureSessionId:_cameraCreationDelayParameters[kSCCameraCreationDelayEventCaptureSessionIdKey] ?: @"null"]; + [sharedCameraMetricsParams + setCameraApi:_cameraCreationDelayParameters[kSCCameraCreationDelayEventCameraApiKey] ?: @"null"]; + [sharedCameraMetricsParams + setCameraPosition:_cameraCreationDelayParameters[kSCCameraCreationDelayEventCameraPositionKey] ?: @"null"]; + [sharedCameraMetricsParams + setCameraOpenSource:_cameraCreationDelayParameters[kSCCameraCreationDelayEventCameraOpenSourceKey] ?: @"null"]; + [sharedCameraMetricsParams + setCameraLevel:_cameraCreationDelayParameters[kSCCameraCreationDelayEventCameraLevelKey] ?: @"null"]; + [sharedCameraMetricsParams + setStartType:_cameraCreationDelayParameters[kSCCameraCreationDelayEventStartTypeKey] ?: @"null"]; + [sharedCameraMetricsParams + setStartSubType:_cameraCreationDelayParameters[kSCCameraCreationDelayEventStartSubTypeKey] ?: @"null"]; + [sharedCameraMetricsParams setSplits:SCDictionaryToJSONString(_cameraCreationDelaySplits)]; + + SCACameraSnapCreateDelay *creationDelay = [[SCACameraSnapCreateDelay alloc] init]; + if (_cameraCreationDelayParameters[kSCCameraCreationDelayEventStartTimeKey] && + _cameraCreationDelayParameters[kSCCameraCreationDelayEventEndTimeKey]) { + double startTime = [_cameraCreationDelayParameters[kSCCameraCreationDelayEventStartTimeKey] doubleValue]; + double endTime = [_cameraCreationDelayParameters[kSCCameraCreationDelayEventEndTimeKey] doubleValue]; + NSNumber *timeAdjustment = + _cameraCreationDelayParameters[kSCCameraCreationDelayEventStartTimeAdjustmentKey] ?: @(0); + [creationDelay setLatencyMillis:SCTimeToMS(endTime - startTime + [timeAdjustment doubleValue])]; + } else { + [creationDelay setLatencyMillis:0]; + } + + if (_cameraCreationDelayParameters[kSCCameraCreationDelayEventContentDurationKey]) { + [creationDelay + setContentDurationMillis:SCTimeToMS( + [_cameraCreationDelayParameters[kSCCameraCreationDelayEventContentDurationKey] + doubleValue])]; + } else { + [creationDelay setContentDurationMillis:0]; + } + [creationDelay setSharedCameraMetricParams:sharedCameraMetricsParams]; + [[SCLogger sharedInstance] logUserTrackedEvent:creationDelay]; +} + +@end diff --git a/Logging/SCLogger+Camera.h b/Logging/SCLogger+Camera.h new file mode 100644 index 0000000..e55c3b3 --- /dev/null +++ b/Logging/SCLogger+Camera.h @@ -0,0 +1,53 @@ +// +// SCLogger+Camera.h +// Snapchat +// +// Created by Derek Peirce on 5/8/17. +// Copyright © 2017 Snapchat, Inc. All rights reserved. +// + +#import "AVCameraViewEnums.h" + +#import +#import + +#import + +typedef NS_ENUM(NSUInteger, CameraCreationDelayLoggingStatus) { + CAMERA_CREATION_DELAY_LOGGING_START, + CAMERA_CREATION_DELAY_LOGGINT_LAST_STEP, + CAMERA_CREATION_DELAY_LOGGING_END, +}; + +@interface SCLogger (Camera) + +@property (nonatomic, strong) NSNumber *cameraCreationDelayLoggingStatus; + +- (void)logCameraCreationStartWithMethod:(SCCameraRecordingMethod)method + lensesEnabled:(BOOL)lensesEnabled + activeLensId:(NSString *)activeLensId + captureSessionId:(NSString *)captureSessionId; +- (void)logStillImageCaptureApi:(NSString *)api; +- (void)logPreCaptureOperationRequestedAt:(CFTimeInterval)requestTime; +- (void)logPreCaptureOperationFinishedAt:(CFTimeInterval)time; +- (void)logCameraCaptureRecordingGestureFinishedAtTime:(CFTimeInterval)endRecordingTime; +- (void)logCameraCaptureFinishedWithDuration:(CFTimeInterval)duration; +- (void)logCameraCaptureContentReady; +- (void)logPreviewFinishedPreparation; +- (void)logPreviewDisplayedForImage:(BOOL)isImage; +- (void)logPreviewAnimationComplete:(BOOL)isImage; +- (void)logPreviewFirstFramePlayed:(BOOL)isImage; +- (void)cancelCameraCreationEvent; + +- (void)logRecordingMayBeTooShortWithMethod:(SCCameraRecordingMethod)method; +- (void)logRecordingWasTooShortWithFirstFrame:(CMTime)firstFrame + frontFacingCamera:(BOOL)isFrontFacing + cameraFlips:(NSInteger)cameraFlips; + +- (void)logManagedCapturerSettingFailure:(NSString *)settingTask error:(NSError *)error; +- (void)logCameraExposureAdjustmentDelayStart; +- (void)logCameraExposureAdjustmentDelayEndWithStrategy:(NSString *)strategy; +- (void)logCameraCreationDelaySubMetricsStartWithSignCode:(kSCSignPostCodeEnum)signPostCode; +- (void)logCameraCreationDelaySubMetricsEndWithSignCode:(kSCSignPostCodeEnum)signPostCod; + +@end diff --git a/Logging/SCLogger+Camera.m b/Logging/SCLogger+Camera.m new file mode 100644 index 0000000..ea2d1df --- /dev/null +++ b/Logging/SCLogger+Camera.m @@ -0,0 +1,303 @@ +// +// SCLogger+Camera.m +// Snapchat +// +// Created by Derek Peirce on 5/8/17. +// Copyright © 2017 Snapchat, Inc. All rights reserved. +// + +#import "SCLogger+Camera.h" + +#import "SCCameraTweaks.h" + +#import +#import +#import +#import +#import + +#import + +@implementation SCLogger (Camera) + +@dynamic cameraCreationDelayLoggingStatus; + +- (NSNumber *)cameraCreationDelayLoggingStatus +{ + return objc_getAssociatedObject(self, @selector(cameraCreationDelayLoggingStatus)); +} + +- (void)setCameraCreationDelayLoggingStatus:(NSNumber *)status +{ + objc_setAssociatedObject(self, @selector(cameraCreationDelayLoggingStatus), status, + OBJC_ASSOCIATION_RETAIN_NONATOMIC); +} + +- (BOOL)shouldLogCameraCreationDelay +{ + return [[self cameraCreationDelayLoggingStatus] intValue] != CAMERA_CREATION_DELAY_LOGGING_END; +} + +- (void)logCameraCreationDelayEnd +{ + if ([[self cameraCreationDelayLoggingStatus] intValue] == CAMERA_CREATION_DELAY_LOGGINT_LAST_STEP) { + SCTraceSignPostEndForMetrics(kSCSignPostCameraCreationDelay, 0, 0, 0, 0); + [self setCameraCreationDelayLoggingStatus:@(CAMERA_CREATION_DELAY_LOGGING_END)]; + } else { + [self setCameraCreationDelayLoggingStatus:@(CAMERA_CREATION_DELAY_LOGGINT_LAST_STEP)]; + } +} + +- (void)logCameraCreationStartWithMethod:(SCCameraRecordingMethod)method + lensesEnabled:(BOOL)lensesEnabled + activeLensId:(NSString *)activeLensId + captureSessionId:(NSString *)captureSessionId +{ + NSMutableDictionary *parameters = [@{ + @"lens_ui_enabled" : @(lensesEnabled), + @"analytics_version" : kSCCameraDelayEventVersion, + @"method" : @(method), + } mutableCopy]; + if (lensesEnabled && activeLensId) { + [parameters setObject:activeLensId forKey:@"lens_id"]; + } + if (captureSessionId) { + [parameters setObject:captureSessionId forKey:@"capture_session_id"]; + } + [self setCameraCreationDelayLoggingStatus:@(CAMERA_CREATION_DELAY_LOGGING_START)]; + [self logCameraCreationDelaySubMetricsStartWithSignCode:kSCSignPostCameraCreationDelay]; + [self logCameraCreationDelaySubMetricsStartWithSignCode:kSCSignPostCameraRecordingGestureFinished]; + [self logCameraCreationDelaySubMetricsStartWithSignCode:kSCSignPostCameraPreCaptureOperationRequested]; + [[SCLogger sharedInstance] logTimedEventStart:kSCCameraCaptureDelayEvent + uniqueId:@"" + isUniqueEvent:NO + parameters:parameters + shouldLogStartTime:YES]; +} + +- (void)logCameraExposureAdjustmentDelayStart +{ + [[SCLogger sharedInstance] logTimedEventStart:kSCCameraExposureAdjustmentDelay + uniqueId:@"" + isUniqueEvent:NO + parameters:nil + shouldLogStartTime:YES]; +} + +- (void)logCameraExposureAdjustmentDelayEndWithStrategy:(NSString *)strategy +{ + [[SCLogger sharedInstance] logTimedEventEnd:kSCCameraExposureAdjustmentDelay + uniqueId:@"" + parameters:@{ + @"strategy" : strategy + }]; +} + +- (void)logCameraCaptureRecordingGestureFinishedAtTime:(CFTimeInterval)endRecordingTime +{ + [self logCameraCreationDelaySubMetricsEndWithSignCode:kSCSignPostCameraRecordingGestureFinished]; + [[SCLogger sharedInstance] + updateLogTimedEvent:kSCCameraCaptureDelayEvent + uniqueId:@"" + update:^(NSMutableDictionary *startParameters) { + NSMutableDictionary *eventParameters = + startParameters[SCPerformanceMetricsKey.kSCLoggerStartEventParametersKey]; + NSNumber *recordStartTime = + (NSNumber *)eventParameters[kSCCameraSubmetricsPreCaptureOperationFinished]; + CFTimeInterval endRecordingTimeOffset = + endRecordingTime - + [startParameters[SCPerformanceMetricsKey.kSCLoggerStartEventTimeKey] doubleValue]; + if (recordStartTime) { + CFTimeInterval timeDisplacement = + ([recordStartTime doubleValue] / 1000.0) - endRecordingTimeOffset; + [eventParameters setObject:@(timeDisplacement) + forKey:SCPerformanceMetricsKey.kSCLoggerStartEventTimeAdjustmentKey]; + } + [self addSplitPoint:kSCCameraSubmetricsRecordingGestureFinished + atTime:endRecordingTime + toEvent:startParameters]; + }]; +} + +- (void)logStillImageCaptureApi:(NSString *)api +{ + [self logCameraCreationDelaySubMetricsEndWithSignCode:kSCSignPostCameraPreCaptureOperationRequested]; + [self logCameraCreationDelaySubMetricsStartWithSignCode:kSCSignPostCameraPreCaptureOperationFinished]; + [self logCameraCreationDelaySubMetricsStartWithSignCode:kSCSignPostCameraCaptureContentReady]; + CFTimeInterval requestTime = CACurrentMediaTime(); + [self updateLogTimedEvent:kSCCameraCaptureDelayEvent + uniqueId:@"" + update:^(NSMutableDictionary *startParameters) { + NSMutableDictionary *eventParameters = + startParameters[SCPerformanceMetricsKey.kSCLoggerStartEventParametersKey]; + [eventParameters setObject:api forKey:@"api_type"]; + [eventParameters setObject:@(1) forKey:@"camera_api_level"]; + [self addSplitPoint:@"PRE_CAPTURE_OPERATION_REQUESTED" + atTime:requestTime + toEvent:startParameters]; + }]; +} + +- (void)logPreCaptureOperationRequestedAt:(CFTimeInterval)requestTime +{ + [self logCameraCreationDelaySubMetricsEndWithSignCode:kSCSignPostCameraPreCaptureOperationRequested]; + [self logCameraCreationDelaySubMetricsStartWithSignCode:kSCSignPostCameraPreCaptureOperationFinished]; + [self logCameraCreationDelaySubMetricsStartWithSignCode:kSCSignPostCameraCaptureContentReady]; + [self updateLogTimedEvent:kSCCameraCaptureDelayEvent + uniqueId:@"" + splitPoint:kSCCameraSubmetricsPreCaptureOperationRequested + time:requestTime]; +} + +- (void)logPreCaptureOperationFinishedAt:(CFTimeInterval)time +{ + [self logCameraCreationDelaySubMetricsEndWithSignCode:kSCSignPostCameraPreCaptureOperationFinished]; + [self logCameraCreationDelaySubMetricsStartWithSignCode:kSCSignPostCameraPreviewPlayerReady]; + [self updateLogTimedEvent:kSCCameraCaptureDelayEvent + uniqueId:@"" + splitPoint:kSCCameraSubmetricsPreCaptureOperationFinished + time:time]; +} + +- (void)logCameraCaptureFinishedWithDuration:(CFTimeInterval)duration +{ + [[SCLogger sharedInstance] + updateLogTimedEvent:kSCCameraCaptureDelayEvent + uniqueId:@"" + update:^(NSMutableDictionary *startParameters) { + NSMutableDictionary *eventParameters = + startParameters[SCPerformanceMetricsKey.kSCLoggerStartEventParametersKey]; + [eventParameters setObject:@(SCTimeInMillisecond(duration)) forKey:@"content_duration"]; + }]; +} + +- (void)logCameraCaptureContentReady +{ + [self logCameraCreationDelaySubMetricsEndWithSignCode:kSCSignPostCameraCaptureContentReady]; + [[SCLogger sharedInstance] updateLogTimedEvent:kSCCameraCaptureDelayEvent + uniqueId:@"" + splitPoint:kSCCameraSubmetricsCameraCaptureContentReady]; +} + +- (void)logPreviewFinishedPreparation +{ + [self logCameraCreationDelaySubMetricsEndWithSignCode:kSCSignPostCameraPreviewFinishPreparation]; + [self logCameraCreationDelaySubMetricsStartWithSignCode:kSCSignPostCameraPreviewAnimationFinish]; + [self updateLogTimedEvent:kSCCameraCaptureDelayEvent + uniqueId:@"" + splitPoint:kSCCameraSubmetricsPreviewFinishPreparation]; +} + +- (void)logPreviewDisplayedForImage:(BOOL)isImage +{ + [self logCameraCreationDelaySubMetricsEndWithSignCode:kSCSignPostCameraPreviewLayoutReady]; + [self updateLogTimedEvent:kSCCameraCaptureDelayEvent uniqueId:@"" splitPoint:kSCCameraSubmetricsPreviewLayoutReady]; +} + +- (void)logPreviewAnimationComplete:(BOOL)isImage +{ + [self updateLogTimedEvent:kSCCameraCaptureDelayEvent + uniqueId:@"" + splitPoint:kSCCameraSubmetricsPreviewAnimationFinish]; + [self logCameraCreationDelaySubMetricsEndWithSignCode:kSCSignPostCameraPreviewAnimationFinish]; + [self logCameraCreationDelayEnd]; + [self conditionallyLogTimedEventEnd:kSCCameraCaptureDelayEvent + uniqueId:@"" + parameters:@{ + @"type" : isImage ? @"image" : @"video", + } + shouldLog:^BOOL(NSDictionary *startParameters) { + // For video, PREVIEW_PLAYER_READY and PREVIEW_ANIMATION_FINISH can happen in either + // order. So here we check for existence of this key, and end timer if the other + // event have happened. + NSMutableDictionary *eventParameters = + startParameters[SCPerformanceMetricsKey.kSCLoggerStartEventParametersKey]; + return eventParameters[kSCCameraSubmetricsPreviewPlayerReady] != nil; + }]; +} + +- (void)logPreviewFirstFramePlayed:(BOOL)isImage +{ + [self updateLogTimedEvent:kSCCameraCaptureDelayEvent uniqueId:@"" splitPoint:kSCCameraSubmetricsPreviewPlayerReady]; + [self logCameraCreationDelaySubMetricsEndWithSignCode:kSCSignPostCameraPreviewPlayerReady]; + [self logCameraCreationDelayEnd]; + [self conditionallyLogTimedEventEnd:kSCCameraCaptureDelayEvent + uniqueId:@"" + parameters:@{ + @"type" : isImage ? @"image" : @"video", + } + shouldLog:^BOOL(NSDictionary *startParameters) { + NSMutableDictionary *eventParameters = + startParameters[SCPerformanceMetricsKey.kSCLoggerStartEventParametersKey]; + // See the comment above for PREVIEW_PLAYER_READY and PREVIEW_ANIMATION_FINISH. + return eventParameters[kSCCameraSubmetricsPreviewAnimationFinish] != nil; + }]; +} + +- (void)cancelCameraCreationEvent +{ + [self cancelLogTimedEvent:kSCCameraCaptureDelayEvent uniqueId:@""]; +} + +- (void)logRecordingMayBeTooShortWithMethod:(SCCameraRecordingMethod)method +{ + [[SCLogger sharedInstance] cancelLogTimedEvent:kSCCameraMetricsRecordingTooShort uniqueId:@""]; + [[SCLogger sharedInstance] logTimedEventStart:kSCCameraMetricsRecordingTooShort + uniqueId:@"" + isUniqueEvent:NO + parameters:@{ + @"method" : @(method), + @"analytics_version" : kSCCameraRecordingTooShortVersion, + } + shouldLogStartTime:YES]; +} + +- (void)logRecordingWasTooShortWithFirstFrame:(CMTime)firstFrame + frontFacingCamera:(BOOL)isFrontFacing + cameraFlips:(NSInteger)cameraFlips +{ + [self logTimedEventEnd:kSCCameraMetricsRecordingTooShort + uniqueId:@"" + update:^(NSDictionary *startParameters, CFTimeInterval eventEndTime, CFTimeInterval adjustedTime) { + NSMutableDictionary *eventParameters = + startParameters[SCPerformanceMetricsKey.kSCLoggerStartEventParametersKey]; + if (CMTIME_IS_VALID(firstFrame)) { + CFTimeInterval startTime = + [startParameters[SCPerformanceMetricsKey.kSCLoggerStartEventTimeKey] doubleValue]; + CFTimeInterval firstFrameRelative = CMTimeGetSeconds(firstFrame) - startTime; + [eventParameters setObject:@(firstFrameRelative) forKey:@"first_frame_s"]; + } + [eventParameters setObject:@(isFrontFacing) forKey:@"is_front_facing"]; + if (cameraFlips) { + [eventParameters setObject:@(cameraFlips > 0) forKey:@"has_camera_been_flipped"]; + } + }]; +} + +- (void)logManagedCapturerSettingFailure:(NSString *)settingTask error:(NSError *)error +{ + NSMutableDictionary *parameters = [[NSMutableDictionary alloc] init]; + parameters[@"setting_task"] = settingTask; + if (error) { + parameters[@"setting error"] = error; + } + [[SCLogger sharedInstance] logTimedEventEnd:kSCCameraManagedCaptureSettingFailure + uniqueId:@"" + parameters:parameters]; +} + +- (void)logCameraCreationDelaySubMetricsStartWithSignCode:(kSCSignPostCodeEnum)signPostCode +{ + if ([self shouldLogCameraCreationDelay]) { + SCTraceSignPostStartForMetrics(signPostCode, 0, 0, 0, 0); + } +} + +- (void)logCameraCreationDelaySubMetricsEndWithSignCode:(kSCSignPostCodeEnum)signPostCode +{ + if ([self shouldLogCameraCreationDelay]) { + SCTraceSignPostEndForMetrics(signPostCode, 0, 0, 0, 0); + } +} + +@end diff --git a/Logging/SCManiphestTicketCreator.h b/Logging/SCManiphestTicketCreator.h new file mode 100644 index 0000000..717ff28 --- /dev/null +++ b/Logging/SCManiphestTicketCreator.h @@ -0,0 +1,24 @@ +// +// SCManiphestTicketCreator.h +// SCCamera +// +// Created by Michel Loenngren on 4/16/18. +// + +#import + +/** + Protocol for filing jira tickets and beta s2r. + */ +@protocol SCManiphestTicketCreator + +- (void)createAndFile:(NSData *)image + creationTime:(long)reportCreationTime + description:(NSString *)bugDescription + email:(NSString *)otherEmail + project:(NSString *)projectName + subproject:(NSString *)subprojectName; + +- (void)createAndFileBetaReport:(NSString *)msg; + +@end