Files
ResearchKit/ResearchKitActiveTask/Front Facing Camera/ORKFrontFacingCameraStepViewController.m
Pariece McKinney c71d1a56dd Public release 3.0.1
2024-05-08 14:56:11 -04:00

384 lines
15 KiB
Objective-C

/*
Copyright (c) 2020, Apple Inc. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
3. Neither the name of the copyright holder(s) nor the names of any contributors
may be used to endorse or promote products derived from this software without
specific prior written permission. No license is granted to the trademarks of
the copyright holders even if such marks are included in this software.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#import <AVFoundation/AVFoundation.h>
#import <AVKit/AVKit.h>
#import <CoreImage/CoreImage.h>
#import <MediaPlayer/MediaPlayer.h>
#import "ORKActiveStepView.h"
#import "ORKActiveStepViewController_Internal.h"
#import "ORKCollectionResult_Private.h"
#import "ORKFrontFacingCameraStep.h"
#import "ORKFrontFacingCameraStepContentView.h"
#import "ORKFrontFacingCameraStepResult.h"
#import "ORKFrontFacingCameraStepViewController.h"
#import "ORKHelpers_Internal.h"
#import "ORKNavigationContainerView_Internal.h"
#import "ORKResult_Private.h"
#import "ORKStepContainerView_Private.h"
#import "ORKStepViewController_Internal.h"
#import "ORKTaskViewController_Internal.h"
@interface ORKFrontFacingCameraStepViewController () <AVCaptureFileOutputRecordingDelegate, AVCaptureVideoDataOutputSampleBufferDelegate>
@property (nonatomic, strong) ORKFrontFacingCameraStepContentView *contentView;
@end
@implementation ORKFrontFacingCameraStepViewController {
NSMutableArray *_results;
ORKFrontFacingCameraStep *_frontFacingCameraStep;
AVCaptureMovieFileOutput *_movieFileOutput;
NSURL *_tempOutputURL;
NSURL *_savedFileURL;
NSString *_savedFileName;
AVCaptureDevice *_frontCameraCaptureDevice;
AVCaptureSession *_captureSession;
NSInteger retryCount;
}
- (instancetype)initWithStep:(ORKStep *)step {
self = [super initWithStep:step];
if (self) {
retryCount = 0;
_frontFacingCameraStep = (ORKFrontFacingCameraStep *)step;
}
return self;
}
- (void)viewDidLoad {
[super viewDidLoad];
_results = [NSMutableArray new];
[self setupContentView];
[self setupConstraints];
[self startSession];
[self.taskViewController setNavigationBarColor:[self.view backgroundColor]];
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[_contentView layoutSubviews];
}
- (void)handleError:(NSError *)error {
// Shut down the session, if running
if (_captureSession.isRunning) {
[_captureSession stopRunning];
}
// Reset the state to before the capture session was setup. Order here is important
_captureSession = nil;
_movieFileOutput = nil;
_tempOutputURL = nil;
_savedFileURL = nil;
// Handle error in the UI.
[_contentView handleError:error];
}
- (void)stepDidFinish {
[super stepDidFinish];
if (_tempOutputURL) {
[self deleteTempVideoFile];
}
[self goForward];
}
- (void)setupContentView {
_contentView = [[ORKFrontFacingCameraStepContentView alloc] initWithTitle:_frontFacingCameraStep.title text:_frontFacingCameraStep.text];
_contentView.layer.cornerRadius = 10.0;
_contentView.layer.maskedCorners = kCALayerMinXMinYCorner | kCALayerMaxXMinYCorner;
_contentView.clipsToBounds = YES;
__weak typeof(self) weakSelf = self;
[_contentView setViewEventHandler:^(ORKFrontFacingCameraStepContentViewEvent event) {
[weakSelf handleContentViewEvent:event];
}];
[self.view addSubview:_contentView];
}
- (void)handleContentViewEvent:(ORKFrontFacingCameraStepContentViewEvent)event {
switch (event)
{
case ORKFrontFacingCameraStepContentViewEventStartRecording:
[self startVideoRecording];
break;
case ORKFrontFacingCameraStepContentViewEventStopRecording:
[self stopVideoRecording];
break;
case ORKFrontFacingCameraStepContentViewEventReviewRecording:
{
AVPlayerItem* playerItem = [AVPlayerItem playerItemWithURL:_tempOutputURL];
AVPlayer *playVideo = [[AVPlayer alloc] initWithPlayerItem:playerItem];
AVPlayerViewController *playerViewController = [[AVPlayerViewController alloc] init];
playerViewController.player = playVideo;
playerViewController.player.volume = 1.0;
[self presentViewController:playerViewController animated:YES completion:nil];
[playVideo play];
break;
}
case ORKFrontFacingCameraStepContentViewEventRetryRecording:
[self deleteTempVideoFile];
retryCount++;
break;
case ORKFrontFacingCameraStepContentViewEventSubmitRecording:
{
[self submitVideo];
break;
}
case ORKFrontFacingCameraStepContentViewEventError:
break;
}
}
-(void)setupConstraints {
_contentView.translatesAutoresizingMaskIntoConstraints = NO;
[[_contentView.topAnchor constraintEqualToAnchor:self.view.topAnchor] setActive:YES];
[[_contentView.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor] setActive:YES];
[[_contentView.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor] setActive:YES];
[[_contentView.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor] setActive:YES];
}
- (void)startSession
{
_captureSession = [AVCaptureSession new];
_frontCameraCaptureDevice = [AVCaptureDevice defaultDeviceWithDeviceType:AVCaptureDeviceTypeBuiltInWideAngleCamera mediaType:AVMediaTypeVideo position:AVCaptureDevicePositionFront];
if (_frontCameraCaptureDevice)
{
NSError *error = nil;
AVCaptureDevice *captureAudioDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];
AVCaptureDeviceInput *deviceInput = [AVCaptureDeviceInput deviceInputWithDevice:_frontCameraCaptureDevice error:&error];
AVCaptureDeviceInput *audioInput = [AVCaptureDeviceInput deviceInputWithDevice:captureAudioDevice error:&error];
[AVAudioSession.sharedInstance setCategory:AVAudioSessionCategoryPlayAndRecord mode:AVAudioSessionModeVideoRecording options:0 error:&error];
if (error) {
[self handleError:[NSError errorWithDomain:NSCocoaErrorDomain code:NSFeatureUnsupportedError userInfo:@{NSLocalizedDescriptionKey:ORKLocalizedString(@"CAPTURE_ERROR_CAMERA_NOT_FOUND", nil)}]];
return;
}
[_captureSession beginConfiguration];
if ([_captureSession canAddInput:deviceInput]) {
[_captureSession addInput:deviceInput];
}
if ([_captureSession canSetSessionPreset:AVCaptureSessionPreset640x480]) {
[_captureSession setSessionPreset:AVCaptureSessionPreset640x480];
}
if ([_captureSession canAddInput:audioInput]) {
[_captureSession addInput:audioInput];
}
_movieFileOutput = [AVCaptureMovieFileOutput new];
if ([_captureSession canAddOutput:_movieFileOutput]) {
[_captureSession addOutput:_movieFileOutput];
AVCaptureConnection *captureConnection = [_movieFileOutput connectionWithMediaType:AVMediaTypeVideo];
if (captureConnection && [captureConnection isVideoStabilizationSupported]) {
captureConnection.preferredVideoStabilizationMode = AVCaptureVideoStabilizationModeAuto;
}
}
AVCaptureVideoDataOutput *output = [AVCaptureVideoDataOutput new];
NSString* key = (NSString*)kCVPixelBufferPixelFormatTypeKey;
NSNumber* value = [NSNumber numberWithUnsignedInt:kCVPixelFormatType_32BGRA];
NSDictionary* videoSettings = [NSDictionary dictionaryWithObject:value forKey:key];
[output setVideoSettings:videoSettings];
output.alwaysDiscardsLateVideoFrames = YES;
if ([_captureSession canAddOutput:output]) {
[_captureSession addOutput:output];
}
AVCaptureConnection *connection = [output connectionWithMediaType:AVMediaTypeVideo];
if ([connection isVideoOrientationSupported]) {
connection.videoOrientation = AVCaptureVideoOrientationPortrait;
}
if ([connection isVideoMirroringSupported]) {
[connection setVideoMirrored:NO];
}
[_captureSession commitConfiguration];
dispatch_queue_attr_t qos = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INITIATED, -1);
dispatch_queue_t recordingQueue = dispatch_queue_create("output.queue", qos);
[output setSampleBufferDelegate:self queue:recordingQueue];
[_contentView setPreviewLayerWithSession:_captureSession];
[_captureSession startRunning];
}
[_contentView layoutSubviews];
}
- (void)startVideoRecording {
if (![_movieFileOutput isRecording]) {
AVCaptureConnection *movieFileOutputConnection = [_movieFileOutput connectionWithMediaType:AVMediaTypeVideo];
[movieFileOutputConnection setVideoOrientation:AVCaptureVideoOrientationPortrait];
NSArray<AVVideoCodecType> *availableVideoCodecTypes = _movieFileOutput.availableVideoCodecTypes;
if (availableVideoCodecTypes && [availableVideoCodecTypes containsObject:AVVideoCodecTypeHEVC]) {
NSString* key = (NSString*)AVVideoCodecKey;
NSString* value = (NSString*)AVVideoCodecTypeHEVC;
NSDictionary* outputSettings = [NSDictionary dictionaryWithObject:value forKey:key];
[_movieFileOutput setOutputSettings:outputSettings forConnection:movieFileOutputConnection];
}
// Start recording to a temporary file.
NSString *tempVideoFilePath = [[NSTemporaryDirectory() stringByAppendingPathComponent:[NSUUID new].UUIDString] stringByAppendingPathExtension:@"mov"];
[_movieFileOutput startRecordingToOutputFileURL:[NSURL fileURLWithPath:tempVideoFilePath] recordingDelegate:self];
}
[_contentView layoutSubviews];
}
- (void)stopVideoRecording {
if (_movieFileOutput && [_movieFileOutput isRecording]) {
[_movieFileOutput stopRecording];
}
}
- (void)submitVideo {
if ([self tempVideoFileExists])
{
//Save video to permanant file
NSString *outputFileName = [NSUUID new].UUIDString;
_savedFileName = [outputFileName stringByAppendingPathExtension:@"mov"];
NSURL *docURL = [NSFileManager.defaultManager URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask].lastObject;
docURL = [docURL URLByAppendingPathComponent:_savedFileName];
NSData *data = [NSData dataWithContentsOfURL:_tempOutputURL];
BOOL wasDataSavedToURL = [data writeToURL:docURL atomically:YES];
if (wasDataSavedToURL)
{
//remove video saved to temp directory if it was saved successfully in the document directory
_savedFileURL = docURL;
[self deleteTempVideoFile];
[self finish];
}
}
}
- (BOOL)tempVideoFileExists {
if (_tempOutputURL && [NSFileManager.defaultManager fileExistsAtPath:_tempOutputURL.relativePath]) {
return YES;
} else {
return NO;
}
}
- (void)deleteTempVideoFile {
if ([self tempVideoFileExists]) {
NSError *error;
[NSFileManager.defaultManager removeItemAtPath:_tempOutputURL.relativePath error:&error];
if (!error) {
_tempOutputURL = nil;
} else {
@throw [NSException exceptionWithName:NSGenericException reason:[NSString stringWithFormat:@"There was an error encountered while attempting to remove the saved video from the temp directory at path: %@", _tempOutputURL.path] userInfo:nil];
}
} else if (_tempOutputURL) {
_tempOutputURL = nil;
}
}
- (ORKStepResult *)result {
ORKStepResult *stepResult = [super result];
NSDate *now = stepResult.endDate;
NSMutableArray *results = [NSMutableArray arrayWithArray:stepResult.results];
ORKFrontFacingCameraStepResult *frontFacingCameraResult = [[ORKFrontFacingCameraStepResult alloc] initWithIdentifier:self.step.identifier];
frontFacingCameraResult.startDate = stepResult.startDate;
frontFacingCameraResult.endDate = now;
frontFacingCameraResult.contentType = @"video/quicktime";
frontFacingCameraResult.fileURL = _savedFileURL;
frontFacingCameraResult.fileName = _savedFileName;
frontFacingCameraResult.retryCount = retryCount;
[results addObject:frontFacingCameraResult];
stepResult.results = [results copy];
return stepResult;
}
#pragma mark - AVCaptureFileOutputRecordingDelegate methods
- (void)captureOutput:(AVCaptureFileOutput *)output didStartRecordingToOutputFileAtURL:(NSURL *)fileURL fromConnections:(NSArray<AVCaptureConnection *> *)connections {
[_contentView startTimerWithMaximumRecordingLimit:_frontFacingCameraStep.maximumRecordingLimit];
}
- (void)captureOutput:(AVCaptureFileOutput *)output didFinishRecordingToOutputFileAtURL:(NSURL *)outputFileURL fromConnections:(NSArray<AVCaptureConnection *> *)connections error:(NSError *)error
{
if (!error)
{
_tempOutputURL = outputFileURL;
[_contentView presentReviewOptionsAllowingReview:_frontFacingCameraStep.allowsReview
allowRetry:_frontFacingCameraStep.allowsRetry];
if (!_frontFacingCameraStep.allowsRetry && !_frontFacingCameraStep.allowsReview) {
[self submitVideo];
}
}
}
@end