From b198ab4aa9615920e48ac8111338ed4ccae4cb3b Mon Sep 17 00:00:00 2001 From: Bell App Lab Date: Fri, 20 Nov 2015 12:26:03 +0000 Subject: [PATCH 1/5] Beautified `_setupCaptureSession` --- FastttCamera/FastttCamera.m | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/FastttCamera/FastttCamera.m b/FastttCamera/FastttCamera.m index 75a2213..765058d 100644 --- a/FastttCamera/FastttCamera.m +++ b/FastttCamera/FastttCamera.m @@ -393,13 +393,10 @@ - (void)_setupCaptureSession return; } -#if !TARGET_IPHONE_SIMULATOR [self _checkDeviceAuthorizationWithCompletion:^(BOOL isAuthorized) { _deviceAuthorized = isAuthorized; -#else - _deviceAuthorized = YES; -#endif + if (!_deviceAuthorized && [self.delegate respondsToSelector:@selector(userDeniedCameraPermissionsForCameraController:)]) { dispatch_async(dispatch_get_main_queue(), ^{ [self.delegate userDeniedCameraPermissionsForCameraController:self]; @@ -466,9 +463,7 @@ - (void)_setupCaptureSession } }); } -#if !TARGET_IPHONE_SIMULATOR }]; -#endif } - (void)_teardownCaptureSession @@ -698,11 +693,17 @@ + (AVCaptureVideoOrientation)_videoOrientationForDeviceOrientation:(UIDeviceOrie - (void)_checkDeviceAuthorizationWithCompletion:(void (^)(BOOL isAuthorized))completion { +#if TARGET_IPHONE_SIMULATOR + if (completion) { + completion(YES); + } +#else [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) { if (completion) { completion(granted); } }]; +#endif } #pragma mark - FastttCameraDevice From 5e5068ebc76a37ef0ab06536d70d08846d09c7b4 Mon Sep 17 00:00:00 2001 From: Bell App Lab Date: Fri, 20 Nov 2015 12:29:44 +0000 Subject: [PATCH 2/5] Added safety check to `exposureMode` --- FastttCamera/FastttCamera.m | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/FastttCamera/FastttCamera.m b/FastttCamera/FastttCamera.m index 765058d..c62164f 100644 --- a/FastttCamera/FastttCamera.m +++ b/FastttCamera/FastttCamera.m @@ -421,7 +421,9 @@ - (void)_setupCaptureSession device.focusMode = AVCaptureFocusModeContinuousAutoFocus; } - device.exposureMode = AVCaptureExposureModeContinuousAutoExposure; + if ([device isExposureModeSupported:AVCaptureExposureModeContinuousAutoExposure]) { + device.exposureMode = AVCaptureExposureModeContinuousAutoExposure; + } [device unlockForConfiguration]; } From 733a4f4f6bbd149916e16300159e7864ef3ca7f7 Mon Sep 17 00:00:00 2001 From: Bell App Lab Date: Fri, 20 Nov 2015 12:59:41 +0000 Subject: [PATCH 3/5] Moved a couple of methods to a private serial `NSOperationQueue`, mainly to avoid calling `[_session startRunning]` on the main thread. --- FastttCamera/FastttCamera.m | 160 +++++++++++++++++++++++++----------- 1 file changed, 112 insertions(+), 48 deletions(-) diff --git a/FastttCamera/FastttCamera.m b/FastttCamera/FastttCamera.m index c62164f..6d52ab8 100644 --- a/FastttCamera/FastttCamera.m +++ b/FastttCamera/FastttCamera.m @@ -27,6 +27,14 @@ @interface FastttCamera () @property (nonatomic, assign) BOOL deviceAuthorized; @property (nonatomic, assign) BOOL isCapturingImage; +//Background +@property (nonatomic, strong) NSOperationQueue *queue; +- (void)enqueue:(void(^)())block; +- (void)toMainThread:(void(^)())block; +@property (nonatomic, assign) UIBackgroundTaskIdentifier bgTaskId; +- (void)startBackgroundTask; +- (void)endBackgroundTask; + @end @implementation FastttCamera @@ -53,6 +61,7 @@ @implementation FastttCamera - (instancetype)init { if ((self = [super init])) { + _bgTaskId = UIBackgroundTaskInvalid; [self _setupCaptureSession]; @@ -106,6 +115,50 @@ - (void)dealloc [[NSNotificationCenter defaultCenter] removeObserver:self]; } +#pragma mark - Background + +- (NSOperationQueue *)queue +{ + if (!_queue) { + _queue = [NSOperationQueue new]; + _queue.name = @"FastttQueue"; + _queue.maxConcurrentOperationCount = 1; + } + return _queue; +} + +- (void)enqueue:(void (^)())block +{ + [self.queue addOperationWithBlock:block]; +} + +- (void)toMainThread:(void (^)())block +{ + [[NSOperationQueue mainQueue] addOperationWithBlock:block]; +} + +- (void)startBackgroundTask +{ + if (self.bgTaskId != UIBackgroundTaskInvalid) { + return; + } + + self.bgTaskId = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{ + [self setBgTaskId:UIBackgroundTaskInvalid]; + [self startBackgroundTask]; + }]; +} + +- (void)endBackgroundTask +{ + if (self.bgTaskId == UIBackgroundTaskInvalid) { + return; + } + + [[UIApplication sharedApplication] endBackgroundTask:self.bgTaskId]; + self.bgTaskId = UIBackgroundTaskInvalid; +} + #pragma mark - View Events - (void)viewDidLoad @@ -393,20 +446,22 @@ - (void)_setupCaptureSession return; } + [self startBackgroundTask]; + + __weak typeof(self)weakSelf = self; [self _checkDeviceAuthorizationWithCompletion:^(BOOL isAuthorized) { _deviceAuthorized = isAuthorized; - if (!_deviceAuthorized && [self.delegate respondsToSelector:@selector(userDeniedCameraPermissionsForCameraController:)]) { - dispatch_async(dispatch_get_main_queue(), ^{ - [self.delegate userDeniedCameraPermissionsForCameraController:self]; - }); + if (!_deviceAuthorized && [[weakSelf delegate] respondsToSelector:@selector(userDeniedCameraPermissionsForCameraController:)]) { + [weakSelf toMainThread:^{ + [[weakSelf delegate] userDeniedCameraPermissionsForCameraController:weakSelf]; + }]; } if (_deviceAuthorized) { - dispatch_async(dispatch_get_main_queue(), ^{ - + [weakSelf enqueue:^{ _session = [AVCaptureSession new]; _session.sessionPreset = AVCaptureSessionPresetPhoto; @@ -445,7 +500,7 @@ - (void)_setupCaptureSession break; } - [self setCameraFlashMode:_cameraFlashMode]; + [weakSelf setCameraFlashMode:_cameraFlashMode]; #endif NSDictionary *outputSettings = @{AVVideoCodecKey:AVVideoCodecJPEG}; @@ -457,13 +512,15 @@ - (void)_setupCaptureSession _deviceOrientation = [IFTTTDeviceOrientation new]; - if (self.isViewLoaded && self.view.window) { - [self startRunning]; - [self _insertPreviewLayer]; - [self _setPreviewVideoOrientation]; - [self _resetZoom]; + if ([weakSelf isViewLoaded] && [[weakSelf view] window]) { + [weakSelf startRunning]; + [weakSelf toMainThread:^{ + [weakSelf _insertPreviewLayer]; + [weakSelf _setPreviewVideoOrientation]; + [weakSelf _resetZoom]; + }]; } - }); + }]; } }]; } @@ -490,6 +547,8 @@ - (void)_teardownCaptureSession [self _removePreviewLayer]; _session = nil; + + [self endBackgroundTask]; } #pragma mark - Capturing a Photo @@ -513,12 +572,14 @@ - (void)_takePhoto [videoConnection setVideoMirrored:(_cameraDevice == FastttCameraDeviceFront)]; } + __weak typeof(self)weakSelf = self; + #if TARGET_IPHONE_SIMULATOR [self _insertPreviewLayer]; - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + [self enqueue:^{ UIImage *fakeImage = [UIImage fastttFakeTestImage]; - [self _processCameraPhoto:fakeImage needsPreviewRotation:needsPreviewRotation previewOrientation:UIDeviceOrientationPortrait]; - }); + [weakSelf _processCameraPhoto:fakeImage needsPreviewRotation:needsPreviewRotation previewOrientation:UIDeviceOrientationPortrait]; + }]; #else UIDeviceOrientation previewOrientation = [self _currentPreviewDeviceOrientation]; @@ -529,22 +590,23 @@ - (void)_takePhoto return; } - if (!self.isCapturingImage) { + if (![weakSelf isCapturingImage]) { return; } NSData *imageData = [AVCaptureStillImageOutput jpegStillImageNSDataRepresentation:imageDataSampleBuffer]; - if ([self.delegate respondsToSelector:@selector(cameraController:didFinishCapturingImageData:)]) { - [self.delegate cameraController:self didFinishCapturingImageData:imageData]; + if ([[weakSelf delegate] respondsToSelector:@selector(cameraController:didFinishCapturingImageData:)]) { + [weakSelf toMainThread:^{ + [[weakSelf delegate] cameraController:weakSelf didFinishCapturingImageData:imageData]; + }]; } - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - + [weakSelf enqueue:^{ UIImage *image = [UIImage imageWithData:imageData]; - [self _processCameraPhoto:image needsPreviewRotation:needsPreviewRotation previewOrientation:previewOrientation]; - }); + [weakSelf _processCameraPhoto:image needsPreviewRotation:needsPreviewRotation previewOrientation:previewOrientation]; + }]; }]; #endif } @@ -563,70 +625,72 @@ - (void)_processCameraPhoto:(UIImage *)image needsPreviewRotation:(BOOL)needsPre - (void)_processImage:(UIImage *)image withCropRect:(CGRect)cropRect maxDimension:(CGFloat)maxDimension fromCamera:(BOOL)fromCamera needsPreviewRotation:(BOOL)needsPreviewRotation previewOrientation:(UIDeviceOrientation)previewOrientation { - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - if (fromCamera && !self.isCapturingImage) { + __weak typeof(self)weakSelf = self; + + [self enqueue:^{ + if (fromCamera && ![weakSelf isCapturingImage]) { return; } FastttCapturedImage *capturedImage = [FastttCapturedImage fastttCapturedFullImage:image]; [capturedImage cropToRect:cropRect - returnsPreview:(fromCamera && self.returnsRotatedPreview) + returnsPreview:(fromCamera && [weakSelf returnsRotatedPreview]) needsPreviewRotation:needsPreviewRotation withPreviewOrientation:previewOrientation withCallback:^(FastttCapturedImage *capturedImage){ - if (fromCamera && !self.isCapturingImage) { + if (fromCamera && ![weakSelf isCapturingImage]) { return; } - if ([self.delegate respondsToSelector:@selector(cameraController:didFinishCapturingImage:)]) { - dispatch_async(dispatch_get_main_queue(), ^{ - [self.delegate cameraController:self didFinishCapturingImage:capturedImage]; - }); + if ([[weakSelf delegate] respondsToSelector:@selector(cameraController:didFinishCapturingImage:)]) { + [weakSelf toMainThread:^{ + [[weakSelf delegate] cameraController:weakSelf didFinishCapturingImage:capturedImage]; + }]; } }]; void (^scaleCallback)(FastttCapturedImage *capturedImage) = ^(FastttCapturedImage *capturedImage) { - if (fromCamera && !self.isCapturingImage) { + if (fromCamera && ![weakSelf isCapturingImage]) { return; } - if ([self.delegate respondsToSelector:@selector(cameraController:didFinishScalingCapturedImage:)]) { - dispatch_async(dispatch_get_main_queue(), ^{ - [self.delegate cameraController:self didFinishScalingCapturedImage:capturedImage]; - }); + if ([[weakSelf delegate] respondsToSelector:@selector(cameraController:didFinishScalingCapturedImage:)]) { + [weakSelf toMainThread:^{ + [[weakSelf delegate] cameraController:weakSelf didFinishScalingCapturedImage:capturedImage]; + }]; } }; - if (fromCamera && !self.isCapturingImage) { + if (fromCamera && ![weakSelf isCapturingImage]) { return; } if (maxDimension > 0.f) { [capturedImage scaleToMaxDimension:maxDimension withCallback:scaleCallback]; - } else if (fromCamera && self.scalesImage) { - [capturedImage scaleToSize:self.view.bounds.size + } else if (fromCamera && [weakSelf scalesImage]) { + [capturedImage scaleToSize:[weakSelf view].bounds.size withCallback:scaleCallback]; } - if (fromCamera && !self.isCapturingImage) { + if (fromCamera && ![weakSelf isCapturingImage]) { return; } - if (self.normalizesImageOrientations) { + if ([weakSelf normalizesImageOrientations]) { [capturedImage normalizeWithCallback:^(FastttCapturedImage *capturedImage){ - if (fromCamera && !self.isCapturingImage) { + if (fromCamera && ![weakSelf isCapturingImage]) { return; } - if ([self.delegate respondsToSelector:@selector(cameraController:didFinishNormalizingCapturedImage:)]) { - dispatch_async(dispatch_get_main_queue(), ^{ - [self.delegate cameraController:self didFinishNormalizingCapturedImage:capturedImage]; - }); + if ([[weakSelf delegate] respondsToSelector:@selector(cameraController:didFinishNormalizingCapturedImage:)]) { + [weakSelf toMainThread:^{ + [[weakSelf delegate] cameraController:weakSelf didFinishNormalizingCapturedImage:capturedImage]; + }]; } }]; } - self.isCapturingImage = NO; - }); + [weakSelf setIsCapturingImage:NO]; + }]; } #pragma mark - AV Orientation From b63725564f92e7da0d38bc70e7d63fe11f7158d0 Mon Sep 17 00:00:00 2001 From: Bell App Lab Date: Fri, 20 Nov 2015 14:21:40 +0000 Subject: [PATCH 4/5] Minimising the need to weakify self (makes for tidier code) --- FastttCamera/FastttCamera.m | 274 +++++++++++++++++++----------------- 1 file changed, 144 insertions(+), 130 deletions(-) diff --git a/FastttCamera/FastttCamera.m b/FastttCamera/FastttCamera.m index 6d52ab8..197c315 100644 --- a/FastttCamera/FastttCamera.m +++ b/FastttCamera/FastttCamera.m @@ -275,17 +275,26 @@ - (void)cancelImageProcessing - (void)processImage:(UIImage *)image withMaxDimension:(CGFloat)maxDimension { - [self _processImage:image withCropRect:CGRectNull maxDimension:maxDimension fromCamera:NO needsPreviewRotation:NO previewOrientation:UIDeviceOrientationUnknown]; + __weak typeof(self)weakSelf = self; + [self enqueue:^{ + [weakSelf _processImage:image withCropRect:CGRectNull maxDimension:maxDimension fromCamera:NO needsPreviewRotation:NO previewOrientation:UIDeviceOrientationUnknown]; + }]; } - (void)processImage:(UIImage *)image withCropRect:(CGRect)cropRect { - [self _processImage:image withCropRect:cropRect maxDimension:0.f fromCamera:NO needsPreviewRotation:NO previewOrientation:UIDeviceOrientationUnknown]; + __weak typeof(self)weakSelf = self; + [self enqueue:^{ + [weakSelf _processImage:image withCropRect:cropRect maxDimension:0.f fromCamera:NO needsPreviewRotation:NO previewOrientation:UIDeviceOrientationUnknown]; + }]; } - (void)processImage:(UIImage *)image withCropRect:(CGRect)cropRect maxDimension:(CGFloat)maxDimension { - [self _processImage:image withCropRect:cropRect maxDimension:maxDimension fromCamera:NO needsPreviewRotation:NO previewOrientation:UIDeviceOrientationUnknown]; + __weak typeof(self)weakSelf = self; + [self enqueue:^{ + [weakSelf _processImage:image withCropRect:cropRect maxDimension:maxDimension fromCamera:NO needsPreviewRotation:NO previewOrientation:UIDeviceOrientationUnknown]; + }]; } #pragma mark - Camera State @@ -453,75 +462,9 @@ - (void)_setupCaptureSession _deviceAuthorized = isAuthorized; - if (!_deviceAuthorized && [[weakSelf delegate] respondsToSelector:@selector(userDeniedCameraPermissionsForCameraController:)]) { - [weakSelf toMainThread:^{ - [[weakSelf delegate] userDeniedCameraPermissionsForCameraController:weakSelf]; - }]; - } - - if (_deviceAuthorized) { - - [weakSelf enqueue:^{ - _session = [AVCaptureSession new]; - _session.sessionPreset = AVCaptureSessionPresetPhoto; - - AVCaptureDevice *device = [AVCaptureDevice cameraDevice:self.cameraDevice]; - - if (!device) { - device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo]; - } - - if ([device lockForConfiguration:nil]) { - if([device isFocusModeSupported:AVCaptureFocusModeContinuousAutoFocus]){ - device.focusMode = AVCaptureFocusModeContinuousAutoFocus; - } - - if ([device isExposureModeSupported:AVCaptureExposureModeContinuousAutoExposure]) { - device.exposureMode = AVCaptureExposureModeContinuousAutoExposure; - } - - [device unlockForConfiguration]; - } - -#if !TARGET_IPHONE_SIMULATOR - AVCaptureDeviceInput *deviceInput = [AVCaptureDeviceInput deviceInputWithDevice:device error:nil]; - [_session addInput:deviceInput]; - - switch (device.position) { - case AVCaptureDevicePositionBack: - _cameraDevice = FastttCameraDeviceRear; - break; - - case AVCaptureDevicePositionFront: - _cameraDevice = FastttCameraDeviceFront; - break; - - default: - break; - } - - [weakSelf setCameraFlashMode:_cameraFlashMode]; -#endif - - NSDictionary *outputSettings = @{AVVideoCodecKey:AVVideoCodecJPEG}; - - _stillImageOutput = [AVCaptureStillImageOutput new]; - _stillImageOutput.outputSettings = outputSettings; - - [_session addOutput:_stillImageOutput]; - - _deviceOrientation = [IFTTTDeviceOrientation new]; - - if ([weakSelf isViewLoaded] && [[weakSelf view] window]) { - [weakSelf startRunning]; - [weakSelf toMainThread:^{ - [weakSelf _insertPreviewLayer]; - [weakSelf _setPreviewVideoOrientation]; - [weakSelf _resetZoom]; - }]; - } - }]; - } + [weakSelf enqueue:^{ + [weakSelf _postAuthorizationSetup]; + }]; }]; } @@ -625,72 +568,70 @@ - (void)_processCameraPhoto:(UIImage *)image needsPreviewRotation:(BOOL)needsPre - (void)_processImage:(UIImage *)image withCropRect:(CGRect)cropRect maxDimension:(CGFloat)maxDimension fromCamera:(BOOL)fromCamera needsPreviewRotation:(BOOL)needsPreviewRotation previewOrientation:(UIDeviceOrientation)previewOrientation { + if (fromCamera && !self.isCapturingImage) { + return; + } + __weak typeof(self)weakSelf = self; - [self enqueue:^{ - if (fromCamera && ![weakSelf isCapturingImage]) { + FastttCapturedImage *capturedImage = [FastttCapturedImage fastttCapturedFullImage:image]; + + [capturedImage cropToRect:cropRect + returnsPreview:(fromCamera && self.returnsRotatedPreview) + needsPreviewRotation:needsPreviewRotation + withPreviewOrientation:previewOrientation + withCallback:^(FastttCapturedImage *capturedImage){ + if (fromCamera && !self.isCapturingImage) { + return; + } + if ([self.delegate respondsToSelector:@selector(cameraController:didFinishCapturingImage:)]) { + [self toMainThread:^{ + [[weakSelf delegate] cameraController:weakSelf didFinishCapturingImage:capturedImage]; + }]; + } + }]; + + void (^scaleCallback)(FastttCapturedImage *capturedImage) = ^(FastttCapturedImage *capturedImage) { + if (fromCamera && !self.isCapturingImage) { return; } - - FastttCapturedImage *capturedImage = [FastttCapturedImage fastttCapturedFullImage:image]; - - [capturedImage cropToRect:cropRect - returnsPreview:(fromCamera && [weakSelf returnsRotatedPreview]) - needsPreviewRotation:needsPreviewRotation - withPreviewOrientation:previewOrientation - withCallback:^(FastttCapturedImage *capturedImage){ - if (fromCamera && ![weakSelf isCapturingImage]) { - return; - } - if ([[weakSelf delegate] respondsToSelector:@selector(cameraController:didFinishCapturingImage:)]) { - [weakSelf toMainThread:^{ - [[weakSelf delegate] cameraController:weakSelf didFinishCapturingImage:capturedImage]; - }]; - } - }]; - - void (^scaleCallback)(FastttCapturedImage *capturedImage) = ^(FastttCapturedImage *capturedImage) { - if (fromCamera && ![weakSelf isCapturingImage]) { + if ([self.delegate respondsToSelector:@selector(cameraController:didFinishScalingCapturedImage:)]) { + [self toMainThread:^{ + [[weakSelf delegate] cameraController:weakSelf didFinishScalingCapturedImage:capturedImage]; + }]; + } + }; + + if (fromCamera && !self.isCapturingImage) { + return; + } + + if (maxDimension > 0.f) { + [capturedImage scaleToMaxDimension:maxDimension + withCallback:scaleCallback]; + } else if (fromCamera && self.scalesImage) { + [capturedImage scaleToSize:self.view.bounds.size + withCallback:scaleCallback]; + } + + if (fromCamera && !self.isCapturingImage) { + return; + } + + if (self.normalizesImageOrientations) { + [capturedImage normalizeWithCallback:^(FastttCapturedImage *capturedImage){ + if (fromCamera && !self.isCapturingImage) { return; } - if ([[weakSelf delegate] respondsToSelector:@selector(cameraController:didFinishScalingCapturedImage:)]) { - [weakSelf toMainThread:^{ - [[weakSelf delegate] cameraController:weakSelf didFinishScalingCapturedImage:capturedImage]; + if ([self.delegate respondsToSelector:@selector(cameraController:didFinishNormalizingCapturedImage:)]) { + [self toMainThread:^{ + [[weakSelf delegate] cameraController:weakSelf didFinishNormalizingCapturedImage:capturedImage]; }]; } - }; - - if (fromCamera && ![weakSelf isCapturingImage]) { - return; - } - - if (maxDimension > 0.f) { - [capturedImage scaleToMaxDimension:maxDimension - withCallback:scaleCallback]; - } else if (fromCamera && [weakSelf scalesImage]) { - [capturedImage scaleToSize:[weakSelf view].bounds.size - withCallback:scaleCallback]; - } - - if (fromCamera && ![weakSelf isCapturingImage]) { - return; - } - - if ([weakSelf normalizesImageOrientations]) { - [capturedImage normalizeWithCallback:^(FastttCapturedImage *capturedImage){ - if (fromCamera && ![weakSelf isCapturingImage]) { - return; - } - if ([[weakSelf delegate] respondsToSelector:@selector(cameraController:didFinishNormalizingCapturedImage:)]) { - [weakSelf toMainThread:^{ - [[weakSelf delegate] cameraController:weakSelf didFinishNormalizingCapturedImage:capturedImage]; - }]; - } - }]; - } - - [weakSelf setIsCapturingImage:NO]; - }]; + }]; + } + + self.isCapturingImage = NO; } #pragma mark - AV Orientation @@ -772,6 +713,79 @@ - (void)_checkDeviceAuthorizationWithCompletion:(void (^)(BOOL isAuthorized))com #endif } +- (void)_postAuthorizationSetup +{ + __weak typeof(self)weakSelf = self; + + if (!_deviceAuthorized && [[weakSelf delegate] respondsToSelector:@selector(userDeniedCameraPermissionsForCameraController:)]) { + [self toMainThread:^{ + [[weakSelf delegate] userDeniedCameraPermissionsForCameraController:weakSelf]; + }]; + } + + if (_deviceAuthorized) { + + _session = [AVCaptureSession new]; + _session.sessionPreset = AVCaptureSessionPresetPhoto; + + AVCaptureDevice *device = [AVCaptureDevice cameraDevice:self.cameraDevice]; + + if (!device) { + device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo]; + } + + if ([device lockForConfiguration:nil]) { + if([device isFocusModeSupported:AVCaptureFocusModeContinuousAutoFocus]){ + device.focusMode = AVCaptureFocusModeContinuousAutoFocus; + } + + if ([device isExposureModeSupported:AVCaptureExposureModeContinuousAutoExposure]) { + device.exposureMode = AVCaptureExposureModeContinuousAutoExposure; + } + + [device unlockForConfiguration]; + } + +#if !TARGET_IPHONE_SIMULATOR + AVCaptureDeviceInput *deviceInput = [AVCaptureDeviceInput deviceInputWithDevice:device error:nil]; + [_session addInput:deviceInput]; + + switch (device.position) { + case AVCaptureDevicePositionBack: + _cameraDevice = FastttCameraDeviceRear; + break; + + case AVCaptureDevicePositionFront: + _cameraDevice = FastttCameraDeviceFront; + break; + + default: + break; + } + + [self setCameraFlashMode:_cameraFlashMode]; +#endif + + NSDictionary *outputSettings = @{AVVideoCodecKey:AVVideoCodecJPEG}; + + _stillImageOutput = [AVCaptureStillImageOutput new]; + _stillImageOutput.outputSettings = outputSettings; + + [_session addOutput:_stillImageOutput]; + + _deviceOrientation = [IFTTTDeviceOrientation new]; + + if (self.isViewLoaded && self.view.window) { + [self startRunning]; + [self toMainThread:^{ + [weakSelf _insertPreviewLayer]; + [weakSelf _setPreviewVideoOrientation]; + [weakSelf _resetZoom]; + }]; + } + } +} + #pragma mark - FastttCameraDevice - (AVCaptureDevice *)_currentCameraDevice From 609cb9f5dccc09bde72a922431a7c70ef3f5dc67 Mon Sep 17 00:00:00 2001 From: Bell App Lab Date: Fri, 20 Nov 2015 14:45:49 +0000 Subject: [PATCH 5/5] Simplified KVO. Introduced autofocus when becoming visible. Implemented AVCaptureSession error and interruption notifications. Added a delegate method to handle interruptions. Now the camera waits for the last focus, exposure and white balance operations to finish before snapping a picture. --- FastttCamera/AVCaptureDevice+FastttCamera.m | 10 + FastttCamera/FastttCamera.m | 180 ++++++++++++++++- FastttCamera/FastttCameraInterface.h | 27 +++ FastttCamera/FastttFocus.h | 34 ++++ FastttCamera/FastttFocus.m | 205 +++++++++++++++++++- 5 files changed, 438 insertions(+), 18 deletions(-) diff --git a/FastttCamera/AVCaptureDevice+FastttCamera.m b/FastttCamera/AVCaptureDevice+FastttCamera.m index 40b1db9..3f11546 100644 --- a/FastttCamera/AVCaptureDevice+FastttCamera.m +++ b/FastttCamera/AVCaptureDevice+FastttCamera.m @@ -121,10 +121,20 @@ - (BOOL)focusAtPointOfInterest:(CGPoint)pointOfInterest if ([self isFocusModeSupported:AVCaptureFocusModeContinuousAutoFocus]) { self.focusMode = AVCaptureFocusModeContinuousAutoFocus; + } else if ([self isFocusModeSupported:AVCaptureFocusModeLocked]) { + self.focusMode = AVCaptureFocusModeLocked; } if ([self isExposureModeSupported:AVCaptureExposureModeContinuousAutoExposure]) { self.exposureMode = AVCaptureExposureModeContinuousAutoExposure; + } else if ([self isExposureModeSupported:AVCaptureExposureModeLocked]) { + self.exposureMode = AVCaptureExposureModeLocked; + } + + if ([self isWhiteBalanceModeSupported:AVCaptureWhiteBalanceModeContinuousAutoWhiteBalance]) { + self.whiteBalanceMode = AVCaptureWhiteBalanceModeContinuousAutoWhiteBalance; + } else if ([self isWhiteBalanceModeSupported:AVCaptureWhiteBalanceModeLocked]) { + self.whiteBalanceMode = AVCaptureWhiteBalanceModeLocked; } [self unlockForConfiguration]; diff --git a/FastttCamera/FastttCamera.m b/FastttCamera/FastttCamera.m index 197c315..e1805f2 100644 --- a/FastttCamera/FastttCamera.m +++ b/FastttCamera/FastttCamera.m @@ -35,6 +35,9 @@ - (void)toMainThread:(void(^)())block; - (void)startBackgroundTask; - (void)endBackgroundTask; +//KVO +@property (nonatomic, assign) BOOL running; + @end @implementation FastttCamera @@ -62,6 +65,7 @@ - (instancetype)init { if ((self = [super init])) { _bgTaskId = UIBackgroundTaskInvalid; + _running = NO; [self _setupCaptureSession]; @@ -199,6 +203,17 @@ - (void)viewWillAppear:(BOOL)animated [self _setPreviewVideoOrientation]; } +- (void)viewDidAppear:(BOOL)animated +{ + [super viewDidAppear:animated]; + + if (self.handlesTapFocus && + _session.isRunning) + { + [self handleTapFocusAtPoint:self.view.center]; + } +} + - (void)viewDidDisappear:(BOOL)animated { [super viewDidDisappear:animated]; @@ -223,11 +238,22 @@ - (void)applicationDidBecomeActive:(NSNotification *)notification [self startRunning]; [self _insertPreviewLayer]; [self _setPreviewVideoOrientation]; + if ([self.delegate respondsToSelector:@selector(cameraControllerDidResume:)]) { + [self.delegate cameraControllerDidResume:self]; + } + if (self.handlesTapFocus && + _session.isRunning) + { + [self handleTapFocusAtPoint:self.view.center]; + } } } - (void)applicationWillResignActive:(NSNotification *)notification { + if ([self.delegate respondsToSelector:@selector(cameraControllerDidPause:)]) { + [self.delegate cameraControllerDidPause:self]; + } [self stopRunning]; } @@ -261,6 +287,10 @@ - (void)takePicture return; } + if (self.handlesTapFocus && self.fastFocus.isFocusing) { //We'll wait for the focus operation to finish + self.isCapturingImage = YES; + } + [self _takePhoto]; } @@ -375,6 +405,8 @@ - (void)setCameraDevice:(FastttCameraDevice)cameraDevice [self setCameraFlashMode:_cameraFlashMode]; [self _resetZoom]; + [self.fastFocus setCurrentDevice:device]; + [self handleTapFocusAtPoint:self.view.center]; } - (void)setCameraFlashMode:(FastttCameraFlashMode)cameraFlashMode @@ -409,6 +441,7 @@ - (void)startRunning { if (![_session isRunning]) { [_session startRunning]; + self.running = _session.isRunning; } } @@ -416,6 +449,7 @@ - (void)stopRunning { if ([_session isRunning]) { [_session stopRunning]; + self.running = _session.isRunning; } } @@ -489,6 +523,18 @@ - (void)_teardownCaptureSession [self _removePreviewLayer]; + [[NSNotificationCenter defaultCenter] removeObserver:self + name:AVCaptureSessionRuntimeErrorNotification + object:_session]; + + [[NSNotificationCenter defaultCenter] removeObserver:self + name:AVCaptureSessionWasInterruptedNotification + object:_session]; + + [[NSNotificationCenter defaultCenter] removeObserver:self + name:AVCaptureSessionInterruptionEndedNotification + object:_session]; + _session = nil; [self endBackgroundTask]; @@ -515,6 +561,31 @@ - (void)_takePhoto [videoConnection setVideoMirrored:(_cameraDevice == FastttCameraDeviceFront)]; } + /* + AVCaptureFocusModeContinuousAutoFocus, AVCaptureExposureModeContinuousAutoExposure and + AVCaptureWhiteBalanceModeContinuousAutoWhiteBalance are a bit too anxious at times and + may start refocusing after starting to take a photo but before arriving at + `captureStillImageAsynchronouslyFromConnection:completionHandler:`. + This may lead to multiple pictures being snapped when we're listening to + `hasFinishedAdjustingFocusAndExposure` events. + To avoid this scenario, we try to lock the camera device before snapping a picture. + */ + AVCaptureDevice *device = [[_session.inputs lastObject] device]; + if ([device lockForConfiguration:nil]) { + if ([device isFocusModeSupported:AVCaptureFocusModeLocked]) { + device.focusMode = AVCaptureFocusModeLocked; + } + + if ([device isExposureModeSupported:AVCaptureExposureModeLocked]) { + device.exposureMode = AVCaptureExposureModeLocked; + } + + if ([device isWhiteBalanceModeSupported:AVCaptureWhiteBalanceModeLocked]) { + device.whiteBalanceMode = AVCaptureWhiteBalanceModeLocked; + } + [device unlockForConfiguration]; + } + __weak typeof(self)weakSelf = self; #if TARGET_IPHONE_SIMULATOR @@ -728,23 +799,50 @@ - (void)_postAuthorizationSetup _session = [AVCaptureSession new]; _session.sessionPreset = AVCaptureSessionPresetPhoto; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(_sessionRuntimeError:) + name:AVCaptureSessionRuntimeErrorNotification + object:_session]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(_sessionWasInterrupted:) + name:AVCaptureSessionWasInterruptedNotification + object:_session]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(_sessionInterruptionEnded:) + name:AVCaptureSessionInterruptionEndedNotification + object:_session]; + AVCaptureDevice *device = [AVCaptureDevice cameraDevice:self.cameraDevice]; if (!device) { device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo]; } - if ([device lockForConfiguration:nil]) { - if([device isFocusModeSupported:AVCaptureFocusModeContinuousAutoFocus]){ - device.focusMode = AVCaptureFocusModeContinuousAutoFocus; - } - - if ([device isExposureModeSupported:AVCaptureExposureModeContinuousAutoExposure]) { - device.exposureMode = AVCaptureExposureModeContinuousAutoExposure; + if (!self.handlesTapFocus) { + if ([device lockForConfiguration:nil]) { + if ([device isFocusModeSupported:AVCaptureFocusModeContinuousAutoFocus]) { + device.focusMode = AVCaptureFocusModeContinuousAutoFocus; + } else if ([device isFocusModeSupported:AVCaptureFocusModeLocked]) { + device.focusMode = AVCaptureFocusModeLocked; + } + + if ([device isExposureModeSupported:AVCaptureExposureModeContinuousAutoExposure]) { + device.exposureMode = AVCaptureExposureModeContinuousAutoExposure; + } else if ([device isExposureModeSupported:AVCaptureExposureModeLocked]) { + device.exposureMode = AVCaptureExposureModeLocked; + } + + if ([device isWhiteBalanceModeSupported:AVCaptureWhiteBalanceModeContinuousAutoWhiteBalance]) { + device.whiteBalanceMode = AVCaptureWhiteBalanceModeContinuousAutoWhiteBalance; + } else if ([device isWhiteBalanceModeSupported:AVCaptureWhiteBalanceModeLocked]) { + device.whiteBalanceMode = AVCaptureWhiteBalanceModeLocked; + } + [device unlockForConfiguration]; } - [device unlockForConfiguration]; - } + } //Else, we're handling this on viewDidAppear or on applicationDidBecomeActive #if !TARGET_IPHONE_SIMULATOR AVCaptureDeviceInput *deviceInput = [AVCaptureDeviceInput deviceInputWithDevice:device error:nil]; @@ -846,6 +944,14 @@ - (BOOL)handleTapFocusAtPoint:(CGPoint)touchPoint return NO; } +- (void)hasFinishedAdjustingFocusAndExposure +{ + if (self.handlesTapFocus && self.isCapturingImage) { //We were waiting for the focus to finish, but now we can snap the picture + self.isCapturingImage = NO; + [self _takePhoto]; + } +} + #pragma mark - FastttZoomDelegate - (BOOL)handlePinchZoomWithScale:(CGFloat)zoomScale @@ -853,4 +959,60 @@ - (BOOL)handlePinchZoomWithScale:(CGFloat)zoomScale return ([self zoomToScale:zoomScale] && self.showsZoomView); } +#pragma mark - Error handling + +/* + These three methods were based on Apple's AVCam. + @see https://developer.apple.com/library/ios/samplecode/AVCam/Introduction/Intro.html + */ + +- (void)_sessionRuntimeError:(NSNotification *)notification +{ + NSError *error = notification.userInfo[AVCaptureSessionErrorKey]; + if (error.code == AVErrorMediaServicesWereReset) { + if (self.isRunning) { + __weak typeof(self)weakSelf = self; + [self enqueue:^{ + [weakSelf startRunning]; + }]; + } + return; + } + [self _teardownCaptureSession]; + if ([self.delegate respondsToSelector:@selector(cameraControllerDidPause:)]) { + __weak typeof(self)weakSelf = self; + [self toMainThread:^{ + [[weakSelf delegate] cameraControllerDidPause:weakSelf]; + }]; + } +} + +- (void)_sessionWasInterrupted:(NSNotification *)notification +{ + if (&AVCaptureSessionInterruptionReasonKey) { + AVCaptureSessionInterruptionReason reason = [notification.userInfo[AVCaptureSessionInterruptionReasonKey] integerValue]; + if (reason == AVCaptureSessionInterruptionReasonVideoDeviceNotAvailableInBackground) { + //We're stopping the camera anyway + return; + } + } + + if ([self.delegate respondsToSelector:@selector(cameraControllerDidPause:)]) { + __weak typeof(self)weakSelf = self; + [self toMainThread:^{ + [[weakSelf delegate] cameraControllerDidPause:weakSelf]; + }]; + } +} + +- (void)_sessionInterruptionEnded:(NSNotification *)notification +{ + if ([self.delegate respondsToSelector:@selector(cameraControllerDidPause:)]) { + __weak typeof(self)weakSelf = self; + [self toMainThread:^{ + [[weakSelf delegate] cameraControllerDidResume:weakSelf]; + }]; + } +} + @end diff --git a/FastttCamera/FastttCameraInterface.h b/FastttCamera/FastttCameraInterface.h index 13cd05c..3532208 100644 --- a/FastttCamera/FastttCameraInterface.h +++ b/FastttCamera/FastttCameraInterface.h @@ -282,6 +282,16 @@ */ - (void)stopRunning; +/** + * Tells the caller whether the camera is running or not. + * + * @note This property is mainly intented to be used as a guide to know whether we were running before an interruption in the current session + * or not. Although you can still observe this property, it is not equivalent to the AVCaptureSession counterpart. + * An instance of FastttCamera may report isRunning == YES, while we're in the middle of an interruption + * in the session. + */ +@property (nonatomic, readonly, getter=isRunning) BOOL running; + @end @@ -359,4 +369,21 @@ */ - (void)userDeniedCameraPermissionsForCameraController:(id)cameraController; +/** + * Called when the camera had been running but was interrupted by the system, or when the app is going to the background. + * + * @discussion This is a great opportunity to unhide that nice UIVisualEffectView you have laid out on top of the camera, if targeting iOS 8+. + * + * @note If you inspect the camera's isRunning property while executing this method, it may still return YES, meaning that the camera + * has't stopped and will resume automatically as soon as possible. If isRunning returns NO, we have encountered an error. + */ +- (void)cameraControllerDidPause:(id)cameraController; + +/** + * Called when the camera was interrupted but resumed. + * + * @discussion This is a great opportunity to hide that nice UIVisualEffectView you have laid out on top of the camera, if targeting iOS 8+. + */ +- (void)cameraControllerDidResume:(id)cameraController; + @end diff --git a/FastttCamera/FastttFocus.h b/FastttCamera/FastttFocus.h index d1c98e7..eef22e6 100644 --- a/FastttCamera/FastttFocus.h +++ b/FastttCamera/FastttFocus.h @@ -9,6 +9,7 @@ #import @protocol FastttFocusDelegate; +@class AVCaptureDevice; /** * Private class to handle focusing. If you want to manually handle focus, set @@ -52,6 +53,26 @@ */ - (void)showFocusViewAtPoint:(CGPoint)location; +/** + * Tells the caller whether a focus operation is currently running. + * + * @discussion By default, FastttFocus KVOs the AVCaptureDevice to know whether a focus operation has finished or not, + * so KVOing this property is essentially the same thing as KVOing AVFoundation. + * + * @note If handlesTapFocus has been set to NO, then this property will never change. + */ +@property (nonatomic, readonly, getter=isFocusing) BOOL focusing; + +/** + * The AVCaptureDevice currently associated with the FastttFocus instance. + * + * @discussion You tipically call this method to let the FastttFocus instance KVO the Capture Device to know whether + * a focus operation is being performed. + * + * @note If handlesTapFocus has been set to NO, then this property doesn't do anything. + */ +@property (nonatomic, weak) AVCaptureDevice *currentDevice; + @end #pragma mark - FastttFocusDelegate @@ -68,4 +89,17 @@ */ - (BOOL)handleTapFocusAtPoint:(CGPoint)touchPoint; +@optional + +/** + * Called when the focus, the exposure and the white balance operations have finished processing. + * + * @warning This method may be called multiple times due to the system automatically adjusting the focus, exposure and white balance trio, + * when AVCaptureFocusModeContinuousAutoFocus, AVCaptureExposureModeContinuousAutoExposure or + * AVCaptureWhiteBalanceModeContinuousAutoWhiteBalance have been set. + * + * @note If handlesTapFocus has been set to NO, then this method won't be called. + */ +- (void)hasFinishedAdjustingFocusAndExposure; + @end diff --git a/FastttCamera/FastttFocus.m b/FastttCamera/FastttFocus.m index 7ce33d6..53bfb51 100644 --- a/FastttCamera/FastttFocus.m +++ b/FastttCamera/FastttFocus.m @@ -7,14 +7,30 @@ // #import "FastttFocus.h" +#import "AVCaptureDevice+FastttCamera.h" CGFloat const kFocusSquareSize = 50.f; +NSString * const kFocusKVOKey = @"adjustingFocus"; +void * FocusKVOContext = &FocusKVOContext; +NSString * const kExposureKVOKey = @"adjustingExposure"; +void * ExposureKVOContext = &ExposureKVOContext; +NSString * const kWhiteBalanceKVOKey = @"adjustingWhiteBalance"; +void * WhiteBalanceKVOContext = &WhiteBalanceKVOContext; @interface FastttFocus () +{ + BOOL _animating; +} @property (nonatomic, strong) UIView *view; @property (nonatomic, strong) UITapGestureRecognizer *tapGestureRecognizer; -@property (nonatomic, assign) BOOL isFocusing; +@property (atomic, assign) BOOL focusing; + +//KVO +@property (nonatomic, assign) BOOL currentlyFocusing; +@property (nonatomic, assign) BOOL currentlyExposing; +@property (nonatomic, assign) BOOL currentlyBalancing; +@property (atomic, weak) NSTimer *focusTimer; @end @@ -34,9 +50,22 @@ + (instancetype)fastttFocusWithView:(UIView *)view gestureDelegate:(id *)change + context:(void *)context +{ + if (context == FocusKVOContext) { + + if ([change[NSKeyValueChangeOldKey] boolValue] == NO && [change[NSKeyValueChangeNewKey] boolValue] == YES) { + self.currentlyFocusing = YES; + [self _checkOverallFocusStatus]; + } else if ([change[NSKeyValueChangeOldKey] boolValue] == YES && [change[NSKeyValueChangeNewKey] boolValue] == NO) { + self.currentlyFocusing = NO; + [self _checkOverallFocusStatus]; + } + + return; + } + + if (context == ExposureKVOContext) { + + if ([change[NSKeyValueChangeOldKey] boolValue] == NO && [change[NSKeyValueChangeNewKey] boolValue] == YES) { + self.currentlyExposing = YES; + [self _checkOverallFocusStatus]; + } else if ([change[NSKeyValueChangeOldKey] boolValue] == YES && [change[NSKeyValueChangeNewKey] boolValue] == NO) { + self.currentlyExposing = NO; + [self _checkOverallFocusStatus]; + } + + return; + } + + if (context == WhiteBalanceKVOContext) { + + if ([change[NSKeyValueChangeOldKey] boolValue] == NO && [change[NSKeyValueChangeNewKey] boolValue] == YES) { + self.currentlyBalancing = YES; + [self _checkOverallFocusStatus]; + } else if ([change[NSKeyValueChangeOldKey] boolValue] == YES && [change[NSKeyValueChangeNewKey] boolValue] == NO) { + self.currentlyBalancing = NO; + [self _checkOverallFocusStatus]; + } + + return; + } + + [super observeValueForKeyPath:keyPath + ofObject:object + change:change + context:context]; +} + +- (void)_checkOverallFocusStatus +{ + /* + Focus, exposure and balance operations happen sequentially. So what we get here is: + + startFocus (self.focusing == YES) + endFocus (self.focusing == NO) + startExposure (self.focusing == YES) + endExposure (self.focusing == NO) + startBalance (self.focusing == YES) + endBalance (self.focusing == NO) + + However, we may not have yet finished processing the last call to _checkOverallFocusStatus + by the time a new call is made. So we synchronise everything to make sure we don't overprocess. + + Additionally, the behaviour we want is this: + + startFocus (self.focusing == YES) + endFocus + startExposure + endExposure + startBalance + endBalance (self.focusing == NO) + + To achieve this, we defer processing the overall status by 0.4 sec when any of the three possible + adjustments (focus, exposure and balance) report having finished. + */ + @synchronized(self) { + BOOL focusing = self.currentlyFocusing || self.currentlyExposing || self.currentlyBalancing; + + if (focusing != self.isFocusing) { + self.focusing = focusing; + + if (self.detectsTaps) { + + if (!focusing) { + if (self.focusTimer) { + /* + According to Apple's docs, changing a timer's fire date is an expensive operation, + but less so than scheduling a new one. + @see https://developer.apple.com/library/ios/documentation/Cocoa/Reference/Foundation/Classes/NSTimer_Class/index.html#//apple_ref/occ/instp/NSTimer/fireDate + */ + self.focusTimer.fireDate = [NSDate dateWithTimeIntervalSinceNow:.4]; + } else { + self.focusTimer = [NSTimer scheduledTimerWithTimeInterval:.4 + target:self + selector:@selector(_deferredOverallFocusStatusCheck:) + userInfo:nil + repeats:NO]; + } + } else { + [self.focusTimer invalidate]; + } + + } + } + } +} + +- (void)_deferredOverallFocusStatusCheck:(NSTimer *)sender +{ + [sender invalidate]; + + if (!self.focusing) { + //Should notify the delegate + /* + We're always on the main thread, so no need to dispatch + Also, we will only hit this point if the delegate implements `hasFinishedAdjustingFocusAndExposure` + */ + [self.delegate hasFinishedAdjustingFocusAndExposure]; + } +} + @end