diff --git a/ios/RN/RNCamera.h b/ios/RN/RNCamera.h index 83efe6d..e7d1cef 100644 --- a/ios/RN/RNCamera.h +++ b/ios/RN/RNCamera.h @@ -29,7 +29,9 @@ @property (assign, nonatomic) NSInteger autoFocus; @property (assign, nonatomic) float focusDepth; @property (assign, nonatomic) NSInteger whiteBalance; -@property (nonatomic, assign, getter=isReadingBarCodes) BOOL barCodeReading; +@property (assign, nonatomic) AVCaptureSessionPreset pictureSize; +@property (nonatomic, assign) BOOL isReadingBarCodes; +@property (nonatomic, assign) BOOL isDetectingFaces; @property(assign, nonatomic) AVVideoCodecType videoCodecType; - (id)initWithBridge:(RCTBridge *)bridge; @@ -39,6 +41,7 @@ - (void)updateFocusDepth; - (void)updateZoom; - (void)updateWhiteBalance; +- (void)updatePictureSize; - (void)updateFaceDetecting:(id)isDetectingFaces; - (void)updateFaceDetectionMode:(id)requestedMode; - (void)updateFaceDetectionLandmarks:(id)requestedLandmarks; @@ -46,11 +49,14 @@ - (void)takePicture:(NSDictionary *)options resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject; - (void)record:(NSDictionary *)options resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject; - (void)stopRecording; +- (void)resumePreview; +- (void)pausePreview; - (void)setupOrDisableBarcodeScanner; - (void)onReady:(NSDictionary *)event; - (void)onMountingError:(NSDictionary *)event; - (void)onCodeRead:(NSDictionary *)event; - (void)onFacesDetected:(NSDictionary *)event; +- (void)onPictureSaved:(NSDictionary *)event; @end diff --git a/ios/RN/RNCamera.m b/ios/RN/RNCamera.m index d4cea11..88e9602 100644 --- a/ios/RN/RNCamera.m +++ b/ios/RN/RNCamera.m @@ -21,6 +21,7 @@ @property (nonatomic, copy) RCTDirectEventBlock onMountError; @property (nonatomic, copy) RCTDirectEventBlock onBarCodeRead; @property (nonatomic, copy) RCTDirectEventBlock onFacesDetected; +@property (nonatomic, copy) RCTDirectEventBlock onPictureSaved; @end @@ -85,6 +86,13 @@ static NSDictionary *defaultFaceDetectorOptions = nil; } } +- (void)onPictureSaved:(NSDictionary *)event +{ + if (_onPictureSaved) { + _onPictureSaved(event); + } +} + - (void)layoutSubviews { [super layoutSubviews]; @@ -210,7 +218,7 @@ static NSDictionary *defaultFaceDetectorOptions = nil; AVCaptureDevice *device = [self.videoCaptureDeviceInput device]; NSError *error = nil; - if (self.autoFocus < 0 || device.focusMode != RNCameraAutoFocusOff || device.position == RNCameraTypeFront) { + if (device == nil || self.autoFocus < 0 || device.focusMode != RNCameraAutoFocusOff || device.position == RNCameraTypeFront) { return; } @@ -284,6 +292,11 @@ static NSDictionary *defaultFaceDetectorOptions = nil; [device unlockForConfiguration]; } +- (void)updatePictureSize +{ + [self updateSessionPreset:self.pictureSize]; +} + #if __has_include() - (void)updateFaceDetecting:(id)faceDetecting { @@ -318,16 +331,24 @@ static NSDictionary *defaultFaceDetectorOptions = nil; [connection setVideoOrientation:orientation]; [self.stillImageOutput captureStillImageAsynchronouslyFromConnection:connection completionHandler: ^(CMSampleBufferRef imageSampleBuffer, NSError *error) { if (imageSampleBuffer && !error) { + BOOL useFastMode = options[@"fastMode"] && [options[@"fastMode"] boolValue]; + if (useFastMode) { + resolve(nil); + } NSData *imageData = [AVCaptureStillImageOutput jpegStillImageNSDataRepresentation:imageSampleBuffer]; UIImage *takenImage = [UIImage imageWithData:imageData]; - CGRect frame = [_previewLayer metadataOutputRectOfInterestForRect:self.frame]; CGImageRef takenCGImage = takenImage.CGImage; - size_t width = CGImageGetWidth(takenCGImage); - size_t height = CGImageGetHeight(takenCGImage); - CGRect cropRect = CGRectMake(frame.origin.x * width, frame.origin.y * height, frame.size.width * width, frame.size.height * height); - takenImage = [RNImageUtils cropImage:takenImage toRect:cropRect]; + CGSize previewSize; + if (UIInterfaceOrientationIsPortrait([[UIApplication sharedApplication] statusBarOrientation])) { + previewSize = CGSizeMake(self.previewLayer.frame.size.height, self.previewLayer.frame.size.width); + } else { + previewSize = CGSizeMake(self.previewLayer.frame.size.width, self.previewLayer.frame.size.height); + } + CGRect cropRect = CGRectMake(0, 0, CGImageGetWidth(takenCGImage), CGImageGetHeight(takenCGImage)); + CGRect croppedSize = AVMakeRectWithAspectRatioInsideRect(previewSize, cropRect); + takenImage = [RNImageUtils cropImage:takenImage toRect:croppedSize]; if ([options[@"mirrorImage"] boolValue]) { takenImage = [RNImageUtils mirrorImage:takenImage]; @@ -377,7 +398,11 @@ static NSDictionary *defaultFaceDetectorOptions = nil; [RNImageUtils updatePhotoMetadata:imageSampleBuffer withAdditionalData:@{ @"Orientation": @(imageRotation) } inResponse:response]; // TODO } - resolve(response); + if (useFastMode) { + [self onPictureSaved:@{@"data": response, @"id": options[@"id"]}]; + } else { + resolve(response); + } } else { reject(@"E_IMAGE_CAPTURE_FAILED", @"Image could not be captured", error); } @@ -447,6 +472,16 @@ static NSDictionary *defaultFaceDetectorOptions = nil; [self.movieFileOutput stopRecording]; } +- (void)resumePreview +{ + [[self.previewLayer connection] setEnabled:YES]; +} + +- (void)pausePreview +{ + [[self.previewLayer connection] setEnabled:NO]; +} + - (void)startSession { #if TARGET_IPHONE_SIMULATOR @@ -571,10 +606,14 @@ static NSDictionary *defaultFaceDetectorOptions = nil; #pragma mark - internal -- (void)updateSessionPreset:(NSString *)preset +- (void)updateSessionPreset:(AVCaptureSessionPreset)preset { #if !(TARGET_IPHONE_SIMULATOR) if (preset) { + if (self.isDetectingFaces && [preset isEqual:AVCaptureSessionPresetPhoto]) { + RCTLog(@"AVCaptureSessionPresetPhoto not supported during face detection. Falling back to AVCaptureSessionPresetHigh"); + preset = AVCaptureSessionPresetHigh; + } dispatch_async(self.sessionQueue, ^{ [self.session beginConfiguration]; if ([self.session canSetSessionPreset:preset]) { diff --git a/ios/RN/RNCameraManager.m b/ios/RN/RNCameraManager.m index 89f710e..8b0b2a5 100644 --- a/ios/RN/RNCameraManager.m +++ b/ios/RN/RNCameraManager.m @@ -16,6 +16,7 @@ RCT_EXPORT_VIEW_PROPERTY(onCameraReady, RCTDirectEventBlock); RCT_EXPORT_VIEW_PROPERTY(onMountError, RCTDirectEventBlock); RCT_EXPORT_VIEW_PROPERTY(onBarCodeRead, RCTDirectEventBlock); RCT_EXPORT_VIEW_PROPERTY(onFacesDetected, RCTDirectEventBlock); +RCT_EXPORT_VIEW_PROPERTY(onPictureSaved, RCTDirectEventBlock); + (BOOL)requiresMainQueueSetup { @@ -65,13 +66,13 @@ RCT_EXPORT_VIEW_PROPERTY(onFacesDetected, RCTDirectEventBlock); }, @"VideoCodec": [[self class] validCodecTypes], @"BarCodeType" : [[self class] validBarCodeTypes], - @"FaceDetection" : [[self class] faceDetectorConstants] + @"FaceDetection" : [[self class] faceDetectorConstants] }; } - (NSArray *)supportedEvents { - return @[@"onCameraReady", @"onMountError", @"onBarCodeRead", @"onFacesDetected"]; + return @[@"onCameraReady", @"onMountError", @"onBarCodeRead", @"onFacesDetected", @"onPictureSaved"]; } + (NSDictionary *)validCodecTypes @@ -111,6 +112,21 @@ RCT_EXPORT_VIEW_PROPERTY(onFacesDetected, RCTDirectEventBlock); }; } ++ (NSDictionary *)pictureSizes +{ + return @{ + @"3840x2160" : AVCaptureSessionPreset3840x2160, + @"1920x1080" : AVCaptureSessionPreset1920x1080, + @"1280x720" : AVCaptureSessionPreset1280x720, + @"640x480" : AVCaptureSessionPreset640x480, + @"352x288" : AVCaptureSessionPreset352x288, + @"Photo" : AVCaptureSessionPresetPhoto, + @"High" : AVCaptureSessionPresetHigh, + @"Medium" : AVCaptureSessionPresetMedium, + @"Low" : AVCaptureSessionPresetLow + }; +} + + (NSDictionary *)faceDetectorConstants { #if __has_include() @@ -158,12 +174,20 @@ RCT_CUSTOM_VIEW_PROPERTY(zoom, NSNumber, RNCamera) RCT_CUSTOM_VIEW_PROPERTY(whiteBalance, NSInteger, RNCamera) { - [view setWhiteBalance: [RCTConvert NSInteger:json]]; + [view setWhiteBalance:[RCTConvert NSInteger:json]]; [view updateWhiteBalance]; } +RCT_CUSTOM_VIEW_PROPERTY(pictureSize, NSString *, RNCamera) +{ + [view setPictureSize:[[self class] pictureSizes][[RCTConvert NSString:json]]]; + [view updatePictureSize]; +} + + RCT_CUSTOM_VIEW_PROPERTY(faceDetectorEnabled, BOOL, RNCamera) { + view.isDetectingFaces = [RCTConvert BOOL:json]; [view updateFaceDetecting:json]; } @@ -185,7 +209,7 @@ RCT_CUSTOM_VIEW_PROPERTY(faceDetectionClassifications, NSString, RNCamera) RCT_CUSTOM_VIEW_PROPERTY(barCodeScannerEnabled, BOOL, RNCamera) { - view.barCodeReading = [RCTConvert BOOL:json]; + view.isReadingBarCodes = [RCTConvert BOOL:json]; [view setupOrDisableBarcodeScanner]; } @@ -200,29 +224,37 @@ RCT_REMAP_METHOD(takePicture, resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { -#if TARGET_IPHONE_SIMULATOR - NSMutableDictionary *response = [[NSMutableDictionary alloc] init]; - float quality = [options[@"quality"] floatValue]; - NSString *path = [RNFileSystem generatePathInDirectory:[[RNFileSystem cacheDirectoryPath] stringByAppendingPathComponent:@"Camera"] withExtension:@".jpg"]; - UIImage *generatedPhoto = [RNImageUtils generatePhotoOfSize:CGSizeMake(200, 200)]; - NSData *photoData = UIImageJPEGRepresentation(generatedPhoto, quality); - response[@"uri"] = [RNImageUtils writeImage:photoData toPath:path]; - response[@"width"] = @(generatedPhoto.size.width); - response[@"height"] = @(generatedPhoto.size.height); - if ([options[@"base64"] boolValue]) { - response[@"base64"] = [photoData base64EncodedStringWithOptions:0]; - } - resolve(response); -#else [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { RNCamera *view = viewRegistry[reactTag]; if (![view isKindOfClass:[RNCamera class]]) { RCTLogError(@"Invalid view returned from registry, expecting RNCamera, got: %@", view); } else { +#if TARGET_IPHONE_SIMULATOR + NSMutableDictionary *response = [[NSMutableDictionary alloc] init]; + float quality = [options[@"quality"] floatValue]; + NSString *path = [RNFileSystem generatePathInDirectory:[[RNFileSystem cacheDirectoryPath] stringByAppendingPathComponent:@"Camera"] withExtension:@".jpg"]; + UIImage *generatedPhoto = [RNImageUtils generatePhotoOfSize:CGSizeMake(200, 200)]; + BOOL useFastMode = options[@"fastMode"] && [options[@"fastMode"] boolValue]; + if (useFastMode) { + resolve(nil); + } + NSData *photoData = UIImageJPEGRepresentation(generatedPhoto, quality); + response[@"uri"] = [RNImageUtils writeImage:photoData toPath:path]; + response[@"width"] = @(generatedPhoto.size.width); + response[@"height"] = @(generatedPhoto.size.height); + if ([options[@"base64"] boolValue]) { + response[@"base64"] = [photoData base64EncodedStringWithOptions:0]; + } + if (useFastMode) { + [view onPictureSaved:@{@"data": response, @"id": options[@"id"]}]; + } else { + resolve(response); + } +#else [view takePicture:options resolve:resolve reject:reject]; +#endif } }]; -#endif } RCT_REMAP_METHOD(record, @@ -245,6 +277,36 @@ RCT_REMAP_METHOD(record, }]; } +RCT_EXPORT_METHOD(resumePreview:(nonnull NSNumber *)reactTag) +{ +#if TARGET_IPHONE_SIMULATOR + return; +#endif + [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { + RNCamera *view = viewRegistry[reactTag]; + if (![view isKindOfClass:[RNCamera class]]) { + RCTLogError(@"Invalid view returned from registry, expecting RNCamera, got: %@", view); + } else { + [view resumePreview]; + } + }]; +} + +RCT_EXPORT_METHOD(pausePreview:(nonnull NSNumber *)reactTag) +{ +#if TARGET_IPHONE_SIMULATOR + return; +#endif + [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { + RNCamera *view = viewRegistry[reactTag]; + if (![view isKindOfClass:[RNCamera class]]) { + RCTLogError(@"Invalid view returned from registry, expecting RNCamera, got: %@", view); + } else { + [view pausePreview]; + } + }]; +} + RCT_REMAP_METHOD(stopRecording, reactTag:(nonnull NSNumber *)reactTag) { [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { @@ -283,5 +345,14 @@ RCT_EXPORT_METHOD(checkVideoAuthorizationStatus:(RCTPromiseResolveBlock)resolve }]; } +RCT_REMAP_METHOD(getAvailablePictureSizes, + ratio:(NSString *)ratio + reactTag:(nonnull NSNumber *)reactTag + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) +{ + resolve([[[self class] pictureSizes] allKeys]); +} + @end