From 5a151a808da940a171f57177cb0cf56acb5af918 Mon Sep 17 00:00:00 2001 From: Matthieu Achard Date: Sun, 14 Jun 2015 19:16:09 +0200 Subject: [PATCH 1/2] added video support for cameraRoll and disk targets --- Camera.ios.js | 4 ++ RCTCameraManager.h | 2 + RCTCameraManager.m | 132 ++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 136 insertions(+), 2 deletions(-) diff --git a/Camera.ios.js b/Camera.ios.js index 5ab85fe..db428ba 100644 --- a/Camera.ios.js +++ b/Camera.ios.js @@ -182,6 +182,10 @@ var Camera = React.createClass({ } NativeModules.CameraManager.capture(options, cb); + }, + + stopCapture() { + NativeModules.CameraManager.stopCapture(); } }); diff --git a/RCTCameraManager.h b/RCTCameraManager.h index a8e4d6f..04fffca 100644 --- a/RCTCameraManager.h +++ b/RCTCameraManager.h @@ -45,6 +45,7 @@ typedef NS_ENUM(NSInteger, RCTCameraFlashMode) { @property (nonatomic) AVCaptureSession *session; @property (nonatomic) AVCaptureDeviceInput *captureDeviceInput; @property (nonatomic) AVCaptureStillImageOutput *stillImageOutput; +@property (nonatomic) AVCaptureMovieFileOutput *movieFileOutput; @property (nonatomic) AVCaptureMetadataOutput *metadataOutput; @property (nonatomic) id runtimeErrorHandlingObserver; @property (nonatomic) NSInteger presetCamera; @@ -56,5 +57,6 @@ typedef NS_ENUM(NSInteger, RCTCameraFlashMode) { - (void)changeFlashMode:(NSInteger)flashMode; - (AVCaptureDevice *)deviceWithMediaType:(NSString *)mediaType preferringPosition:(AVCaptureDevicePosition)position; - (void)capture:(NSDictionary*)options callback:(RCTResponseSenderBlock)callback; +- (void)stopCapture; @end diff --git a/RCTCameraManager.m b/RCTCameraManager.m index c560aea..dfbee60 100644 --- a/RCTCameraManager.m +++ b/RCTCameraManager.m @@ -9,7 +9,10 @@ #import #import -@implementation RCTCameraManager +@implementation RCTCameraManager { + NSInteger videoTarget; + RCTResponseSenderBlock videoCallback; +} RCT_EXPORT_MODULE(); @@ -93,6 +96,21 @@ RCT_EXPORT_VIEW_PROPERTY(flashMode, NSInteger); self.captureDeviceInput = captureDeviceInput; } } + + AVCaptureDevice *audioCaptureDevice = [self deviceWithMediaType:AVMediaTypeAudio preferringPosition:self.presetCamera]; + if (audioCaptureDevice != nil) { + AVCaptureDeviceInput *audioInput = [AVCaptureDeviceInput deviceInputWithDevice:audioCaptureDevice error:&error]; + + if (error) + { + NSLog(@"%@", error); + } + + if ([self.session canAddInput:audioInput]) + { + [self.session addInput:audioInput]; + } + } AVCaptureStillImageOutput *stillImageOutput = [[AVCaptureStillImageOutput alloc] init]; if ([self.session canAddOutput:stillImageOutput]) @@ -101,6 +119,13 @@ RCT_EXPORT_VIEW_PROPERTY(flashMode, NSInteger); [self.session addOutput:stillImageOutput]; self.stillImageOutput = stillImageOutput; } + + AVCaptureMovieFileOutput *movieFileOutput = [[AVCaptureMovieFileOutput alloc] init]; + if ([self.session canAddOutput:movieFileOutput]) + { + [self.session addOutput:movieFileOutput]; + self.movieFileOutput = movieFileOutput; + } AVCaptureMetadataOutput *metadataOutput = [[AVCaptureMetadataOutput alloc] init]; if ([self.session canAddOutput:metadataOutput]) { @@ -193,7 +218,23 @@ RCT_EXPORT_METHOD(capture:(NSDictionary *)options callback:(RCTResponseSenderBlo [self captureStill:captureTarget callback:callback]; } else if (captureMode == RCTCameraCaptureModeVideo) { - // waiting for incoming PRs + if (self.movieFileOutput.recording) { + callback(@[RCTMakeError(@"Already Recording", nil, nil)]); + return; + } + + Float64 totalSeconds = [[options valueForKey:@"totalSeconds"] floatValue]; + int32_t preferredTimeScale = [[options valueForKey:@"preferredTimeScale"] intValue]; + CMTime maxDuration = CMTimeMakeWithSeconds(totalSeconds || 60, preferredTimeScale | 30); + self.movieFileOutput.maxRecordedDuration = maxDuration; + + [self captureVideo:captureTarget callback:callback]; + } +} + +RCT_EXPORT_METHOD(stopCapture) { + if (self.movieFileOutput.recording) { + [self.movieFileOutput stopRecording]; } } @@ -234,6 +275,93 @@ RCT_EXPORT_METHOD(capture:(NSDictionary *)options callback:(RCTResponseSenderBlo }]; } +-(void)captureVideo:(NSInteger)target callback:(RCTResponseSenderBlock)callback { + [[self.movieFileOutput connectionWithMediaType:AVMediaTypeVideo] setVideoOrientation:self.previewLayer.connection.videoOrientation]; + //Create temporary URL to record to + NSString *outputPath = [[NSString alloc] initWithFormat:@"%@%@", NSTemporaryDirectory(), @"output.mov"]; + NSURL *outputURL = [[NSURL alloc] initFileURLWithPath:outputPath]; + NSFileManager *fileManager = [NSFileManager defaultManager]; + if ([fileManager fileExistsAtPath:outputPath]) + { + NSError *error; + if ([fileManager removeItemAtPath:outputPath error:&error] == NO) + { + callback(@[RCTMakeError(error.description, nil, nil)]); + return; + } + } + //Start recording + [self.movieFileOutput startRecordingToOutputFileURL:outputURL recordingDelegate:self]; + + videoCallback = callback; + videoTarget = target; +} + +- (void)captureOutput:(AVCaptureFileOutput *)captureOutput +didFinishRecordingToOutputFileAtURL:(NSURL *)outputFileURL + fromConnections:(NSArray *)connections + error:(NSError *)error +{ + + NSLog(@"didFinishRecordingToOutputFileAtURL - enter"); + + BOOL RecordedSuccessfully = YES; + if ([error code] != noErr) + { + // A problem occurred: Find out if the recording was successful. + id value = [[error userInfo] objectForKey:AVErrorRecordingSuccessfullyFinishedKey]; + if (value) + { + RecordedSuccessfully = [value boolValue]; + } + } + if (RecordedSuccessfully) + { + //----- RECORDED SUCESSFULLY ----- + NSLog(@"didFinishRecordingToOutputFileAtURL - success"); + + if (videoTarget == RCTCameraCaptureTargetCameraRoll) { + ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init]; + if ([library videoAtPathIsCompatibleWithSavedPhotosAlbum:outputFileURL]) + { + [library writeVideoAtPathToSavedPhotosAlbum:outputFileURL + completionBlock:^(NSURL *assetURL, NSError *error) + { + if (error) + { + videoCallback(@[RCTMakeError(error.description, nil, nil)]); + return; + } + + videoCallback(@[[NSNull null], [assetURL absoluteString]]); + }]; + } + } + else if (videoTarget == RCTCameraCaptureTargetDisk) { + NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); + NSString *documentsDirectory = [paths firstObject]; + NSString *fullPath = [[documentsDirectory stringByAppendingPathComponent:[[NSUUID UUID] UUIDString]] stringByAppendingPathExtension:@"mov"]; + + NSFileManager * fileManager = [ NSFileManager defaultManager]; + NSError * error = nil; + + //copying destination + if ( !( [ fileManager copyItemAtPath:[outputFileURL path] toPath:fullPath error:&error ]) ) + { + videoCallback(@[RCTMakeError(error.description, nil, nil)]); + return; + } + videoCallback(@[[NSNull null], fullPath]); + } + else { + videoCallback(@[RCTMakeError(@"Target not supported", nil, nil)]); + } + + } else { + videoCallback(@[RCTMakeError(@"Error while recording", nil, nil)]); + } +} + - (NSString *)saveImage:(UIImage *)image withName:(NSString *)name { NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); NSString *documentsDirectory = [paths firstObject]; From 035166f5252bc25286a09263b022fe07fc3d8c87 Mon Sep 17 00:00:00 2001 From: Lochlan Wansbrough Date: Thu, 2 Jul 2015 13:04:25 -0700 Subject: [PATCH 2/2] Fixed up some of the video bugs, refactored some of the code --- Camera.ios.js | 14 +++++- RCTCameraManager.h | 4 +- RCTCameraManager.m | 114 ++++++++++++++++++++------------------------- 3 files changed, 67 insertions(+), 65 deletions(-) diff --git a/Camera.ios.js b/Camera.ios.js index db428ba..2d4a5af 100644 --- a/Camera.ios.js +++ b/Camera.ios.js @@ -67,7 +67,8 @@ var Camera = React.createClass({ getInitialState() { return { - isAuthorized: false + isAuthorized: false, + isRecording: false }; }, @@ -81,6 +82,10 @@ var Camera = React.createClass({ componentWillUnmount() { this.cameraBarCodeReadListener.remove(); + + if (this.state.isRecording) { + this.stopRecording(); + } }, render() { @@ -176,6 +181,12 @@ var Camera = React.createClass({ if (typeof options.mode === 'string') { options.mode = constants.CaptureMode[options.mode]; } + + if (options.mode === constants.CaptureMode.video) { + options.totalSeconds = (options.totalSeconds > -1 ? options.totalSeconds : -1); + options.preferredTimeScale = options.preferredTimeScale || 30; + this.setState({ isRecording: true }); + } if (typeof options.target === 'string') { options.target = constants.CaptureTarget[options.target]; @@ -185,6 +196,7 @@ var Camera = React.createClass({ }, stopCapture() { + this.setState({ isRecording: false }); NativeModules.CameraManager.stopCapture(); } diff --git a/RCTCameraManager.h b/RCTCameraManager.h index 04fffca..03fdfc5 100644 --- a/RCTCameraManager.h +++ b/RCTCameraManager.h @@ -39,7 +39,7 @@ typedef NS_ENUM(NSInteger, RCTCameraFlashMode) { RCTCameraFlashModeAuto = AVCaptureFlashModeAuto }; -@interface RCTCameraManager : RCTViewManager +@interface RCTCameraManager : RCTViewManager @property (nonatomic) dispatch_queue_t sessionQueue; @property (nonatomic) AVCaptureSession *session; @@ -50,6 +50,8 @@ typedef NS_ENUM(NSInteger, RCTCameraFlashMode) { @property (nonatomic) id runtimeErrorHandlingObserver; @property (nonatomic) NSInteger presetCamera; @property (nonatomic) AVCaptureVideoPreviewLayer *previewLayer; +@property (nonatomic) NSInteger videoTarget; +@property (nonatomic, strong) RCTResponseSenderBlock videoCallback; - (void)changeAspect:(NSString *)aspect; - (void)changeCamera:(NSInteger)camera; diff --git a/RCTCameraManager.m b/RCTCameraManager.m index d3efd50..14cbd22 100644 --- a/RCTCameraManager.m +++ b/RCTCameraManager.m @@ -9,10 +9,7 @@ #import #import -@implementation RCTCameraManager { - NSInteger videoTarget; - RCTResponseSenderBlock videoCallback; -} +@implementation RCTCameraManager RCT_EXPORT_MODULE(); @@ -224,9 +221,11 @@ RCT_EXPORT_METHOD(capture:(NSDictionary *)options callback:(RCTResponseSenderBlo } Float64 totalSeconds = [[options valueForKey:@"totalSeconds"] floatValue]; - int32_t preferredTimeScale = [[options valueForKey:@"preferredTimeScale"] intValue]; - CMTime maxDuration = CMTimeMakeWithSeconds(totalSeconds || 60, preferredTimeScale | 30); - self.movieFileOutput.maxRecordedDuration = maxDuration; + if (totalSeconds > -1) { + int32_t preferredTimeScale = [[options valueForKey:@"preferredTimeScale"] intValue]; + CMTime maxDuration = CMTimeMakeWithSeconds(totalSeconds, preferredTimeScale); + self.movieFileOutput.maxRecordedDuration = maxDuration; + } [self captureVideo:captureTarget callback:callback]; } @@ -294,25 +293,26 @@ RCT_EXPORT_METHOD(stopCapture) { } -(void)captureVideo:(NSInteger)target callback:(RCTResponseSenderBlock)callback { + [[self.movieFileOutput connectionWithMediaType:AVMediaTypeVideo] setVideoOrientation:self.previewLayer.connection.videoOrientation]; + //Create temporary URL to record to NSString *outputPath = [[NSString alloc] initWithFormat:@"%@%@", NSTemporaryDirectory(), @"output.mov"]; NSURL *outputURL = [[NSURL alloc] initFileURLWithPath:outputPath]; NSFileManager *fileManager = [NSFileManager defaultManager]; - if ([fileManager fileExistsAtPath:outputPath]) - { + if ([fileManager fileExistsAtPath:outputPath]) { NSError *error; - if ([fileManager removeItemAtPath:outputPath error:&error] == NO) - { + if ([fileManager removeItemAtPath:outputPath error:&error] == NO) { callback(@[RCTMakeError(error.description, nil, nil)]); return; } } + //Start recording [self.movieFileOutput startRecordingToOutputFileURL:outputURL recordingDelegate:self]; - videoCallback = callback; - videoTarget = target; + self.videoCallback = callback; + self.videoTarget = target; } - (void)captureOutput:(AVCaptureFileOutput *)captureOutput @@ -321,62 +321,50 @@ didFinishRecordingToOutputFileAtURL:(NSURL *)outputFileURL error:(NSError *)error { - NSLog(@"didFinishRecordingToOutputFileAtURL - enter"); - - BOOL RecordedSuccessfully = YES; - if ([error code] != noErr) - { + BOOL recordSuccess = YES; + if ([error code] != noErr) { // A problem occurred: Find out if the recording was successful. id value = [[error userInfo] objectForKey:AVErrorRecordingSuccessfullyFinishedKey]; - if (value) - { - RecordedSuccessfully = [value boolValue]; + if (value) { + recordSuccess = [value boolValue]; } } - if (RecordedSuccessfully) - { - //----- RECORDED SUCESSFULLY ----- - NSLog(@"didFinishRecordingToOutputFileAtURL - success"); - - if (videoTarget == RCTCameraCaptureTargetCameraRoll) { - ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init]; - if ([library videoAtPathIsCompatibleWithSavedPhotosAlbum:outputFileURL]) - { - [library writeVideoAtPathToSavedPhotosAlbum:outputFileURL - completionBlock:^(NSURL *assetURL, NSError *error) - { - if (error) - { - videoCallback(@[RCTMakeError(error.description, nil, nil)]); - return; - } - - videoCallback(@[[NSNull null], [assetURL absoluteString]]); - }]; - } + if (!recordSuccess) { + self.videoCallback(@[RCTMakeError(@"Error while recording", nil, nil)]); + return; + } + + if (self.videoTarget == RCTCameraCaptureTargetCameraRoll) { + ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init]; + if ([library videoAtPathIsCompatibleWithSavedPhotosAlbum:outputFileURL]) { + [library writeVideoAtPathToSavedPhotosAlbum:outputFileURL + completionBlock:^(NSURL *assetURL, NSError *error) { + if (error) { + self.videoCallback(@[RCTMakeError(error.description, nil, nil)]); + return; + } + + self.videoCallback(@[[NSNull null], [assetURL absoluteString]]); + }]; } - else if (videoTarget == RCTCameraCaptureTargetDisk) { - NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); - NSString *documentsDirectory = [paths firstObject]; - NSString *fullPath = [[documentsDirectory stringByAppendingPathComponent:[[NSUUID UUID] UUIDString]] stringByAppendingPathExtension:@"mov"]; - - NSFileManager * fileManager = [ NSFileManager defaultManager]; - NSError * error = nil; - - //copying destination - if ( !( [ fileManager copyItemAtPath:[outputFileURL path] toPath:fullPath error:&error ]) ) - { - videoCallback(@[RCTMakeError(error.description, nil, nil)]); - return; - } - videoCallback(@[[NSNull null], fullPath]); + } + else if (self.videoTarget == RCTCameraCaptureTargetDisk) { + NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); + NSString *documentsDirectory = [paths firstObject]; + NSString *fullPath = [[documentsDirectory stringByAppendingPathComponent:[[NSUUID UUID] UUIDString]] stringByAppendingPathExtension:@"mov"]; + + NSFileManager * fileManager = [NSFileManager defaultManager]; + NSError * error = nil; + + //copying destination + if (!([fileManager copyItemAtPath:[outputFileURL path] toPath:fullPath error:&error])) { + self.videoCallback(@[RCTMakeError(error.description, nil, nil)]); + return; } - else { - videoCallback(@[RCTMakeError(@"Target not supported", nil, nil)]); - } - - } else { - videoCallback(@[RCTMakeError(@"Error while recording", nil, nil)]); + self.videoCallback(@[[NSNull null], fullPath]); + } + else { + self.videoCallback(@[RCTMakeError(@"Target not supported", nil, nil)]); } }