feat: High quality mode (enableHighQualityPhotos) (#194)

* feat: High Quality photo capture

* prepare photo output for re-used settings

* use high quality captures

* Remove `enableVirtualDeviceFusion` as that is enabled by default

* Clean up configuration, remove default

* format

* Update CameraViewManager.kt

* rename

* Update CameraProps.ts

* Fix overriding `photoSettings`

* Update CameraView+TakePhoto.swift

* Update CameraView+TakePhoto.swift
This commit is contained in:
Marc Rousavy 2021-06-10 13:49:34 +02:00 committed by GitHub
parent 88a30e5723
commit 0e606affce
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 68 additions and 48 deletions

View File

@ -49,9 +49,6 @@ suspend fun CameraView.takePhoto(options: ReadableMap): WritableMap = coroutineS
if (options.hasKey("enableDualCameraFusion")) { if (options.hasKey("enableDualCameraFusion")) {
// TODO enableDualCameraFusion // TODO enableDualCameraFusion
} }
if (options.hasKey("enableVirtualDeviceFusion")) {
// TODO enableVirtualDeviceFusion
}
if (options.hasKey("enableAutoStabilization")) { if (options.hasKey("enableAutoStabilization")) {
// TODO enableAutoStabilization // TODO enableAutoStabilization
} }

View File

@ -39,7 +39,7 @@ import kotlin.math.min
// TODO: Configurable FPS higher than 30 // TODO: Configurable FPS higher than 30
// TODO: High-speed video recordings (export in CameraViewModule::getAvailableVideoDevices(), and set in CameraView::configurePreview()) (120FPS+) // TODO: High-speed video recordings (export in CameraViewModule::getAvailableVideoDevices(), and set in CameraView::configurePreview()) (120FPS+)
// TODO: configureSession() enableDepthData // TODO: configureSession() enableDepthData
// TODO: configureSession() enableHighResolutionCapture // TODO: configureSession() enableHighQualityPhotos
// TODO: configureSession() enablePortraitEffectsMatteDelivery // TODO: configureSession() enablePortraitEffectsMatteDelivery
// TODO: configureSession() colorSpace // TODO: configureSession() colorSpace
@ -55,7 +55,6 @@ import kotlin.math.min
// TODO: takePhoto() photoCodec ("hevc" | "jpeg" | "raw") // TODO: takePhoto() photoCodec ("hevc" | "jpeg" | "raw")
// TODO: takePhoto() qualityPrioritization // TODO: takePhoto() qualityPrioritization
// TODO: takePhoto() enableAutoRedEyeReduction // TODO: takePhoto() enableAutoRedEyeReduction
// TODO: takePhoto() enableVirtualDeviceFusion
// TODO: takePhoto() enableAutoStabilization // TODO: takePhoto() enableAutoStabilization
// TODO: takePhoto() enableAutoDistortionCorrection // TODO: takePhoto() enableAutoDistortionCorrection
// TODO: takePhoto() return with jsi::Value Image reference for faster capture // TODO: takePhoto() return with jsi::Value Image reference for faster capture
@ -66,7 +65,7 @@ class CameraView(context: Context) : FrameLayout(context), LifecycleOwner {
// props that require reconfiguring // props that require reconfiguring
var cameraId: String? = null // this is actually not a react prop directly, but the result of setting device={} var cameraId: String? = null // this is actually not a react prop directly, but the result of setting device={}
var enableDepthData = false var enableDepthData = false
var enableHighResolutionCapture: Boolean? = null var enableHighQualityPhotos: Boolean? = null
var enablePortraitEffectsMatteDelivery = false var enablePortraitEffectsMatteDelivery = false
// use-cases // use-cases
var photo: Boolean? = null var photo: Boolean? = null

View File

@ -51,11 +51,11 @@ class CameraViewManager : SimpleViewManager<CameraView>() {
view.enableDepthData = enableDepthData view.enableDepthData = enableDepthData
} }
@ReactProp(name = "enableHighResolutionCapture") @ReactProp(name = "enableHighQualityPhotos")
fun setEnableHighResolutionCapture(view: CameraView, enableHighResolutionCapture: Boolean?) { fun setEnableHighQualityPhotos(view: CameraView, enableHighQualityPhotos: Boolean?) {
if (view.enableHighResolutionCapture != enableHighResolutionCapture) if (view.enableHighQualityPhotos != enableHighQualityPhotos)
addChangedPropToTransaction(view, "enableHighResolutionCapture") addChangedPropToTransaction(view, "enableHighQualityPhotos")
view.enableHighResolutionCapture = enableHighResolutionCapture view.enableHighQualityPhotos = enableHighQualityPhotos
} }
@ReactProp(name = "enablePortraitEffectsMatteDelivery") @ReactProp(name = "enablePortraitEffectsMatteDelivery")

View File

@ -94,12 +94,21 @@ extension CameraView {
if photo?.boolValue == true { if photo?.boolValue == true {
ReactLogger.log(level: .info, message: "Adding Photo output...") ReactLogger.log(level: .info, message: "Adding Photo output...")
photoOutput = AVCapturePhotoOutput() photoOutput = AVCapturePhotoOutput()
photoOutput!.isDepthDataDeliveryEnabled = photoOutput!.isDepthDataDeliverySupported && enableDepthData
if let enableHighResolutionCapture = self.enableHighResolutionCapture?.boolValue { if enableHighQualityPhotos?.boolValue == true {
photoOutput!.isHighResolutionCaptureEnabled = enableHighResolutionCapture photoOutput!.isHighResolutionCaptureEnabled = true
if #available(iOS 13.0, *) {
photoOutput!.isVirtualDeviceConstituentPhotoDeliveryEnabled = photoOutput!.isVirtualDeviceConstituentPhotoDeliverySupported
photoOutput!.maxPhotoQualityPrioritization = .quality
} else {
photoOutput!.isDualCameraDualPhotoDeliveryEnabled = photoOutput!.isDualCameraDualPhotoDeliverySupported
} }
if #available(iOS 12.0, *) { }
photoOutput!.isPortraitEffectsMatteDeliveryEnabled = photoOutput!.isPortraitEffectsMatteDeliverySupported && self.enablePortraitEffectsMatteDelivery if enableDepthData {
photoOutput!.isDepthDataDeliveryEnabled = photoOutput!.isDepthDataDeliverySupported
}
if #available(iOS 12.0, *), enablePortraitEffectsMatteDelivery {
photoOutput!.isPortraitEffectsMatteDeliveryEnabled = photoOutput!.isPortraitEffectsMatteDeliverySupported
} }
guard captureSession.canAddOutput(photoOutput!) else { guard captureSession.canAddOutput(photoOutput!) else {
invokeOnError(.parameter(.unsupportedOutput(outputDescriptor: "photo-output"))) invokeOnError(.parameter(.unsupportedOutput(outputDescriptor: "photo-output")))

View File

@ -36,19 +36,35 @@ extension CameraView {
} }
} }
var photoSettings = AVCapturePhotoSettings() ReactLogger.log(level: .info, message: "Capturing photo...")
var format: [String: Any]?
// photo codec
if let photoCodecString = options["photoCodec"] as? String { if let photoCodecString = options["photoCodec"] as? String {
guard let photoCodec = AVVideoCodecType(withString: photoCodecString) else { guard let photoCodec = AVVideoCodecType(withString: photoCodecString) else {
promise.reject(error: .capture(.invalidPhotoCodec))
return
}
if photoOutput.availablePhotoCodecTypes.contains(photoCodec) {
photoSettings = AVCapturePhotoSettings(format: [AVVideoCodecKey: photoCodec])
} else {
promise.reject(error: .parameter(.invalid(unionName: "PhotoCodec", receivedValue: photoCodecString))) promise.reject(error: .parameter(.invalid(unionName: "PhotoCodec", receivedValue: photoCodecString)))
return return
} }
if photoOutput.availablePhotoCodecTypes.contains(photoCodec) {
format = [AVVideoCodecKey: photoCodec]
} else {
promise.reject(error: .capture(.invalidPhotoCodec))
return
} }
}
// Create photo settings
let photoSettings = AVCapturePhotoSettings(format: format)
// default, overridable settings if high quality capture was enabled
if self.enableHighQualityPhotos?.boolValue == true {
photoSettings.isHighResolutionPhotoEnabled = true
if #available(iOS 13.0, *) {
photoSettings.photoQualityPrioritization = .quality
}
}
// flash
if videoDeviceInput.device.isFlashAvailable, let flash = options["flash"] as? String { if videoDeviceInput.device.isFlashAvailable, let flash = options["flash"] as? String {
guard let flashMode = AVCaptureDevice.FlashMode(withString: flash) else { guard let flashMode = AVCaptureDevice.FlashMode(withString: flash) else {
promise.reject(error: .parameter(.invalid(unionName: "FlashMode", receivedValue: flash))) promise.reject(error: .parameter(.invalid(unionName: "FlashMode", receivedValue: flash)))
@ -56,16 +72,14 @@ extension CameraView {
} }
photoSettings.flashMode = flashMode photoSettings.flashMode = flashMode
} }
photoSettings.isHighResolutionPhotoEnabled = photoOutput.isHighResolutionCaptureEnabled
if !photoSettings.__availablePreviewPhotoPixelFormatTypes.isEmpty { // depth data
photoSettings.previewPhotoFormat = [kCVPixelBufferPixelFormatTypeKey as String: photoSettings.__availablePreviewPhotoPixelFormatTypes.first!]
}
photoSettings.isDepthDataDeliveryEnabled = photoOutput.isDepthDataDeliveryEnabled photoSettings.isDepthDataDeliveryEnabled = photoOutput.isDepthDataDeliveryEnabled
photoSettings.embedsDepthDataInPhoto = photoSettings.isDepthDataDeliveryEnabled
if #available(iOS 12.0, *) { if #available(iOS 12.0, *) {
photoSettings.isPortraitEffectsMatteDeliveryEnabled = photoOutput.isPortraitEffectsMatteDeliveryEnabled photoSettings.isPortraitEffectsMatteDeliveryEnabled = photoOutput.isPortraitEffectsMatteDeliveryEnabled
photoSettings.embedsPortraitEffectsMatteInPhoto = photoSettings.isPortraitEffectsMatteDeliveryEnabled
} }
// quality prioritization
if #available(iOS 13.0, *), let qualityPrioritization = options["qualityPrioritization"] as? String { if #available(iOS 13.0, *), let qualityPrioritization = options["qualityPrioritization"] as? String {
guard let photoQualityPrioritization = AVCapturePhotoOutput.QualityPrioritization(withString: qualityPrioritization) else { guard let photoQualityPrioritization = AVCapturePhotoOutput.QualityPrioritization(withString: qualityPrioritization) else {
promise.reject(error: .parameter(.invalid(unionName: "QualityPrioritization", receivedValue: qualityPrioritization))) promise.reject(error: .parameter(.invalid(unionName: "QualityPrioritization", receivedValue: qualityPrioritization)))
@ -73,24 +87,26 @@ extension CameraView {
} }
photoSettings.photoQualityPrioritization = photoQualityPrioritization photoSettings.photoQualityPrioritization = photoQualityPrioritization
} }
// red-eye reduction
if #available(iOS 12.0, *), let autoRedEyeReduction = options["enableAutoRedEyeReduction"] as? Bool { if #available(iOS 12.0, *), let autoRedEyeReduction = options["enableAutoRedEyeReduction"] as? Bool {
photoSettings.isAutoRedEyeReductionEnabled = autoRedEyeReduction photoSettings.isAutoRedEyeReductionEnabled = autoRedEyeReduction
} }
if let enableVirtualDeviceFusion = options["enableVirtualDeviceFusion"] as? Bool {
if #available(iOS 13.0, *) { // stabilization
photoSettings.isAutoVirtualDeviceFusionEnabled = enableVirtualDeviceFusion
} else {
photoSettings.isAutoDualCameraFusionEnabled = enableVirtualDeviceFusion
}
}
if let enableAutoStabilization = options["enableAutoStabilization"] as? Bool { if let enableAutoStabilization = options["enableAutoStabilization"] as? Bool {
photoSettings.isAutoStillImageStabilizationEnabled = enableAutoStabilization photoSettings.isAutoStillImageStabilizationEnabled = enableAutoStabilization
} }
// distortion correction
if #available(iOS 14.1, *), let enableAutoDistortionCorrection = options["enableAutoDistortionCorrection"] as? Bool { if #available(iOS 14.1, *), let enableAutoDistortionCorrection = options["enableAutoDistortionCorrection"] as? Bool {
photoSettings.isAutoContentAwareDistortionCorrectionEnabled = enableAutoDistortionCorrection photoSettings.isAutoContentAwareDistortionCorrectionEnabled = enableAutoDistortionCorrection
} }
photoOutput.capturePhoto(with: photoSettings, delegate: PhotoCaptureDelegate(promise: promise)) photoOutput.capturePhoto(with: photoSettings, delegate: PhotoCaptureDelegate(promise: promise))
// Assume that `takePhoto` is always called with the same parameters, so prepare the next call too.
photoOutput.setPreparedPhotoSettingsArray([photoSettings], completionHandler: nil)
} }
} }
} }

View File

@ -21,7 +21,7 @@ import UIKit
private let propsThatRequireReconfiguration = ["cameraId", private let propsThatRequireReconfiguration = ["cameraId",
"enableDepthData", "enableDepthData",
"enableHighResolutionCapture", "enableHighQualityPhotos",
"enablePortraitEffectsMatteDelivery", "enablePortraitEffectsMatteDelivery",
"preset", "preset",
"photo", "photo",
@ -40,7 +40,7 @@ public final class CameraView: UIView {
// props that require reconfiguring // props that require reconfiguring
@objc var cameraId: NSString? @objc var cameraId: NSString?
@objc var enableDepthData = false @objc var enableDepthData = false
@objc var enableHighResolutionCapture: NSNumber? // nullable bool @objc var enableHighQualityPhotos: NSNumber? // nullable bool
@objc var enablePortraitEffectsMatteDelivery = false @objc var enablePortraitEffectsMatteDelivery = false
@objc var preset: String? @objc var preset: String?
// use cases // use cases

View File

@ -25,7 +25,7 @@ RCT_EXTERN_METHOD(getAvailableCameraDevices:(RCTPromiseResolveBlock)resolve reje
RCT_EXPORT_VIEW_PROPERTY(isActive, BOOL); RCT_EXPORT_VIEW_PROPERTY(isActive, BOOL);
RCT_EXPORT_VIEW_PROPERTY(cameraId, NSString); RCT_EXPORT_VIEW_PROPERTY(cameraId, NSString);
RCT_EXPORT_VIEW_PROPERTY(enableDepthData, BOOL); RCT_EXPORT_VIEW_PROPERTY(enableDepthData, BOOL);
RCT_EXPORT_VIEW_PROPERTY(enableHighResolutionCapture, NSNumber); // nullable bool RCT_EXPORT_VIEW_PROPERTY(enableHighQualityPhotos, NSNumber); // nullable bool
RCT_EXPORT_VIEW_PROPERTY(enablePortraitEffectsMatteDelivery, BOOL); RCT_EXPORT_VIEW_PROPERTY(enablePortraitEffectsMatteDelivery, BOOL);
// use cases // use cases
RCT_EXPORT_VIEW_PROPERTY(photo, NSNumber); // nullable bool RCT_EXPORT_VIEW_PROPERTY(photo, NSNumber); // nullable bool

View File

@ -135,11 +135,17 @@ export interface CameraProps extends ViewProps {
*/ */
enablePortraitEffectsMatteDelivery?: boolean; enablePortraitEffectsMatteDelivery?: boolean;
/** /**
* Indicates whether the photo render pipeline should be configured to deliver high resolution still images * Indicates whether the Camera should prepare the photo pipeline to provide maximum quality photos.
*
* This enables:
* * High Resolution Capture ([`isHighResolutionCaptureEnabled`](https://developer.apple.com/documentation/avfoundation/avcapturephotooutput/1648721-ishighresolutioncaptureenabled))
* * Virtual Device fusion for greater detail ([`isVirtualDeviceConstituentPhotoDeliveryEnabled`](https://developer.apple.com/documentation/avfoundation/avcapturephotooutput/3192189-isvirtualdeviceconstituentphotod))
* * Dual Device fusion for greater detail ([`isDualCameraDualPhotoDeliveryEnabled`](https://developer.apple.com/documentation/avfoundation/avcapturephotosettings/2873917-isdualcameradualphotodeliveryena))
* * Sets the maximum quality prioritization to `.quality` ([`maxPhotoQualityPrioritization`](https://developer.apple.com/documentation/avfoundation/avcapturephotooutput/3182995-maxphotoqualityprioritization))
* *
* @default false * @default false
*/ */
enableHighResolutionCapture?: boolean; enableHighQualityPhotos?: boolean;
//#region Events //#region Events
/** /**

View File

@ -24,13 +24,6 @@ export interface TakePhotoOptions {
* @default false * @default false
*/ */
enableAutoRedEyeReduction?: boolean; enableAutoRedEyeReduction?: boolean;
/**
* Specifies whether a virtual multi-cam device should capture images from all containing physical cameras
* to create a combined, higher quality image.
*
* @see [`isAutoVirtualDeviceFusionEnabled`](https://developer.apple.com/documentation/avfoundation/avcapturephotosettings/3192192-isautovirtualdevicefusionenabled)
*/
enableVirtualDeviceFusion?: boolean;
/** /**
* Indicates whether still image stabilization will be employed when capturing the photo * Indicates whether still image stabilization will be employed when capturing the photo
* *