diff --git a/apps/simple-camera/__tests__/visioncamera.controller.harness.ts b/apps/simple-camera/__tests__/visioncamera.controller.harness.ts index dd17e172ae..a5a682fb58 100644 --- a/apps/simple-camera/__tests__/visioncamera.controller.harness.ts +++ b/apps/simple-camera/__tests__/visioncamera.controller.harness.ts @@ -133,9 +133,6 @@ describe('VisionCamera - Controller', () => { await session.start() try { - // TODO: Add setTorchMode('on', STRENGTH) test when we expose something like - // CameraDevice.supportsTorchStrength - currently this might throw on - // some phones without a way to check upfront if it supports setting strength! await controller.setTorchMode('on') expect(controller.torchMode).toBe('on') @@ -146,6 +143,46 @@ describe('VisionCamera - Controller', () => { } }) + it('sets torchMode with a specific strength when the device supports it', async () => { + if (backDevice.maxTorchStrength === 0) { + console.log( + '[SKIP] torchStrength: device only supports binary torch (maxTorchStrength === 0)', + ) + return + } + const session = await VisionCamera.createCameraSession(false) + const photoOutput = VisionCamera.createPhotoOutput({ + targetResolution: CommonResolutions.HD_4_3, + containerFormat: 'jpeg', + quality: 0.8, + qualityPrioritization: 'balanced', + }) + const [controller] = await session.configure([ + { + input: backDevice, + outputs: [{ output: photoOutput, mirrorMode: 'auto' }], + constraints: [], + }, + ]) + if (controller == null) throw new Error('no controller') + await session.start() + + try { + const target = backDevice.maxTorchStrength / 2 + await controller.setTorchMode('on', target) + expect(controller.torchMode).toBe('on') + expect(controller.torchStrength).toBeGreaterThan(0) + expect(controller.torchStrength).toBeLessThanOrEqual( + backDevice.maxTorchStrength, + ) + + await controller.setTorchMode('off') + expect(controller.torchMode).toBe('off') + } finally { + await session.stop() + } + }) + it('sets exposure bias to min/max when the device supports it', async () => { if (!backDevice.supportsExposureBias) { console.log('[SKIP] exposureBias: not supported on this device') diff --git a/packages/react-native-vision-camera/android/src/main/java/com/margelo/nitro/camera/hybrids/inputs/HybridCameraDevice.kt b/packages/react-native-vision-camera/android/src/main/java/com/margelo/nitro/camera/hybrids/inputs/HybridCameraDevice.kt index 8353f8e15a..9e1e6c8ce5 100644 --- a/packages/react-native-vision-camera/android/src/main/java/com/margelo/nitro/camera/hybrids/inputs/HybridCameraDevice.kt +++ b/packages/react-native-vision-camera/android/src/main/java/com/margelo/nitro/camera/hybrids/inputs/HybridCameraDevice.kt @@ -168,6 +168,14 @@ class HybridCameraDevice( override val hasTorch: Boolean get() = cameraInfo.hasFlashUnit() + override val maxTorchStrength: Double + // CameraX reports `maxTorchStrengthLevel == 1` for devices that only + // support binary torch (on/off) and `> 1` for devices that accept a + // strength level. Our public `setTorchMode(mode, strength?)` API + // normalizes the incoming strength to 0..1 before multiplying by + // `maxTorchStrengthLevel`, so the public maximum is always `1.0` + // when strength is configurable, and `0.0` otherwise. + get() = if (cameraInfo.maxTorchStrengthLevel > 1) 1.0 else 0.0 override val supportsLowLightBoost: Boolean get() = cameraInfo.isLowLightBoostSupported diff --git a/packages/react-native-vision-camera/android/src/main/java/com/margelo/nitro/camera/hybrids/inputs/HybridPhysicalCameraDevice.kt b/packages/react-native-vision-camera/android/src/main/java/com/margelo/nitro/camera/hybrids/inputs/HybridPhysicalCameraDevice.kt index 353deed164..0110028b29 100644 --- a/packages/react-native-vision-camera/android/src/main/java/com/margelo/nitro/camera/hybrids/inputs/HybridPhysicalCameraDevice.kt +++ b/packages/react-native-vision-camera/android/src/main/java/com/margelo/nitro/camera/hybrids/inputs/HybridPhysicalCameraDevice.kt @@ -90,6 +90,7 @@ class HybridPhysicalCameraDevice( override val maxWhiteBalanceGain: Double = 0.0 override val hasFlash: Boolean = false override val hasTorch: Boolean = false + override val maxTorchStrength: Double = 0.0 override val supportsLowLightBoost: Boolean = false override val minZoom: Double = 0.0 override val maxZoom: Double = 0.0 diff --git a/packages/react-native-vision-camera/ios/Hybrid Objects/Inputs/HybridCameraDevice.swift b/packages/react-native-vision-camera/ios/Hybrid Objects/Inputs/HybridCameraDevice.swift index ff8b64d9f0..57b2ebb5d7 100644 --- a/packages/react-native-vision-camera/ios/Hybrid Objects/Inputs/HybridCameraDevice.swift +++ b/packages/react-native-vision-camera/ios/Hybrid Objects/Inputs/HybridCameraDevice.swift @@ -195,6 +195,13 @@ final class HybridCameraDevice: HybridCameraDeviceSpec, NativeCameraDevice { return device.hasTorch } + var maxTorchStrength: Double { + // AVCaptureDevice.maxAvailableTorchLevel is a platform constant (1.0). + // When the device has a torch, torch strength is always configurable via + // setTorchModeOn(level:) in the 0...1 range. + return device.hasTorch ? Double(AVCaptureDevice.maxAvailableTorchLevel) : 0.0 + } + var supportsLowLightBoost: Bool { return device.isLowLightBoostSupported } diff --git a/packages/react-native-vision-camera/nitrogen/generated/android/c++/JHybridCameraDeviceSpec.cpp b/packages/react-native-vision-camera/nitrogen/generated/android/c++/JHybridCameraDeviceSpec.cpp index 5acb643ec8..6d06345ff7 100644 --- a/packages/react-native-vision-camera/nitrogen/generated/android/c++/JHybridCameraDeviceSpec.cpp +++ b/packages/react-native-vision-camera/nitrogen/generated/android/c++/JHybridCameraDeviceSpec.cpp @@ -308,6 +308,11 @@ namespace margelo::nitro::camera { auto __result = method(_javaPart); return static_cast(__result); } + double JHybridCameraDeviceSpec::getMaxTorchStrength() { + static const auto method = _javaPart->javaClassStatic()->getMethod("getMaxTorchStrength"); + auto __result = method(_javaPart); + return __result; + } bool JHybridCameraDeviceSpec::getSupportsLowLightBoost() { static const auto method = _javaPart->javaClassStatic()->getMethod("getSupportsLowLightBoost"); auto __result = method(_javaPart); diff --git a/packages/react-native-vision-camera/nitrogen/generated/android/c++/JHybridCameraDeviceSpec.hpp b/packages/react-native-vision-camera/nitrogen/generated/android/c++/JHybridCameraDeviceSpec.hpp index 1394a67c23..cdf5332c7c 100644 --- a/packages/react-native-vision-camera/nitrogen/generated/android/c++/JHybridCameraDeviceSpec.hpp +++ b/packages/react-native-vision-camera/nitrogen/generated/android/c++/JHybridCameraDeviceSpec.hpp @@ -82,6 +82,7 @@ namespace margelo::nitro::camera { bool getSupportsWhiteBalanceLocking() override; bool getHasFlash() override; bool getHasTorch() override; + double getMaxTorchStrength() override; bool getSupportsLowLightBoost() override; double getMinZoom() override; double getMaxZoom() override; diff --git a/packages/react-native-vision-camera/nitrogen/generated/android/kotlin/com/margelo/nitro/camera/HybridCameraDeviceSpec.kt b/packages/react-native-vision-camera/nitrogen/generated/android/kotlin/com/margelo/nitro/camera/HybridCameraDeviceSpec.kt index d0a824772f..094c545d19 100644 --- a/packages/react-native-vision-camera/nitrogen/generated/android/kotlin/com/margelo/nitro/camera/HybridCameraDeviceSpec.kt +++ b/packages/react-native-vision-camera/nitrogen/generated/android/kotlin/com/margelo/nitro/camera/HybridCameraDeviceSpec.kt @@ -153,6 +153,10 @@ abstract class HybridCameraDeviceSpec: HybridObject() { @get:Keep abstract val hasTorch: Boolean + @get:DoNotStrip + @get:Keep + abstract val maxTorchStrength: Double + @get:DoNotStrip @get:Keep abstract val supportsLowLightBoost: Boolean diff --git a/packages/react-native-vision-camera/nitrogen/generated/ios/c++/HybridCameraDeviceSpecSwift.hpp b/packages/react-native-vision-camera/nitrogen/generated/ios/c++/HybridCameraDeviceSpecSwift.hpp index 37b4822c57..ac0d9a02ca 100644 --- a/packages/react-native-vision-camera/nitrogen/generated/ios/c++/HybridCameraDeviceSpecSwift.hpp +++ b/packages/react-native-vision-camera/nitrogen/generated/ios/c++/HybridCameraDeviceSpecSwift.hpp @@ -218,6 +218,9 @@ namespace margelo::nitro::camera { inline bool getHasTorch() noexcept override { return _swiftPart.hasTorch(); } + inline double getMaxTorchStrength() noexcept override { + return _swiftPart.getMaxTorchStrength(); + } inline bool getSupportsLowLightBoost() noexcept override { return _swiftPart.getSupportsLowLightBoost(); } diff --git a/packages/react-native-vision-camera/nitrogen/generated/ios/swift/HybridCameraDeviceSpec.swift b/packages/react-native-vision-camera/nitrogen/generated/ios/swift/HybridCameraDeviceSpec.swift index 421263f45a..bc2a32d09d 100644 --- a/packages/react-native-vision-camera/nitrogen/generated/ios/swift/HybridCameraDeviceSpec.swift +++ b/packages/react-native-vision-camera/nitrogen/generated/ios/swift/HybridCameraDeviceSpec.swift @@ -42,6 +42,7 @@ public protocol HybridCameraDeviceSpec_protocol: HybridObject { var supportsWhiteBalanceLocking: Bool { get } var hasFlash: Bool { get } var hasTorch: Bool { get } + var maxTorchStrength: Double { get } var supportsLowLightBoost: Bool { get } var minZoom: Double { get } var maxZoom: Double { get } diff --git a/packages/react-native-vision-camera/nitrogen/generated/ios/swift/HybridCameraDeviceSpec_cxx.swift b/packages/react-native-vision-camera/nitrogen/generated/ios/swift/HybridCameraDeviceSpec_cxx.swift index 3dc81a38e6..9f37c12d8c 100644 --- a/packages/react-native-vision-camera/nitrogen/generated/ios/swift/HybridCameraDeviceSpec_cxx.swift +++ b/packages/react-native-vision-camera/nitrogen/generated/ios/swift/HybridCameraDeviceSpec_cxx.swift @@ -393,6 +393,13 @@ open class HybridCameraDeviceSpec_cxx { } } + public final var maxTorchStrength: Double { + @inline(__always) + get { + return self.__implementation.maxTorchStrength + } + } + public final var supportsLowLightBoost: Bool { @inline(__always) get { diff --git a/packages/react-native-vision-camera/nitrogen/generated/shared/c++/HybridCameraDeviceSpec.cpp b/packages/react-native-vision-camera/nitrogen/generated/shared/c++/HybridCameraDeviceSpec.cpp index e65e9dbf35..dc5f375755 100644 --- a/packages/react-native-vision-camera/nitrogen/generated/shared/c++/HybridCameraDeviceSpec.cpp +++ b/packages/react-native-vision-camera/nitrogen/generated/shared/c++/HybridCameraDeviceSpec.cpp @@ -46,6 +46,7 @@ namespace margelo::nitro::camera { prototype.registerHybridGetter("supportsWhiteBalanceLocking", &HybridCameraDeviceSpec::getSupportsWhiteBalanceLocking); prototype.registerHybridGetter("hasFlash", &HybridCameraDeviceSpec::getHasFlash); prototype.registerHybridGetter("hasTorch", &HybridCameraDeviceSpec::getHasTorch); + prototype.registerHybridGetter("maxTorchStrength", &HybridCameraDeviceSpec::getMaxTorchStrength); prototype.registerHybridGetter("supportsLowLightBoost", &HybridCameraDeviceSpec::getSupportsLowLightBoost); prototype.registerHybridGetter("minZoom", &HybridCameraDeviceSpec::getMinZoom); prototype.registerHybridGetter("maxZoom", &HybridCameraDeviceSpec::getMaxZoom); diff --git a/packages/react-native-vision-camera/nitrogen/generated/shared/c++/HybridCameraDeviceSpec.hpp b/packages/react-native-vision-camera/nitrogen/generated/shared/c++/HybridCameraDeviceSpec.hpp index 5e7ce2815b..e0d28a89fc 100644 --- a/packages/react-native-vision-camera/nitrogen/generated/shared/c++/HybridCameraDeviceSpec.hpp +++ b/packages/react-native-vision-camera/nitrogen/generated/shared/c++/HybridCameraDeviceSpec.hpp @@ -114,6 +114,7 @@ namespace margelo::nitro::camera { virtual bool getSupportsWhiteBalanceLocking() = 0; virtual bool getHasFlash() = 0; virtual bool getHasTorch() = 0; + virtual double getMaxTorchStrength() = 0; virtual bool getSupportsLowLightBoost() = 0; virtual double getMinZoom() = 0; virtual double getMaxZoom() = 0; diff --git a/packages/react-native-vision-camera/src/specs/inputs/CameraDevice.nitro.ts b/packages/react-native-vision-camera/src/specs/inputs/CameraDevice.nitro.ts index 18e07ebe01..f0c398c28e 100644 --- a/packages/react-native-vision-camera/src/specs/inputs/CameraDevice.nitro.ts +++ b/packages/react-native-vision-camera/src/specs/inputs/CameraDevice.nitro.ts @@ -492,6 +492,21 @@ export interface CameraDevice * @see {@linkcode CameraController.setTorchMode | setTorchMode(...)} */ readonly hasTorch: boolean + /** + * The maximum `strength` value accepted by + * {@linkcode CameraController.setTorchMode | setTorchMode('on', strength)}. + * + * - `0` — Configurable torch strength is **not supported** on this device. + * You may still call {@linkcode CameraController.setTorchMode | setTorchMode('on')} + * / `setTorchMode('off')` to toggle the torch, but passing a specific + * {@linkcode CameraController.setTorchMode | strength} value is not supported + * and will throw. + * - `> 0` — Strength is configurable in the normalized range + * from `0` (off) to `maxTorchStrength` (brightest). + * + * @see {@linkcode CameraController.setTorchMode | setTorchMode(...)} + */ + readonly maxTorchStrength: number // pragma MARK: Low Light Boost /**