From e16b5703df906c8c1e38395a1cb4efacec96d801 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Tue, 30 Jun 2026 12:07:35 +0200 Subject: [PATCH 1/4] feat: Support `getDefaultCamera('external')` on Android too --- .../com/margelo/nitro/camera/HybridCameraDeviceFactory.kt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/react-native-vision-camera/android/src/main/java/com/margelo/nitro/camera/HybridCameraDeviceFactory.kt b/packages/react-native-vision-camera/android/src/main/java/com/margelo/nitro/camera/HybridCameraDeviceFactory.kt index f25c568cf9..550e545619 100644 --- a/packages/react-native-vision-camera/android/src/main/java/com/margelo/nitro/camera/HybridCameraDeviceFactory.kt +++ b/packages/react-native-vision-camera/android/src/main/java/com/margelo/nitro/camera/HybridCameraDeviceFactory.kt @@ -3,10 +3,12 @@ package com.margelo.nitro.camera import android.content.Context import android.content.SharedPreferences import android.util.Log +import androidx.annotation.OptIn import androidx.camera.camera2.adapter.CameraInfoAdapter.Companion.cameraId import androidx.camera.core.CameraIdentifier import androidx.camera.core.CameraPresenceListener import androidx.camera.core.CameraSelector +import androidx.camera.core.ExperimentalLensFacing import androidx.camera.extensions.ExtensionMode import androidx.camera.extensions.ExtensionsManager import androidx.camera.lifecycle.ProcessCameraProvider @@ -108,11 +110,16 @@ class HybridCameraDeviceFactory( } } + @OptIn(ExperimentalLensFacing::class) override fun getDefaultCamera(position: CameraPosition): HybridCameraDeviceSpec? { val selector = when (position) { CameraPosition.FRONT -> CameraSelector.DEFAULT_FRONT_CAMERA CameraPosition.BACK -> CameraSelector.DEFAULT_BACK_CAMERA + CameraPosition.EXTERNAL -> + CameraSelector.Builder() + .requireLensFacing(CameraSelector.LENS_FACING_EXTERNAL) + .build() else -> return null } try { From 1b6f921f6086e7b743e7865e2c1d3687c3aec0b4 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Tue, 30 Jun 2026 12:12:57 +0200 Subject: [PATCH 2/4] fix: Replace `CameraPosition` with `TargetCameraPosition` --- apps/simple-camera/ios/Podfile.lock | 4 +- .../nitro/camera/HybridCameraDeviceFactory.kt | 12 +-- .../Converters/AV+CameraPosition.swift | 14 ---- .../HybridCameraDeviceFactory.swift | 34 ++++---- .../c++/JHybridCameraDeviceFactorySpec.cpp | 14 ++-- .../c++/JHybridCameraDeviceFactorySpec.hpp | 2 +- .../android/c++/JTargetCameraPosition.hpp | 61 ++++++++++++++ .../camera/HybridCameraDeviceFactorySpec.kt | 2 +- .../nitro/camera/TargetCameraPosition.kt | 24 ++++++ .../ios/VisionCamera-Swift-Cxx-Umbrella.hpp | 3 + .../HybridCameraDeviceFactorySpecSwift.hpp | 8 +- .../swift/HybridCameraDeviceFactorySpec.swift | 2 +- .../HybridCameraDeviceFactorySpec_cxx.swift | 2 +- .../ios/swift/TargetCameraPosition.swift | 44 ++++++++++ .../c++/HybridCameraDeviceFactorySpec.hpp | 8 +- .../shared/c++/TargetCameraPosition.hpp | 80 +++++++++++++++++++ .../src/CameraDevices.ts | 7 +- .../src/specs/common-types/CameraPosition.ts | 10 ++- .../specs/inputs/CameraDeviceFactory.nitro.ts | 9 ++- 19 files changed, 275 insertions(+), 65 deletions(-) create mode 100644 packages/react-native-vision-camera/nitrogen/generated/android/c++/JTargetCameraPosition.hpp create mode 100644 packages/react-native-vision-camera/nitrogen/generated/android/kotlin/com/margelo/nitro/camera/TargetCameraPosition.kt create mode 100644 packages/react-native-vision-camera/nitrogen/generated/ios/swift/TargetCameraPosition.swift create mode 100644 packages/react-native-vision-camera/nitrogen/generated/shared/c++/TargetCameraPosition.hpp diff --git a/apps/simple-camera/ios/Podfile.lock b/apps/simple-camera/ios/Podfile.lock index 727225291f..c7b3406c3c 100644 --- a/apps/simple-camera/ios/Podfile.lock +++ b/apps/simple-camera/ios/Podfile.lock @@ -72,7 +72,7 @@ PODS: - ReactCommon/turbomodule/core - ReactNativeDependencies - Yoga - - NitroModules (0.35.9): + - NitroModules (0.36.1): - hermes-engine - RCTRequired - RCTTypeSafety @@ -2762,7 +2762,7 @@ SPEC CHECKSUMS: MLKitVision: 39a5a812db83c4a0794445088e567f3631c11961 nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 NitroImage: 33c4698f86b081bd3d0245223f34ea561b0934ea - NitroModules: 16bc17a076b12304d608f7c915b9d321f56dfc19 + NitroModules: b4174dd303728e16ad1afb79f64c1a5c69a3b373 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 RCTDeprecation: a4c521821fab57cbb125b36effe84d897d0dfa12 RCTRequired: 9f3a7e5645d4bc3f551593de7550bb66ab6e42bc diff --git a/packages/react-native-vision-camera/android/src/main/java/com/margelo/nitro/camera/HybridCameraDeviceFactory.kt b/packages/react-native-vision-camera/android/src/main/java/com/margelo/nitro/camera/HybridCameraDeviceFactory.kt index 550e545619..f398d4c7c6 100644 --- a/packages/react-native-vision-camera/android/src/main/java/com/margelo/nitro/camera/HybridCameraDeviceFactory.kt +++ b/packages/react-native-vision-camera/android/src/main/java/com/margelo/nitro/camera/HybridCameraDeviceFactory.kt @@ -111,16 +111,16 @@ class HybridCameraDeviceFactory( } @OptIn(ExperimentalLensFacing::class) - override fun getDefaultCamera(position: CameraPosition): HybridCameraDeviceSpec? { + override fun getDefaultCamera(position: TargetCameraPosition): HybridCameraDeviceSpec? { val selector = when (position) { - CameraPosition.FRONT -> CameraSelector.DEFAULT_FRONT_CAMERA - CameraPosition.BACK -> CameraSelector.DEFAULT_BACK_CAMERA - CameraPosition.EXTERNAL -> - CameraSelector.Builder() + TargetCameraPosition.FRONT -> CameraSelector.DEFAULT_FRONT_CAMERA + TargetCameraPosition.BACK -> CameraSelector.DEFAULT_BACK_CAMERA + TargetCameraPosition.EXTERNAL -> + CameraSelector + .Builder() .requireLensFacing(CameraSelector.LENS_FACING_EXTERNAL) .build() - else -> return null } try { val defaultCamera = cameraProvider.getCameraInfo(selector) diff --git a/packages/react-native-vision-camera/ios/Extensions/Converters/AV+CameraPosition.swift b/packages/react-native-vision-camera/ios/Extensions/Converters/AV+CameraPosition.swift index 79b6f42c73..7498411d67 100644 --- a/packages/react-native-vision-camera/ios/Extensions/Converters/AV+CameraPosition.swift +++ b/packages/react-native-vision-camera/ios/Extensions/Converters/AV+CameraPosition.swift @@ -22,18 +22,4 @@ extension CameraPosition { self = .unspecified } } - - func toAVCaptureDevicePosition() -> AVCaptureDevice.Position { - switch self { - case .back: - return .back - case .front: - return .front - case .unspecified: - return .unspecified - case .external: - // External is not supported on iOS - return .unspecified - } - } } diff --git a/packages/react-native-vision-camera/ios/Hybrid Objects/HybridCameraDeviceFactory.swift b/packages/react-native-vision-camera/ios/Hybrid Objects/HybridCameraDeviceFactory.swift index 8f59ac9e3b..4e10a61680 100644 --- a/packages/react-native-vision-camera/ios/Hybrid Objects/HybridCameraDeviceFactory.swift +++ b/packages/react-native-vision-camera/ios/Hybrid Objects/HybridCameraDeviceFactory.swift @@ -73,29 +73,27 @@ final class HybridCameraDeviceFactory: HybridCameraDeviceFactorySpec { return HybridCameraDevice(device: device) } - func getDefaultCamera(position: CameraPosition) throws -> (any HybridCameraDeviceSpec)? { - if #available(iOS 17.0, *), position == .external { - // On iOS, Position "external" is for some reason reflected on the .deviceType, not on .position. - let device = AVCaptureDevice.default( - .external, - for: .video, - position: .unspecified) - guard let device else { - return nil - } - return HybridCameraDevice(device: device) - } - - let device = AVCaptureDevice.default( - .builtInWideAngleCamera, - for: .video, - position: position.toAVCaptureDevicePosition()) - guard let device else { + func getDefaultCamera(position: TargetCameraPosition) throws -> (any HybridCameraDeviceSpec)? { + guard let device = getDefaultAVCaptureDevice(at: position) else { return nil } return HybridCameraDevice(device: device) } + private func getDefaultAVCaptureDevice(at position: TargetCameraPosition) -> AVCaptureDevice? { + switch position { + case .back: + // Get default wide-angle at .back + return AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) + case .front: + // Get default wide-angle at .front + return AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .front) + case .external: + // On iOS, Position "external" is for some reason reflected on the .deviceType, not on .position. + return AVCaptureDevice.default(.external, for: .video, position: .unspecified) + } + } + func getSupportedExtensions(camera: any HybridCameraDeviceSpec) throws -> Promise< [any HybridCameraExtensionSpec] > { diff --git a/packages/react-native-vision-camera/nitrogen/generated/android/c++/JHybridCameraDeviceFactorySpec.cpp b/packages/react-native-vision-camera/nitrogen/generated/android/c++/JHybridCameraDeviceFactorySpec.cpp index e5da02d957..a92a29fe8d 100644 --- a/packages/react-native-vision-camera/nitrogen/generated/android/c++/JHybridCameraDeviceFactorySpec.cpp +++ b/packages/react-native-vision-camera/nitrogen/generated/android/c++/JHybridCameraDeviceFactorySpec.cpp @@ -13,8 +13,8 @@ namespace margelo::nitro::camera { class HybridCameraDeviceSpec; } namespace margelo::nitro::camera { struct ListenerSubscription; } // Forward declaration of `HybridCameraExtensionSpec` to properly resolve imports. namespace margelo::nitro::camera { class HybridCameraExtensionSpec; } -// Forward declaration of `CameraPosition` to properly resolve imports. -namespace margelo::nitro::camera { enum class CameraPosition; } +// Forward declaration of `TargetCameraPosition` to properly resolve imports. +namespace margelo::nitro::camera { enum class TargetCameraPosition; } #include #include "HybridCameraDeviceSpec.hpp" @@ -32,8 +32,8 @@ namespace margelo::nitro::camera { enum class CameraPosition; } #include "JHybridCameraExtensionSpec.hpp" #include "JFunc_void_std__vector_std__shared_ptr_HybridCameraDeviceSpec__.hpp" #include -#include "CameraPosition.hpp" -#include "JCameraPosition.hpp" +#include "TargetCameraPosition.hpp" +#include "JTargetCameraPosition.hpp" namespace margelo::nitro::camera { @@ -148,9 +148,9 @@ namespace margelo::nitro::camera { return __promise; }(); } - std::optional> JHybridCameraDeviceFactorySpec::getDefaultCamera(CameraPosition position) { - static const auto method = _javaPart->javaClassStatic()->getMethod(jni::alias_ref /* position */)>("getDefaultCamera"); - auto __result = method(_javaPart, JCameraPosition::fromCpp(position)); + std::optional> JHybridCameraDeviceFactorySpec::getDefaultCamera(TargetCameraPosition position) { + static const auto method = _javaPart->javaClassStatic()->getMethod(jni::alias_ref /* position */)>("getDefaultCamera"); + auto __result = method(_javaPart, JTargetCameraPosition::fromCpp(position)); return __result != nullptr ? std::make_optional(__result->getJHybridCameraDeviceSpec()) : std::nullopt; } diff --git a/packages/react-native-vision-camera/nitrogen/generated/android/c++/JHybridCameraDeviceFactorySpec.hpp b/packages/react-native-vision-camera/nitrogen/generated/android/c++/JHybridCameraDeviceFactorySpec.hpp index 19a60bea95..311f7e162c 100644 --- a/packages/react-native-vision-camera/nitrogen/generated/android/c++/JHybridCameraDeviceFactorySpec.hpp +++ b/packages/react-native-vision-camera/nitrogen/generated/android/c++/JHybridCameraDeviceFactorySpec.hpp @@ -60,7 +60,7 @@ namespace margelo::nitro::camera { ListenerSubscription addOnCameraDevicesChangedListener(const std::function>& /* newDevices */)>& listener) override; std::optional> getCameraForId(const std::string& id) override; std::shared_ptr>>> getSupportedExtensions(const std::shared_ptr& camera) override; - std::optional> getDefaultCamera(CameraPosition position) override; + std::optional> getDefaultCamera(TargetCameraPosition position) override; private: jni::global_ref _javaPart; diff --git a/packages/react-native-vision-camera/nitrogen/generated/android/c++/JTargetCameraPosition.hpp b/packages/react-native-vision-camera/nitrogen/generated/android/c++/JTargetCameraPosition.hpp new file mode 100644 index 0000000000..ce0b4cf4a1 --- /dev/null +++ b/packages/react-native-vision-camera/nitrogen/generated/android/c++/JTargetCameraPosition.hpp @@ -0,0 +1,61 @@ +/// +/// JTargetCameraPosition.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +#pragma once + +#include +#include "TargetCameraPosition.hpp" + +namespace margelo::nitro::camera { + + using namespace facebook; + + /** + * The C++ JNI bridge between the C++ enum "TargetCameraPosition" and the Kotlin enum "TargetCameraPosition". + */ + struct JTargetCameraPosition final: public jni::JavaClass { + public: + static constexpr auto kJavaDescriptor = "Lcom/margelo/nitro/camera/TargetCameraPosition;"; + + public: + /** + * Convert this Java/Kotlin-based enum to the C++ enum TargetCameraPosition. + */ + [[maybe_unused]] + [[nodiscard]] + TargetCameraPosition toCpp() const { + static const auto clazz = javaClassStatic(); + static const auto fieldOrdinal = clazz->getField("value"); + int ordinal = this->getFieldValue(fieldOrdinal); + return static_cast(ordinal); + } + + public: + /** + * Create a Java/Kotlin-based enum with the given C++ enum's value. + */ + [[maybe_unused]] + static jni::alias_ref fromCpp(TargetCameraPosition value) { + static const auto clazz = javaClassStatic(); + switch (value) { + case TargetCameraPosition::EXTERNAL: + static const auto fieldEXTERNAL = clazz->getStaticField("EXTERNAL"); + return clazz->getStaticFieldValue(fieldEXTERNAL); + case TargetCameraPosition::FRONT: + static const auto fieldFRONT = clazz->getStaticField("FRONT"); + return clazz->getStaticFieldValue(fieldFRONT); + case TargetCameraPosition::BACK: + static const auto fieldBACK = clazz->getStaticField("BACK"); + return clazz->getStaticFieldValue(fieldBACK); + default: + std::string stringValue = std::to_string(static_cast(value)); + throw std::invalid_argument("Invalid enum value (" + stringValue + "!"); + } + } + }; + +} // namespace margelo::nitro::camera diff --git a/packages/react-native-vision-camera/nitrogen/generated/android/kotlin/com/margelo/nitro/camera/HybridCameraDeviceFactorySpec.kt b/packages/react-native-vision-camera/nitrogen/generated/android/kotlin/com/margelo/nitro/camera/HybridCameraDeviceFactorySpec.kt index b41306e76c..2c8aa0a70b 100644 --- a/packages/react-native-vision-camera/nitrogen/generated/android/kotlin/com/margelo/nitro/camera/HybridCameraDeviceFactorySpec.kt +++ b/packages/react-native-vision-camera/nitrogen/generated/android/kotlin/com/margelo/nitro/camera/HybridCameraDeviceFactorySpec.kt @@ -60,7 +60,7 @@ abstract class HybridCameraDeviceFactorySpec: HybridObject() { @DoNotStrip @Keep - abstract fun getDefaultCamera(position: CameraPosition): HybridCameraDeviceSpec? + abstract fun getDefaultCamera(position: TargetCameraPosition): HybridCameraDeviceSpec? // Default implementation of `HybridObject.toString()` override fun toString(): String { diff --git a/packages/react-native-vision-camera/nitrogen/generated/android/kotlin/com/margelo/nitro/camera/TargetCameraPosition.kt b/packages/react-native-vision-camera/nitrogen/generated/android/kotlin/com/margelo/nitro/camera/TargetCameraPosition.kt new file mode 100644 index 0000000000..5b1a1b932f --- /dev/null +++ b/packages/react-native-vision-camera/nitrogen/generated/android/kotlin/com/margelo/nitro/camera/TargetCameraPosition.kt @@ -0,0 +1,24 @@ +/// +/// TargetCameraPosition.kt +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +package com.margelo.nitro.camera + +import androidx.annotation.Keep +import com.facebook.proguard.annotations.DoNotStrip + +/** + * Represents the JavaScript enum/union "TargetCameraPosition". + */ +@DoNotStrip +@Keep +enum class TargetCameraPosition(@DoNotStrip @Keep val value: Int) { + EXTERNAL(0), + FRONT(1), + BACK(2); + + companion object +} diff --git a/packages/react-native-vision-camera/nitrogen/generated/ios/VisionCamera-Swift-Cxx-Umbrella.hpp b/packages/react-native-vision-camera/nitrogen/generated/ios/VisionCamera-Swift-Cxx-Umbrella.hpp index 8ab82ad614..14418805ee 100644 --- a/packages/react-native-vision-camera/nitrogen/generated/ios/VisionCamera-Swift-Cxx-Umbrella.hpp +++ b/packages/react-native-vision-camera/nitrogen/generated/ios/VisionCamera-Swift-Cxx-Umbrella.hpp @@ -198,6 +198,8 @@ namespace margelo::nitro::camera { enum class ScannedObjectType; } namespace margelo::nitro::camera { enum class SceneAdaptiveness; } // Forward declaration of `Size` to properly resolve imports. namespace margelo::nitro::camera { struct Size; } +// Forward declaration of `TargetCameraPosition` to properly resolve imports. +namespace margelo::nitro::camera { enum class TargetCameraPosition; } // Forward declaration of `TargetColorRange` to properly resolve imports. namespace margelo::nitro::camera { enum class TargetColorRange; } // Forward declaration of `TargetColorSpace` to properly resolve imports. @@ -326,6 +328,7 @@ namespace margelo::nitro::camera { struct WhiteBalanceTemperatureAndTint; } #include "ScannedObjectType.hpp" #include "SceneAdaptiveness.hpp" #include "Size.hpp" +#include "TargetCameraPosition.hpp" #include "TargetColorRange.hpp" #include "TargetColorSpace.hpp" #include "TargetDynamicRange.hpp" diff --git a/packages/react-native-vision-camera/nitrogen/generated/ios/c++/HybridCameraDeviceFactorySpecSwift.hpp b/packages/react-native-vision-camera/nitrogen/generated/ios/c++/HybridCameraDeviceFactorySpecSwift.hpp index f166161e87..d1da50f2f3 100644 --- a/packages/react-native-vision-camera/nitrogen/generated/ios/c++/HybridCameraDeviceFactorySpecSwift.hpp +++ b/packages/react-native-vision-camera/nitrogen/generated/ios/c++/HybridCameraDeviceFactorySpecSwift.hpp @@ -18,8 +18,8 @@ namespace margelo::nitro::camera { class HybridCameraDeviceSpec; } namespace margelo::nitro::camera { struct ListenerSubscription; } // Forward declaration of `HybridCameraExtensionSpec` to properly resolve imports. namespace margelo::nitro::camera { class HybridCameraExtensionSpec; } -// Forward declaration of `CameraPosition` to properly resolve imports. -namespace margelo::nitro::camera { enum class CameraPosition; } +// Forward declaration of `TargetCameraPosition` to properly resolve imports. +namespace margelo::nitro::camera { enum class TargetCameraPosition; } #include #include "HybridCameraDeviceSpec.hpp" @@ -30,7 +30,7 @@ namespace margelo::nitro::camera { enum class CameraPosition; } #include #include "HybridCameraExtensionSpec.hpp" #include -#include "CameraPosition.hpp" +#include "TargetCameraPosition.hpp" #include "VisionCamera-Swift-Cxx-Umbrella.hpp" @@ -120,7 +120,7 @@ namespace margelo::nitro::camera { auto __value = std::move(__result.value()); return __value; } - inline std::optional> getDefaultCamera(CameraPosition position) override { + inline std::optional> getDefaultCamera(TargetCameraPosition position) override { auto __result = _swiftPart.getDefaultCamera(static_cast(position)); if (__result.hasError()) [[unlikely]] { std::rethrow_exception(__result.error()); diff --git a/packages/react-native-vision-camera/nitrogen/generated/ios/swift/HybridCameraDeviceFactorySpec.swift b/packages/react-native-vision-camera/nitrogen/generated/ios/swift/HybridCameraDeviceFactorySpec.swift index ce0ba0d904..0b3e720057 100644 --- a/packages/react-native-vision-camera/nitrogen/generated/ios/swift/HybridCameraDeviceFactorySpec.swift +++ b/packages/react-native-vision-camera/nitrogen/generated/ios/swift/HybridCameraDeviceFactorySpec.swift @@ -18,7 +18,7 @@ public protocol HybridCameraDeviceFactorySpec_protocol: HybridObject { func addOnCameraDevicesChangedListener(listener: @escaping (_ newDevices: [(any HybridCameraDeviceSpec)]) -> Void) throws -> ListenerSubscription func getCameraForId(id: String) throws -> (any HybridCameraDeviceSpec)? func getSupportedExtensions(camera: (any HybridCameraDeviceSpec)) throws -> Promise<[(any HybridCameraExtensionSpec)]> - func getDefaultCamera(position: CameraPosition) throws -> (any HybridCameraDeviceSpec)? + func getDefaultCamera(position: TargetCameraPosition) throws -> (any HybridCameraDeviceSpec)? } public extension HybridCameraDeviceFactorySpec_protocol { diff --git a/packages/react-native-vision-camera/nitrogen/generated/ios/swift/HybridCameraDeviceFactorySpec_cxx.swift b/packages/react-native-vision-camera/nitrogen/generated/ios/swift/HybridCameraDeviceFactorySpec_cxx.swift index ae6fb283ad..9850026333 100644 --- a/packages/react-native-vision-camera/nitrogen/generated/ios/swift/HybridCameraDeviceFactorySpec_cxx.swift +++ b/packages/react-native-vision-camera/nitrogen/generated/ios/swift/HybridCameraDeviceFactorySpec_cxx.swift @@ -273,7 +273,7 @@ open class HybridCameraDeviceFactorySpec_cxx { @inline(__always) public final func getDefaultCamera(position: Int32) -> bridge.Result_std__optional_std__shared_ptr_HybridCameraDeviceSpec___ { do { - let __result = try self.__implementation.getDefaultCamera(position: margelo.nitro.camera.CameraPosition(rawValue: position)!) + let __result = try self.__implementation.getDefaultCamera(position: margelo.nitro.camera.TargetCameraPosition(rawValue: position)!) let __resultCpp = { () -> bridge.std__optional_std__shared_ptr_HybridCameraDeviceSpec__ in if let __unwrappedValue = __result { return bridge.create_std__optional_std__shared_ptr_HybridCameraDeviceSpec__({ () -> bridge.std__shared_ptr_HybridCameraDeviceSpec_ in diff --git a/packages/react-native-vision-camera/nitrogen/generated/ios/swift/TargetCameraPosition.swift b/packages/react-native-vision-camera/nitrogen/generated/ios/swift/TargetCameraPosition.swift new file mode 100644 index 0000000000..d9843c7d21 --- /dev/null +++ b/packages/react-native-vision-camera/nitrogen/generated/ios/swift/TargetCameraPosition.swift @@ -0,0 +1,44 @@ +/// +/// TargetCameraPosition.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +/** + * Represents the JS union `TargetCameraPosition`, backed by a C++ enum. + */ +public typealias TargetCameraPosition = margelo.nitro.camera.TargetCameraPosition + +public extension TargetCameraPosition { + /** + * Get a TargetCameraPosition for the given String value, or + * return `nil` if the given value was invalid/unknown. + */ + init?(fromString string: String) { + switch string { + case "external": + self = .external + case "front": + self = .front + case "back": + self = .back + default: + return nil + } + } + + /** + * Get the String value this TargetCameraPosition represents. + */ + var stringValue: String { + switch self { + case .external: + return "external" + case .front: + return "front" + case .back: + return "back" + } + } +} diff --git a/packages/react-native-vision-camera/nitrogen/generated/shared/c++/HybridCameraDeviceFactorySpec.hpp b/packages/react-native-vision-camera/nitrogen/generated/shared/c++/HybridCameraDeviceFactorySpec.hpp index cde15be396..4b30739c8c 100644 --- a/packages/react-native-vision-camera/nitrogen/generated/shared/c++/HybridCameraDeviceFactorySpec.hpp +++ b/packages/react-native-vision-camera/nitrogen/generated/shared/c++/HybridCameraDeviceFactorySpec.hpp @@ -19,8 +19,8 @@ namespace margelo::nitro::camera { class HybridCameraDeviceSpec; } namespace margelo::nitro::camera { struct ListenerSubscription; } // Forward declaration of `HybridCameraExtensionSpec` to properly resolve imports. namespace margelo::nitro::camera { class HybridCameraExtensionSpec; } -// Forward declaration of `CameraPosition` to properly resolve imports. -namespace margelo::nitro::camera { enum class CameraPosition; } +// Forward declaration of `TargetCameraPosition` to properly resolve imports. +namespace margelo::nitro::camera { enum class TargetCameraPosition; } #include #include "HybridCameraDeviceSpec.hpp" @@ -31,7 +31,7 @@ namespace margelo::nitro::camera { enum class CameraPosition; } #include #include "HybridCameraExtensionSpec.hpp" #include -#include "CameraPosition.hpp" +#include "TargetCameraPosition.hpp" namespace margelo::nitro::camera { @@ -70,7 +70,7 @@ namespace margelo::nitro::camera { virtual ListenerSubscription addOnCameraDevicesChangedListener(const std::function>& /* newDevices */)>& listener) = 0; virtual std::optional> getCameraForId(const std::string& id) = 0; virtual std::shared_ptr>>> getSupportedExtensions(const std::shared_ptr& camera) = 0; - virtual std::optional> getDefaultCamera(CameraPosition position) = 0; + virtual std::optional> getDefaultCamera(TargetCameraPosition position) = 0; protected: // Hybrid Setup diff --git a/packages/react-native-vision-camera/nitrogen/generated/shared/c++/TargetCameraPosition.hpp b/packages/react-native-vision-camera/nitrogen/generated/shared/c++/TargetCameraPosition.hpp new file mode 100644 index 0000000000..9fa77b37b5 --- /dev/null +++ b/packages/react-native-vision-camera/nitrogen/generated/shared/c++/TargetCameraPosition.hpp @@ -0,0 +1,80 @@ +/// +/// TargetCameraPosition.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +#pragma once + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + +namespace margelo::nitro::camera { + + /** + * An enum which can be represented as a JavaScript union (TargetCameraPosition). + */ + enum class TargetCameraPosition { + EXTERNAL SWIFT_NAME(external) = 0, + FRONT SWIFT_NAME(front) = 1, + BACK SWIFT_NAME(back) = 2, + } CLOSED_ENUM; + +} // namespace margelo::nitro::camera + +namespace margelo::nitro { + + // C++ TargetCameraPosition <> JS TargetCameraPosition (union) + template <> + struct JSIConverter final { + static inline margelo::nitro::camera::TargetCameraPosition fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { + std::string unionValue = JSIConverter::fromJSI(runtime, arg); + switch (hashString(unionValue.c_str(), unionValue.size())) { + case hashString("external"): return margelo::nitro::camera::TargetCameraPosition::EXTERNAL; + case hashString("front"): return margelo::nitro::camera::TargetCameraPosition::FRONT; + case hashString("back"): return margelo::nitro::camera::TargetCameraPosition::BACK; + default: [[unlikely]] + throw std::invalid_argument("Cannot convert \"" + unionValue + "\" to enum TargetCameraPosition - invalid value!"); + } + } + static inline jsi::Value toJSI(jsi::Runtime& runtime, margelo::nitro::camera::TargetCameraPosition arg) { + switch (arg) { + case margelo::nitro::camera::TargetCameraPosition::EXTERNAL: return JSIConverter::toJSI(runtime, "external"); + case margelo::nitro::camera::TargetCameraPosition::FRONT: return JSIConverter::toJSI(runtime, "front"); + case margelo::nitro::camera::TargetCameraPosition::BACK: return JSIConverter::toJSI(runtime, "back"); + default: [[unlikely]] + throw std::invalid_argument("Cannot convert TargetCameraPosition to JS - invalid value: " + + std::to_string(static_cast(arg)) + "!"); + } + } + static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { + if (!value.isString()) { + return false; + } + std::string unionValue = JSIConverter::fromJSI(runtime, value); + switch (hashString(unionValue.c_str(), unionValue.size())) { + case hashString("external"): + case hashString("front"): + case hashString("back"): + return true; + default: + return false; + } + } + }; + +} // namespace margelo::nitro diff --git a/packages/react-native-vision-camera/src/CameraDevices.ts b/packages/react-native-vision-camera/src/CameraDevices.ts index aee7279897..19db257986 100644 --- a/packages/react-native-vision-camera/src/CameraDevices.ts +++ b/packages/react-native-vision-camera/src/CameraDevices.ts @@ -1,4 +1,7 @@ -import type { CameraPosition } from './specs/common-types/CameraPosition' +import type { + CameraPosition, + TargetCameraPosition, +} from './specs/common-types/CameraPosition' import type { ListenerSubscription } from './specs/common-types/ListenerSubscription' import type { CameraDevice } from './specs/inputs/CameraDevice.nitro' import type { CameraExtension } from './specs/inputs/CameraExtension.nitro' @@ -57,7 +60,7 @@ export function getAllCameraDevices(): CameraDevice[] { * @see {@linkcode getCameraDevice} */ export async function getDefaultCameraDevice( - position: CameraPosition, + position: TargetCameraPosition, ): Promise { const factory = await factoryPromise return factory.getDefaultCamera(position) diff --git a/packages/react-native-vision-camera/src/specs/common-types/CameraPosition.ts b/packages/react-native-vision-camera/src/specs/common-types/CameraPosition.ts index 41c9becf65..45f4b4a350 100644 --- a/packages/react-native-vision-camera/src/specs/common-types/CameraPosition.ts +++ b/packages/react-native-vision-camera/src/specs/common-types/CameraPosition.ts @@ -1,5 +1,13 @@ import type { CameraDevice } from '../inputs/CameraDevice.nitro' +/** + * Represents the target physical position of a {@linkcode CameraDevice} on the device. + * - `'front'`: Target Cameras located on the front of the device, typically used for selfies. + * - `'back'`: Target Cameras located on the back of the device, typically used for capturing the scene. + * - `'external'`: Target external Cameras (e.g. USB, Continuity Camera). + */ +export type TargetCameraPosition = 'front' | 'back' | 'external' + /** * Represents the physical position of a {@linkcode CameraDevice} on the device. * - `'front'`: The Camera is located on the front of the device, typically used for selfies. @@ -7,4 +15,4 @@ import type { CameraDevice } from '../inputs/CameraDevice.nitro' * - `'external'`: The Camera is an external Camera (e.g. USB, Continuity Camera). * - `'unspecified'`: The Camera's position is unknown or not applicable. */ -export type CameraPosition = 'front' | 'back' | 'external' | 'unspecified' +export type CameraPosition = TargetCameraPosition | 'unspecified' diff --git a/packages/react-native-vision-camera/src/specs/inputs/CameraDeviceFactory.nitro.ts b/packages/react-native-vision-camera/src/specs/inputs/CameraDeviceFactory.nitro.ts index f324f37f2c..85f88351ed 100644 --- a/packages/react-native-vision-camera/src/specs/inputs/CameraDeviceFactory.nitro.ts +++ b/packages/react-native-vision-camera/src/specs/inputs/CameraDeviceFactory.nitro.ts @@ -1,6 +1,9 @@ import type { HybridObject } from 'react-native-nitro-modules' import type { CameraFactory } from '../CameraFactory.nitro' -import type { CameraPosition } from '../common-types/CameraPosition' +import type { + CameraPosition, + TargetCameraPosition, +} from '../common-types/CameraPosition' import type { ListenerSubscription } from '../common-types/ListenerSubscription' import type { CameraSession } from '../session/CameraSession.nitro' import type { CameraDevice } from './CameraDevice.nitro' @@ -126,7 +129,7 @@ export interface CameraDeviceFactory /** * Get the platform's default {@linkcode CameraDevice} - * at the given {@linkcode CameraPosition}. + * at the given {@linkcode TargetCameraPosition}. */ - getDefaultCamera(position: CameraPosition): CameraDevice | undefined + getDefaultCamera(position: TargetCameraPosition): CameraDevice | undefined } From aea0ace349bdabd7ee02db60d4854fae52f903a7 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Tue, 30 Jun 2026 12:15:42 +0200 Subject: [PATCH 3/4] add getDefaultCamera test --- .../__tests__/visioncamera.devices.harness.ts | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/apps/simple-camera/__tests__/visioncamera.devices.harness.ts b/apps/simple-camera/__tests__/visioncamera.devices.harness.ts index e08fc95b8a..a8647f32f2 100644 --- a/apps/simple-camera/__tests__/visioncamera.devices.harness.ts +++ b/apps/simple-camera/__tests__/visioncamera.devices.harness.ts @@ -2,6 +2,7 @@ import { beforeAll, describe, expect, it } from 'react-native-harness' import type { CameraDevice, CameraDeviceFactory, + TargetCameraPosition, } from 'react-native-vision-camera' import { VisionCamera } from 'react-native-vision-camera' @@ -25,6 +26,28 @@ describe('VisionCamera - Devices', () => { expect(hasFront).toBe(true) }) + it('returns the default camera for each available target position', () => { + const positions: TargetCameraPosition[] = ['back', 'front', 'external'] + + for (const position of positions) { + const defaultCamera = factory.getDefaultCamera(position) + const devicesAtPosition = factory.cameraDevices.filter( + (d) => d.position === position, + ) + + if (devicesAtPosition.length === 0) { + expect(defaultCamera).toBe(undefined) + continue + } + + expect(defaultCamera).toBeDefined() + expect(defaultCamera?.position).toBe(position) + expect(devicesAtPosition.some((d) => d.id === defaultCamera?.id)).toBe( + true, + ) + } + }) + it('logs external cameras when present (optional)', (context) => { const external = factory.cameraDevices.filter( (d) => d.position === 'external', From baad6d948111802310a4955466c1b752e1ea101d Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Tue, 30 Jun 2026 12:18:00 +0200 Subject: [PATCH 4/4] guard ios 17 external --- .../ios/Hybrid Objects/HybridCameraDeviceFactory.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/react-native-vision-camera/ios/Hybrid Objects/HybridCameraDeviceFactory.swift b/packages/react-native-vision-camera/ios/Hybrid Objects/HybridCameraDeviceFactory.swift index 4e10a61680..5cdc026b20 100644 --- a/packages/react-native-vision-camera/ios/Hybrid Objects/HybridCameraDeviceFactory.swift +++ b/packages/react-native-vision-camera/ios/Hybrid Objects/HybridCameraDeviceFactory.swift @@ -90,6 +90,9 @@ final class HybridCameraDeviceFactory: HybridCameraDeviceFactorySpec { return AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .front) case .external: // On iOS, Position "external" is for some reason reflected on the .deviceType, not on .position. + guard #available(iOS 17.0, *) else { + return nil + } return AVCaptureDevice.default(.external, for: .video, position: .unspecified) } }