diff --git a/android/src/main/java/com/mrousavy/camera/CameraViewModule.kt b/android/src/main/java/com/mrousavy/camera/CameraViewModule.kt index 3281e50..e9b2b8e 100644 --- a/android/src/main/java/com/mrousavy/camera/CameraViewModule.kt +++ b/android/src/main/java/com/mrousavy/camera/CameraViewModule.kt @@ -173,7 +173,7 @@ class CameraViewModule(reactContext: ReactApplicationContext) : ReactContextBase map.putDouble("minZoom", 1.0) map.putDouble("maxZoom", maxScalerZoom.toDouble()) } - map.putDouble("neutralZoom", characteristics.neutralZoomPercent.toDouble()) + map.putDouble("neutralZoom", 1.0) // TODO: Optimize? val maxImageOutputSize = cameraConfig.outputFormats diff --git a/android/src/main/java/com/mrousavy/camera/utils/CameraCharacteristicsUtils.kt b/android/src/main/java/com/mrousavy/camera/utils/CameraCharacteristicsUtils.kt index dce89ff..f9a39f5 100644 --- a/android/src/main/java/com/mrousavy/camera/utils/CameraCharacteristicsUtils.kt +++ b/android/src/main/java/com/mrousavy/camera/utils/CameraCharacteristicsUtils.kt @@ -56,25 +56,3 @@ fun CameraCharacteristics.getFieldOfView(): Double { return 2 * atan(sensorSize.bigger / (focalLengths[0] * 2)) * (180 / PI) } - -fun CameraCharacteristics.supportsFps(fps: Int): Boolean { - return this.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES)!! - .any { it.upper >= fps && it.lower <= fps } -} - -/** - * Get the value at which the Zoom is at neutral state (wide-angle camera zoom 0) (in percent, between 0.0-1.0) - * - * * On single-camera physical devices this value will always be 0 - * * On devices with multiple cameras, e.g. triple-camera, this value will be a value between 0.0 and 1.0, where the field-of-view and zoom looks "neutral" - */ -val CameraCharacteristics.neutralZoomPercent: Float - get() { - val zoomRange = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) - this.get(CameraCharacteristics.CONTROL_ZOOM_RATIO_RANGE) - else null - return if (zoomRange != null) - ((1.0f - zoomRange.lower) / (zoomRange.upper - zoomRange.lower)) - else - 0.0f - } diff --git a/example/src/CameraPage.tsx b/example/src/CameraPage.tsx index f9e92af..b7b5e43 100644 --- a/example/src/CameraPage.tsx +++ b/example/src/CameraPage.tsx @@ -95,18 +95,23 @@ export const CameraPage: NavigationFunctionComponent = ({ componentId }) => { }, [formats, fps, enableHdr]); //#region Animated Zoom - const formatMaxZoom = format?.maxZoom ?? 1; - const maxZoomFactor = Math.min(formatMaxZoom, MAX_ZOOM_FACTOR); - const neutralZoom = device?.neutralZoom ?? 0; - const neutralZoomScaled = (neutralZoom / maxZoomFactor) * formatMaxZoom; - const maxZoomScaled = (1 / formatMaxZoom) * maxZoomFactor; + // This just maps the zoom factor to a percentage value. + // so e.g. for [min, neutr., max] values [1, 2, 128] this would result in [0, 0.0081, 1] + const minZoomFactor = device?.minZoom ?? 1; + const neutralZoomFactor = device?.neutralZoom ?? 1; + const maxZoomFactor = device?.maxZoom ?? 1; + const maxZoomFactorClamped = Math.min(maxZoomFactor, MAX_ZOOM_FACTOR); - const cameraAnimatedProps = useAnimatedProps( - () => ({ - zoom: interpolate(zoom.value, [0, neutralZoomScaled, 1], [0, neutralZoom, maxZoomScaled], Extrapolate.CLAMP), - }), - [maxZoomScaled, neutralZoom, neutralZoomScaled, zoom], - ); + const neutralZoomOut = (neutralZoomFactor - minZoomFactor) / (maxZoomFactor - minZoomFactor); + const neutralZoomIn = (neutralZoomOut / maxZoomFactorClamped) * maxZoomFactor; + const maxZoomOut = maxZoomFactorClamped / maxZoomFactor; + + const cameraAnimatedProps = useAnimatedProps(() => { + const z = interpolate(zoom.value, [0, neutralZoomIn, 1], [0, neutralZoomOut, maxZoomOut], Extrapolate.CLAMP); + return { + zoom: isNaN(z) ? 0 : z, + }; + }, [maxZoomOut, neutralZoomOut, neutralZoomIn, zoom]); //#endregion //#region Callbacks @@ -155,8 +160,8 @@ export const CameraPage: NavigationFunctionComponent = ({ componentId }) => { //#region Effects useEffect(() => { // Run everytime the neutralZoomScaled value changes. (reset zoom when device changes) - zoom.value = neutralZoomScaled; - }, [neutralZoomScaled, zoom]); + zoom.value = neutralZoomIn; + }, [neutralZoomIn, zoom]); //#endregion //#region Pinch to Zoom Gesture diff --git a/ios/CameraView+AVCaptureSession.swift b/ios/CameraView+AVCaptureSession.swift index 278958b..f675f4d 100644 --- a/ios/CameraView+AVCaptureSession.swift +++ b/ios/CameraView+AVCaptureSession.swift @@ -68,7 +68,6 @@ extension CameraView { guard let videoDevice = AVCaptureDevice(uniqueID: cameraId) else { return invokeOnError(.device(.invalid)) } - zoom = NSNumber(value: Double(videoDevice.neutralZoomPercent)) videoDeviceInput = try AVCaptureDeviceInput(device: videoDevice) guard captureSession.canAddInput(videoDeviceInput!) else { return invokeOnError(.parameter(.unsupportedInput(inputDescriptor: "video-input"))) diff --git a/ios/CameraViewManager.swift b/ios/CameraViewManager.swift index ba94f00..3890e8a 100644 --- a/ios/CameraViewManager.swift +++ b/ios/CameraViewManager.swift @@ -96,8 +96,8 @@ final class CameraViewManager: RCTViewManager { "hasFlash": $0.hasFlash, "hasTorch": $0.hasTorch, "minZoom": $0.minAvailableVideoZoomFactor, + "neutralZoom": $0.neutralZoomFactor, "maxZoom": $0.maxAvailableVideoZoomFactor, - "neutralZoom": $0.neutralZoomPercent, "isMultiCam": $0.isMultiCam, "supportsDepthCapture": false, // TODO: supportsDepthCapture "supportsRawCapture": false, // TODO: supportsRawCapture diff --git a/ios/Extensions/AVCaptureDevice+neutralZoom.swift b/ios/Extensions/AVCaptureDevice+neutralZoom.swift index 8112896..7660c02 100644 --- a/ios/Extensions/AVCaptureDevice+neutralZoom.swift +++ b/ios/Extensions/AVCaptureDevice+neutralZoom.swift @@ -9,6 +9,12 @@ import AVFoundation extension AVCaptureDevice { + /** + Get the value at which the Zoom factor is neutral. + + For normal wide-angle devices, this is always going to be 1.0, since this is the default scale. + For devices with an ultra-wide-angle camera, this value is going to be the value where the wide-angle device will switch over. + */ var neutralZoomFactor: CGFloat { if #available(iOS 13.0, *) { if let indexOfWideAngle = self.constituentDevices.firstIndex(where: { $0.deviceType == .builtInWideAngleCamera }) { @@ -19,14 +25,4 @@ extension AVCaptureDevice { } return 1.0 } - - /** - Get the value at which the Zoom value is neutral, in percent (0.0-1.0) - - * On single-camera physical devices, this value will always be 0.0 - * On devices with multiple cameras, e.g. triple-camera, this value will be a value between 0.0 and 1.0, where the field-of-view and zoom looks "neutral" - */ - var neutralZoomPercent: CGFloat { - return (neutralZoomFactor - minAvailableVideoZoomFactor) / (maxAvailableVideoZoomFactor - minAvailableVideoZoomFactor) - } } diff --git a/src/CameraDevice.ts b/src/CameraDevice.ts index 10c0a2a..238e28a 100644 --- a/src/CameraDevice.ts +++ b/src/CameraDevice.ts @@ -228,12 +228,20 @@ export interface CameraDevice { */ maxZoom: number; /** - * The zoom percentage (`0.0`-`1.0`) where the camera is "neutral". + * The zoom factor where the camera is "neutral". * - * * For single-physical cameras this property is always `0.0`. - * * For multi cameras this property is a value between `0.0` and `1.0`, where the camera is in wide-angle mode and hasn't switched to the ultra-wide (`0.5`x zoom) or telephoto camera yet. + * * For single-physical cameras this property is always `1.0`. + * * For multi cameras this property is a value between `minZoom` and `maxZoom`, where the camera is in _wide-angle_ mode and hasn't switched to the _ultra-wide-angle_ (`0.5`x zoom) or telephoto camera yet. * * Use this value as an initial value for the zoom property if you implement custom zoom. (e.g. reanimated shared value should be initially set to this value) + * @example + * const device = ... + * + * const neutralZoomPercent = (device.neutralZoom - device.minZoom) / (device.maxZoom - device.minZoom) + * const zoomFactor = useSharedValue(neutralZoomPercent) // <-- initial value so it doesn't start at ultra-wide + * const cameraProps = useAnimatedProps(() => ({ + * zoom: zoomFactor.value + * })) */ neutralZoom: number; /**