From f08313935ca14951739e29c17407092d951afe00 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Tue, 30 Jun 2026 12:24:49 +0200 Subject: [PATCH 01/11] feat: Add support for passing declarative position to Camera Device configuration --- .../android/VisionCamera+autolinking.cmake | 1 + .../android/c++/JCameraDeviceOrPosition.cpp | 26 +++++++ .../android/c++/JCameraDeviceOrPosition.hpp | 72 +++++++++++++++++++ .../android/c++/JCameraSessionConnection.hpp | 13 ++-- .../android/c++/JHybridCameraSessionSpec.cpp | 7 +- .../nitro/camera/CameraDeviceOrPosition.kt | 62 ++++++++++++++++ .../nitro/camera/CameraSessionConnection.kt | 6 +- .../ios/VisionCamera-Swift-Cxx-Bridge.hpp | 32 +++++++++ .../ios/c++/HybridCameraSessionSpecSwift.hpp | 5 +- .../ios/swift/CameraDeviceOrPosition.swift | 30 ++++++++ .../ios/swift/CameraSessionConnection.swift | 39 +++++++--- .../shared/c++/CameraSessionConnection.hpp | 15 ++-- .../specs/session/CameraSessionConnection.ts | 9 ++- 13 files changed, 290 insertions(+), 27 deletions(-) create mode 100644 packages/react-native-vision-camera/nitrogen/generated/android/c++/JCameraDeviceOrPosition.cpp create mode 100644 packages/react-native-vision-camera/nitrogen/generated/android/c++/JCameraDeviceOrPosition.hpp create mode 100644 packages/react-native-vision-camera/nitrogen/generated/android/kotlin/com/margelo/nitro/camera/CameraDeviceOrPosition.kt create mode 100644 packages/react-native-vision-camera/nitrogen/generated/ios/swift/CameraDeviceOrPosition.swift diff --git a/packages/react-native-vision-camera/nitrogen/generated/android/VisionCamera+autolinking.cmake b/packages/react-native-vision-camera/nitrogen/generated/android/VisionCamera+autolinking.cmake index adfdf1fef3..4dcb7a1d87 100644 --- a/packages/react-native-vision-camera/nitrogen/generated/android/VisionCamera+autolinking.cmake +++ b/packages/react-native-vision-camera/nitrogen/generated/android/VisionCamera+autolinking.cmake @@ -107,6 +107,7 @@ target_sources( ../nitrogen/generated/android/c++/JHybridCameraVideoOutputSpec.cpp ../nitrogen/generated/android/c++/JHybridRecorderSpec.cpp ../nitrogen/generated/android/c++/JHybridCameraSessionSpec.cpp + ../nitrogen/generated/android/c++/JCameraDeviceOrPosition.cpp ../nitrogen/generated/android/c++/JHybridCameraSessionConfigSpec.cpp ../nitrogen/generated/android/c++/JHybridFrameConverterSpec.cpp ../nitrogen/generated/android/c++/JHybridFrameRendererViewSpec.cpp diff --git a/packages/react-native-vision-camera/nitrogen/generated/android/c++/JCameraDeviceOrPosition.cpp b/packages/react-native-vision-camera/nitrogen/generated/android/c++/JCameraDeviceOrPosition.cpp new file mode 100644 index 0000000000..c165917c32 --- /dev/null +++ b/packages/react-native-vision-camera/nitrogen/generated/android/c++/JCameraDeviceOrPosition.cpp @@ -0,0 +1,26 @@ +/// +/// JCameraDeviceOrPosition.cpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +#include "JCameraDeviceOrPosition.hpp" + +namespace margelo::nitro::camera { + /** + * Converts JCameraDeviceOrPosition to std::variant, TargetCameraPosition> + */ + std::variant, TargetCameraPosition> JCameraDeviceOrPosition::toCpp() const { + if (isInstanceOf(JCameraDeviceOrPosition_impl::First::javaClassStatic())) { + // It's a `std::shared_ptr` + auto jniValue = static_cast(this)->getValue(); + return jniValue->getJHybridCameraDeviceSpec(); + } else if (isInstanceOf(JCameraDeviceOrPosition_impl::Second::javaClassStatic())) { + // It's a `TargetCameraPosition` + auto jniValue = static_cast(this)->getValue(); + return jniValue->toCpp(); + } + throw std::invalid_argument("Variant is unknown Kotlin instance!"); + } +} // namespace margelo::nitro::camera diff --git a/packages/react-native-vision-camera/nitrogen/generated/android/c++/JCameraDeviceOrPosition.hpp b/packages/react-native-vision-camera/nitrogen/generated/android/c++/JCameraDeviceOrPosition.hpp new file mode 100644 index 0000000000..5152dddf75 --- /dev/null +++ b/packages/react-native-vision-camera/nitrogen/generated/android/c++/JCameraDeviceOrPosition.hpp @@ -0,0 +1,72 @@ +/// +/// JCameraDeviceOrPosition.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 + +#include +#include "HybridCameraDeviceSpec.hpp" +#include "TargetCameraPosition.hpp" +#include +#include "JHybridCameraDeviceSpec.hpp" +#include "JTargetCameraPosition.hpp" + +namespace margelo::nitro::camera { + + using namespace facebook; + + /** + * The C++ JNI bridge between the C++ std::variant and the Java class "CameraDeviceOrPosition". + */ + class JCameraDeviceOrPosition: public jni::JavaClass { + public: + static constexpr auto kJavaDescriptor = "Lcom/margelo/nitro/camera/CameraDeviceOrPosition;"; + + static jni::local_ref create_0(jni::alias_ref value) { + static const auto method = javaClassStatic()->getStaticMethod)>("create"); + return method(javaClassStatic(), value); + } + static jni::local_ref create_1(jni::alias_ref value) { + static const auto method = javaClassStatic()->getStaticMethod)>("create"); + return method(javaClassStatic(), value); + } + + static jni::local_ref fromCpp(const std::variant, TargetCameraPosition>& variant) { + switch (variant.index()) { + case 0: return create_0(std::dynamic_pointer_cast(std::get<0>(variant))->getJavaPart()); + case 1: return create_1(JTargetCameraPosition::fromCpp(std::get<1>(variant))); + default: throw std::invalid_argument("Variant holds unknown index! (" + std::to_string(variant.index()) + ")"); + } + } + + [[nodiscard]] std::variant, TargetCameraPosition> toCpp() const; + }; + + namespace JCameraDeviceOrPosition_impl { + class First final: public jni::JavaClass { + public: + static constexpr auto kJavaDescriptor = "Lcom/margelo/nitro/camera/CameraDeviceOrPosition$First;"; + + [[nodiscard]] jni::local_ref getValue() const { + static const auto field = javaClassStatic()->getField("value"); + return getFieldValue(field); + } + }; + + class Second final: public jni::JavaClass { + public: + static constexpr auto kJavaDescriptor = "Lcom/margelo/nitro/camera/CameraDeviceOrPosition$Second;"; + + [[nodiscard]] jni::local_ref getValue() const { + static const auto field = javaClassStatic()->getField("value"); + return getFieldValue(field); + } + }; + } // namespace JCameraDeviceOrPosition_impl +} // namespace margelo::nitro::camera diff --git a/packages/react-native-vision-camera/nitrogen/generated/android/c++/JCameraSessionConnection.hpp b/packages/react-native-vision-camera/nitrogen/generated/android/c++/JCameraSessionConnection.hpp index bb3533c1bd..680de8cadd 100644 --- a/packages/react-native-vision-camera/nitrogen/generated/android/c++/JCameraSessionConnection.hpp +++ b/packages/react-native-vision-camera/nitrogen/generated/android/c++/JCameraSessionConnection.hpp @@ -17,6 +17,7 @@ #include "HybridCameraOutputSpec.hpp" #include "HybridCameraSessionConfigSpec.hpp" #include "JBinnedConstraint.hpp" +#include "JCameraDeviceOrPosition.hpp" #include "JCameraOutputConfiguration.hpp" #include "JConstraint.hpp" #include "JFPSConstraint.hpp" @@ -30,6 +31,7 @@ #include "JPixelFormatConstraint.hpp" #include "JPreviewStabilizationModeConstraint.hpp" #include "JResolutionBiasConstraint.hpp" +#include "JTargetCameraPosition.hpp" #include "JTargetColorRange.hpp" #include "JTargetColorSpace.hpp" #include "JTargetDynamicRange.hpp" @@ -43,6 +45,7 @@ #include "PixelFormatConstraint.hpp" #include "PreviewStabilizationModeConstraint.hpp" #include "ResolutionBiasConstraint.hpp" +#include "TargetCameraPosition.hpp" #include "TargetColorRange.hpp" #include "TargetColorSpace.hpp" #include "TargetDynamicRange.hpp" @@ -76,8 +79,8 @@ namespace margelo::nitro::camera { [[nodiscard]] CameraSessionConnection toCpp() const { static const auto clazz = javaClassStatic(); - static const auto fieldInput = clazz->getField("input"); - jni::local_ref input = this->getFieldValue(fieldInput); + static const auto fieldInput = clazz->getField("input"); + jni::local_ref input = this->getFieldValue(fieldInput); static const auto fieldOutputs = clazz->getField>("outputs"); jni::local_ref> outputs = this->getFieldValue(fieldOutputs); static const auto fieldConstraints = clazz->getField>("constraints"); @@ -89,7 +92,7 @@ namespace margelo::nitro::camera { static const auto fieldOnSessionConfigSelected = clazz->getField("onSessionConfigSelected"); jni::local_ref onSessionConfigSelected = this->getFieldValue(fieldOnSessionConfigSelected); return CameraSessionConnection( - input->getJHybridCameraDeviceSpec(), + input->toCpp(), [&](auto&& __input) { size_t __size = __input->size(); std::vector __vector; @@ -130,12 +133,12 @@ namespace margelo::nitro::camera { */ [[maybe_unused]] static jni::local_ref fromCpp(const CameraSessionConnection& value) { - using JSignature = JCameraSessionConnection(jni::alias_ref, jni::alias_ref>, jni::alias_ref>, jni::alias_ref, jni::alias_ref, jni::alias_ref); + using JSignature = JCameraSessionConnection(jni::alias_ref, jni::alias_ref>, jni::alias_ref>, jni::alias_ref, jni::alias_ref, jni::alias_ref); static const auto clazz = javaClassStatic(); static const auto create = clazz->getStaticMethod("fromCpp"); return create( clazz, - std::dynamic_pointer_cast(value.input)->getJavaPart(), + JCameraDeviceOrPosition::fromCpp(value.input), [&](auto&& __input) { size_t __size = __input.size(); jni::local_ref> __array = jni::JArrayClass::newArray(__size); diff --git a/packages/react-native-vision-camera/nitrogen/generated/android/c++/JHybridCameraSessionSpec.cpp b/packages/react-native-vision-camera/nitrogen/generated/android/c++/JHybridCameraSessionSpec.cpp index 15851995f2..28013c89eb 100644 --- a/packages/react-native-vision-camera/nitrogen/generated/android/c++/JHybridCameraSessionSpec.cpp +++ b/packages/react-native-vision-camera/nitrogen/generated/android/c++/JHybridCameraSessionSpec.cpp @@ -15,6 +15,8 @@ namespace margelo::nitro::camera { struct ListenerSubscription; } namespace margelo::nitro::camera { struct CameraSessionConnection; } // Forward declaration of `HybridCameraDeviceSpec` to properly resolve imports. namespace margelo::nitro::camera { class HybridCameraDeviceSpec; } +// Forward declaration of `TargetCameraPosition` to properly resolve imports. +namespace margelo::nitro::camera { enum class TargetCameraPosition; } // Forward declaration of `CameraOutputConfiguration` to properly resolve imports. namespace margelo::nitro::camera { struct CameraOutputConfiguration; } // Forward declaration of `MirrorMode` to properly resolve imports. @@ -71,7 +73,11 @@ namespace margelo::nitro::camera { enum class InterruptionReason; } #include "CameraSessionConnection.hpp" #include "JCameraSessionConnection.hpp" #include "HybridCameraDeviceSpec.hpp" +#include "TargetCameraPosition.hpp" +#include +#include "JCameraDeviceOrPosition.hpp" #include "JHybridCameraDeviceSpec.hpp" +#include "JTargetCameraPosition.hpp" #include "CameraOutputConfiguration.hpp" #include "JCameraOutputConfiguration.hpp" #include "MirrorMode.hpp" @@ -86,7 +92,6 @@ namespace margelo::nitro::camera { enum class InterruptionReason; } #include "PhotoHDRConstraint.hpp" #include "PixelFormatConstraint.hpp" #include "BinnedConstraint.hpp" -#include #include "JConstraint.hpp" #include "JFPSConstraint.hpp" #include "JVideoStabilizationModeConstraint.hpp" diff --git a/packages/react-native-vision-camera/nitrogen/generated/android/kotlin/com/margelo/nitro/camera/CameraDeviceOrPosition.kt b/packages/react-native-vision-camera/nitrogen/generated/android/kotlin/com/margelo/nitro/camera/CameraDeviceOrPosition.kt new file mode 100644 index 0000000000..6189d88d5d --- /dev/null +++ b/packages/react-native-vision-camera/nitrogen/generated/android/kotlin/com/margelo/nitro/camera/CameraDeviceOrPosition.kt @@ -0,0 +1,62 @@ +/// +/// CameraDeviceOrPosition.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 com.facebook.proguard.annotations.DoNotStrip + + +/** + * Represents the TypeScript variant "HybridCameraDeviceSpec | TargetCameraPosition". + */ +@Suppress("ClassName") +@DoNotStrip +sealed class CameraDeviceOrPosition { + @DoNotStrip + data class First(@DoNotStrip val value: HybridCameraDeviceSpec): CameraDeviceOrPosition() + @DoNotStrip + data class Second(@DoNotStrip val value: TargetCameraPosition): CameraDeviceOrPosition() + + inline fun asType(): T? { + return when (this) { + is First -> (value) as? T + is Second -> (value) as? T + } + } + inline fun isType(): Boolean { + return asType() != null + } + inline fun match(first: (HybridCameraDeviceSpec) -> R, second: (TargetCameraPosition) -> R): R { + return when (this) { + is First -> first(value) + is Second -> second(value) + } + } + + val isFirst: Boolean + get() = this is First + val isSecond: Boolean + get() = this is Second + + fun asFirstOrNull(): HybridCameraDeviceSpec? { + val value = (this as? First)?.value ?: return null + return value + } + fun asSecondOrNull(): TargetCameraPosition? { + val value = (this as? Second)?.value ?: return null + return value + } + + companion object { + @JvmStatic + @DoNotStrip + fun create(value: HybridCameraDeviceSpec): CameraDeviceOrPosition = First(value) + @JvmStatic + @DoNotStrip + fun create(value: TargetCameraPosition): CameraDeviceOrPosition = Second(value) + } +} diff --git a/packages/react-native-vision-camera/nitrogen/generated/android/kotlin/com/margelo/nitro/camera/CameraSessionConnection.kt b/packages/react-native-vision-camera/nitrogen/generated/android/kotlin/com/margelo/nitro/camera/CameraSessionConnection.kt index 238fa5b981..6bb1c1d16f 100644 --- a/packages/react-native-vision-camera/nitrogen/generated/android/kotlin/com/margelo/nitro/camera/CameraSessionConnection.kt +++ b/packages/react-native-vision-camera/nitrogen/generated/android/kotlin/com/margelo/nitro/camera/CameraSessionConnection.kt @@ -20,7 +20,7 @@ import java.util.Objects data class CameraSessionConnection( @DoNotStrip @Keep - val input: HybridCameraDeviceSpec, + val input: CameraDeviceOrPosition, @DoNotStrip @Keep val outputs: Array, @@ -40,7 +40,7 @@ data class CameraSessionConnection( /** * Create a new instance of CameraSessionConnection from Kotlin */ - constructor(input: HybridCameraDeviceSpec, outputs: Array, constraints: Array, initialZoom: Double?, initialExposureBias: Double?, onSessionConfigSelected: ((config: HybridCameraSessionConfigSpec) -> Unit)?): + constructor(input: CameraDeviceOrPosition, outputs: Array, constraints: Array, initialZoom: Double?, initialExposureBias: Double?, onSessionConfigSelected: ((config: HybridCameraSessionConfigSpec) -> Unit)?): this(input, outputs, constraints, initialZoom, initialExposureBias, onSessionConfigSelected?.let { Func_void_std__shared_ptr_HybridCameraSessionConfigSpec__java(it) }) override fun equals(other: Any?): Boolean { @@ -73,7 +73,7 @@ data class CameraSessionConnection( @Keep @Suppress("unused") @JvmStatic - private fun fromCpp(input: HybridCameraDeviceSpec, outputs: Array, constraints: Array, initialZoom: Double?, initialExposureBias: Double?, onSessionConfigSelected: Func_void_std__shared_ptr_HybridCameraSessionConfigSpec_?): CameraSessionConnection { + private fun fromCpp(input: CameraDeviceOrPosition, outputs: Array, constraints: Array, initialZoom: Double?, initialExposureBias: Double?, onSessionConfigSelected: Func_void_std__shared_ptr_HybridCameraSessionConfigSpec_?): CameraSessionConnection { return CameraSessionConnection(input, outputs, constraints, initialZoom, initialExposureBias, onSessionConfigSelected) } } diff --git a/packages/react-native-vision-camera/nitrogen/generated/ios/VisionCamera-Swift-Cxx-Bridge.hpp b/packages/react-native-vision-camera/nitrogen/generated/ios/VisionCamera-Swift-Cxx-Bridge.hpp index 91a1028a3c..9cbee63205 100644 --- a/packages/react-native-vision-camera/nitrogen/generated/ios/VisionCamera-Swift-Cxx-Bridge.hpp +++ b/packages/react-native-vision-camera/nitrogen/generated/ios/VisionCamera-Swift-Cxx-Bridge.hpp @@ -156,6 +156,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. @@ -324,6 +326,7 @@ namespace VisionCamera { class HybridZoomGestureControllerSpec_cxx; } #include "ScannedObjectType.hpp" #include "SceneAdaptiveness.hpp" #include "Size.hpp" +#include "TargetCameraPosition.hpp" #include "TargetColorRange.hpp" #include "TargetColorSpace.hpp" #include "TargetDynamicRange.hpp" @@ -2649,6 +2652,35 @@ namespace margelo::nitro::camera::bridge::swift { return Func_void_std__vector_std__shared_ptr_HybridCameraControllerSpec___Wrapper(std::move(value)); } + // pragma MARK: std::variant, TargetCameraPosition> + /** + * Wrapper struct for `std::variant, TargetCameraPosition>`. + * std::variant cannot be used in Swift because of a Swift bug. + * Not even specializing it works. So we create a wrapper struct. + */ + struct std__variant_std__shared_ptr_HybridCameraDeviceSpec___TargetCameraPosition_ final { + std::variant, TargetCameraPosition> variant; + std__variant_std__shared_ptr_HybridCameraDeviceSpec___TargetCameraPosition_(std::variant, TargetCameraPosition> variant): variant(variant) { } + operator std::variant, TargetCameraPosition>() const noexcept { + return variant; + } + inline size_t index() const noexcept { + return variant.index(); + } + inline std::shared_ptr get_0() const noexcept { + return std::get<0>(variant); + } + inline TargetCameraPosition get_1() const noexcept { + return std::get<1>(variant); + } + }; + inline std__variant_std__shared_ptr_HybridCameraDeviceSpec___TargetCameraPosition_ create_std__variant_std__shared_ptr_HybridCameraDeviceSpec___TargetCameraPosition_(const std::shared_ptr& value) noexcept { + return std__variant_std__shared_ptr_HybridCameraDeviceSpec___TargetCameraPosition_(value); + } + inline std__variant_std__shared_ptr_HybridCameraDeviceSpec___TargetCameraPosition_ create_std__variant_std__shared_ptr_HybridCameraDeviceSpec___TargetCameraPosition_(TargetCameraPosition value) noexcept { + return std__variant_std__shared_ptr_HybridCameraDeviceSpec___TargetCameraPosition_(value); + } + // pragma MARK: std::optional& /* config */)>> /** * Specialized version of `std::optional& / * config * /)>>`. diff --git a/packages/react-native-vision-camera/nitrogen/generated/ios/c++/HybridCameraSessionSpecSwift.hpp b/packages/react-native-vision-camera/nitrogen/generated/ios/c++/HybridCameraSessionSpecSwift.hpp index 9204993f94..74b2ed6d66 100644 --- a/packages/react-native-vision-camera/nitrogen/generated/ios/c++/HybridCameraSessionSpecSwift.hpp +++ b/packages/react-native-vision-camera/nitrogen/generated/ios/c++/HybridCameraSessionSpecSwift.hpp @@ -18,6 +18,8 @@ namespace margelo::nitro::camera { class HybridCameraControllerSpec; } namespace margelo::nitro::camera { struct CameraSessionConnection; } // Forward declaration of `HybridCameraDeviceSpec` to properly resolve imports. namespace margelo::nitro::camera { class HybridCameraDeviceSpec; } +// Forward declaration of `TargetCameraPosition` to properly resolve imports. +namespace margelo::nitro::camera { enum class TargetCameraPosition; } // Forward declaration of `CameraOutputConfiguration` to properly resolve imports. namespace margelo::nitro::camera { struct CameraOutputConfiguration; } // Forward declaration of `MirrorMode` to properly resolve imports. @@ -67,6 +69,8 @@ namespace margelo::nitro::camera { enum class InterruptionReason; } #include #include "CameraSessionConnection.hpp" #include "HybridCameraDeviceSpec.hpp" +#include "TargetCameraPosition.hpp" +#include #include "CameraOutputConfiguration.hpp" #include "MirrorMode.hpp" #include "HybridCameraOutputSpec.hpp" @@ -78,7 +82,6 @@ namespace margelo::nitro::camera { enum class InterruptionReason; } #include "PhotoHDRConstraint.hpp" #include "PixelFormatConstraint.hpp" #include "BinnedConstraint.hpp" -#include #include "TargetStabilizationMode.hpp" #include "TargetDynamicRange.hpp" #include "TargetDynamicRangeBitDepth.hpp" diff --git a/packages/react-native-vision-camera/nitrogen/generated/ios/swift/CameraDeviceOrPosition.swift b/packages/react-native-vision-camera/nitrogen/generated/ios/swift/CameraDeviceOrPosition.swift new file mode 100644 index 0000000000..6f2c1d2c92 --- /dev/null +++ b/packages/react-native-vision-camera/nitrogen/generated/ios/swift/CameraDeviceOrPosition.swift @@ -0,0 +1,30 @@ +/// +/// CameraDeviceOrPosition.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + + + +/** + * An Swift enum with associated values representing a Variant/Union type. + * JS type: `hybrid-object | enum` + */ +@frozen +public indirect enum CameraDeviceOrPosition { + case first((any HybridCameraDeviceSpec)) + case second(TargetCameraPosition) +} + +public extension CameraDeviceOrPosition { + func asType(_ type: T.Type = T.self) -> T? { + switch self { + case .first(let value): return value as? T + case .second(let value): return value as? T + } + } + func isType(_ type: T.Type = T.self) -> Bool { + return self.asType(type) != nil + } +} diff --git a/packages/react-native-vision-camera/nitrogen/generated/ios/swift/CameraSessionConnection.swift b/packages/react-native-vision-camera/nitrogen/generated/ios/swift/CameraSessionConnection.swift index 696b201b89..f5bcf226e7 100644 --- a/packages/react-native-vision-camera/nitrogen/generated/ios/swift/CameraSessionConnection.swift +++ b/packages/react-native-vision-camera/nitrogen/generated/ios/swift/CameraSessionConnection.swift @@ -18,11 +18,18 @@ public extension CameraSessionConnection { /** * Create a new instance of `CameraSessionConnection`. */ - init(input: (any HybridCameraDeviceSpec), outputs: [CameraOutputConfiguration], constraints: [Constraint], initialZoom: Double?, initialExposureBias: Double?, onSessionConfigSelected: ((_ config: (any HybridCameraSessionConfigSpec)) -> Void)?) { - self.init({ () -> bridge.std__shared_ptr_HybridCameraDeviceSpec_ in - let __cxxWrapped = input.getCxxWrapper() - return __cxxWrapped.getCxxPart() - }(), { () -> bridge.std__vector_CameraOutputConfiguration_ in + init(input: CameraDeviceOrPosition, outputs: [CameraOutputConfiguration], constraints: [Constraint], initialZoom: Double?, initialExposureBias: Double?, onSessionConfigSelected: ((_ config: (any HybridCameraSessionConfigSpec)) -> Void)?) { + self.init({ () -> bridge.std__variant_std__shared_ptr_HybridCameraDeviceSpec___TargetCameraPosition_ in + switch input { + case .first(let __value): + return bridge.create_std__variant_std__shared_ptr_HybridCameraDeviceSpec___TargetCameraPosition_({ () -> bridge.std__shared_ptr_HybridCameraDeviceSpec_ in + let __cxxWrapped = __value.getCxxWrapper() + return __cxxWrapped.getCxxPart() + }()) + case .second(let __value): + return bridge.create_std__variant_std__shared_ptr_HybridCameraDeviceSpec___TargetCameraPosition_(__value) + } + }().variant, { () -> bridge.std__vector_CameraOutputConfiguration_ in var __vector = bridge.create_std__vector_CameraOutputConfiguration_(outputs.count) for __item in outputs { __vector.push_back(__item) @@ -78,11 +85,23 @@ public extension CameraSessionConnection { } @inline(__always) - var input: (any HybridCameraDeviceSpec) { - return { () -> any HybridCameraDeviceSpec in - let __unsafePointer = bridge.get_std__shared_ptr_HybridCameraDeviceSpec_(self.__input) - let __instance = HybridCameraDeviceSpec_cxx.fromUnsafe(__unsafePointer) - return __instance.getHybridCameraDeviceSpec() + var input: CameraDeviceOrPosition { + return { () -> CameraDeviceOrPosition in + let __variant = bridge.std__variant_std__shared_ptr_HybridCameraDeviceSpec___TargetCameraPosition_(self.__input) + switch __variant.index() { + case 0: + let __actual = __variant.get_0() + return .first({ () -> any HybridCameraDeviceSpec in + let __unsafePointer = bridge.get_std__shared_ptr_HybridCameraDeviceSpec_(__actual) + let __instance = HybridCameraDeviceSpec_cxx.fromUnsafe(__unsafePointer) + return __instance.getHybridCameraDeviceSpec() + }()) + case 1: + let __actual = __variant.get_1() + return .second(__actual) + default: + fatalError("Variant can never have index \(__variant.index())!") + } }() } diff --git a/packages/react-native-vision-camera/nitrogen/generated/shared/c++/CameraSessionConnection.hpp b/packages/react-native-vision-camera/nitrogen/generated/shared/c++/CameraSessionConnection.hpp index 82389224a1..49aa074570 100644 --- a/packages/react-native-vision-camera/nitrogen/generated/shared/c++/CameraSessionConnection.hpp +++ b/packages/react-native-vision-camera/nitrogen/generated/shared/c++/CameraSessionConnection.hpp @@ -30,6 +30,8 @@ // Forward declaration of `HybridCameraDeviceSpec` to properly resolve imports. namespace margelo::nitro::camera { class HybridCameraDeviceSpec; } +// Forward declaration of `TargetCameraPosition` to properly resolve imports. +namespace margelo::nitro::camera { enum class TargetCameraPosition; } // Forward declaration of `CameraOutputConfiguration` to properly resolve imports. namespace margelo::nitro::camera { struct CameraOutputConfiguration; } // Forward declaration of `FPSConstraint` to properly resolve imports. @@ -53,6 +55,8 @@ namespace margelo::nitro::camera { class HybridCameraSessionConfigSpec; } #include #include "HybridCameraDeviceSpec.hpp" +#include "TargetCameraPosition.hpp" +#include #include "CameraOutputConfiguration.hpp" #include #include "FPSConstraint.hpp" @@ -63,7 +67,6 @@ namespace margelo::nitro::camera { class HybridCameraSessionConfigSpec; } #include "PhotoHDRConstraint.hpp" #include "PixelFormatConstraint.hpp" #include "BinnedConstraint.hpp" -#include #include #include "HybridCameraSessionConfigSpec.hpp" #include @@ -75,7 +78,7 @@ namespace margelo::nitro::camera { */ struct CameraSessionConnection final { public: - std::shared_ptr input SWIFT_PRIVATE; + std::variant, TargetCameraPosition> input SWIFT_PRIVATE; std::vector outputs SWIFT_PRIVATE; std::vector> constraints SWIFT_PRIVATE; std::optional initialZoom SWIFT_PRIVATE; @@ -84,7 +87,7 @@ namespace margelo::nitro::camera { public: CameraSessionConnection() = default; - explicit CameraSessionConnection(std::shared_ptr input, std::vector outputs, std::vector> constraints, std::optional initialZoom, std::optional initialExposureBias, std::optional& /* config */)>> onSessionConfigSelected): input(input), outputs(outputs), constraints(constraints), initialZoom(initialZoom), initialExposureBias(initialExposureBias), onSessionConfigSelected(onSessionConfigSelected) {} + explicit CameraSessionConnection(std::variant, TargetCameraPosition> input, std::vector outputs, std::vector> constraints, std::optional initialZoom, std::optional initialExposureBias, std::optional& /* config */)>> onSessionConfigSelected): input(input), outputs(outputs), constraints(constraints), initialZoom(initialZoom), initialExposureBias(initialExposureBias), onSessionConfigSelected(onSessionConfigSelected) {} public: // CameraSessionConnection is not equatable because these properties are not equatable: onSessionConfigSelected @@ -100,7 +103,7 @@ namespace margelo::nitro { static inline margelo::nitro::camera::CameraSessionConnection fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { jsi::Object obj = arg.asObject(runtime); return margelo::nitro::camera::CameraSessionConnection( - JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "input"))), + JSIConverter, margelo::nitro::camera::TargetCameraPosition>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "input"))), JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "outputs"))), JSIConverter>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "constraints"))), JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "initialZoom"))), @@ -110,7 +113,7 @@ namespace margelo::nitro { } static inline jsi::Value toJSI(jsi::Runtime& runtime, const margelo::nitro::camera::CameraSessionConnection& arg) { jsi::Object obj(runtime); - obj.setProperty(runtime, PropNameIDCache::get(runtime, "input"), JSIConverter>::toJSI(runtime, arg.input)); + obj.setProperty(runtime, PropNameIDCache::get(runtime, "input"), JSIConverter, margelo::nitro::camera::TargetCameraPosition>>::toJSI(runtime, arg.input)); obj.setProperty(runtime, PropNameIDCache::get(runtime, "outputs"), JSIConverter>::toJSI(runtime, arg.outputs)); obj.setProperty(runtime, PropNameIDCache::get(runtime, "constraints"), JSIConverter>>::toJSI(runtime, arg.constraints)); obj.setProperty(runtime, PropNameIDCache::get(runtime, "initialZoom"), JSIConverter>::toJSI(runtime, arg.initialZoom)); @@ -126,7 +129,7 @@ namespace margelo::nitro { if (!nitro::isPlainObject(runtime, obj)) { return false; } - if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "input")))) return false; + if (!JSIConverter, margelo::nitro::camera::TargetCameraPosition>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "input")))) return false; if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "outputs")))) return false; if (!JSIConverter>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "constraints")))) return false; if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "initialZoom")))) return false; diff --git a/packages/react-native-vision-camera/src/specs/session/CameraSessionConnection.ts b/packages/react-native-vision-camera/src/specs/session/CameraSessionConnection.ts index 27c028123e..d76f66d4ff 100644 --- a/packages/react-native-vision-camera/src/specs/session/CameraSessionConnection.ts +++ b/packages/react-native-vision-camera/src/specs/session/CameraSessionConnection.ts @@ -1,9 +1,12 @@ import type { CameraFactory } from '../CameraFactory.nitro' +import type { TargetCameraPosition } from '../common-types/CameraPosition' import type { Constraint } from '../common-types/Constraint' import type { CameraDevice } from '../inputs/CameraDevice.nitro' import type { CameraOutputConfiguration } from './CameraOutputConfiguration' import type { CameraSessionConfig } from './CameraSessionConfig.nitro' +type CameraDeviceOrPosition = CameraDevice | TargetCameraPosition + /** * Specifies a single Camera input stream connection * streaming into zero or more outputs. @@ -13,8 +16,12 @@ export interface CameraSessionConnection { * The input device of this {@linkcode CameraSessionConnection}. * This {@linkcode CameraDevice} will stream into all given * {@linkcode outputs}. + * + * Pass a {@linkcode TargetCameraPosition} to automatically + * select the default {@linkcode CameraDevice} at the given + * position. */ - input: CameraDevice + input: CameraDeviceOrPosition /** * All output configurations that the given {@linkcode input} * device will stream into. From 26001b531d125efe457dee7dd53f196c22cfcd10 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Tue, 30 Jun 2026 12:40:43 +0200 Subject: [PATCH 02/11] use extension method for default for --- .../AVCaptureDevice+defaultFor.swift | 28 +++++++++++++++++++ .../HybridCameraDeviceFactory.swift | 19 +------------ 2 files changed, 29 insertions(+), 18 deletions(-) create mode 100644 packages/react-native-vision-camera/ios/Extensions/AVFoundation/AVCaptureDevice+defaultFor.swift diff --git a/packages/react-native-vision-camera/ios/Extensions/AVFoundation/AVCaptureDevice+defaultFor.swift b/packages/react-native-vision-camera/ios/Extensions/AVFoundation/AVCaptureDevice+defaultFor.swift new file mode 100644 index 0000000000..bed41b10df --- /dev/null +++ b/packages/react-native-vision-camera/ios/Extensions/AVFoundation/AVCaptureDevice+defaultFor.swift @@ -0,0 +1,28 @@ +/// +/// AVCaptureDevice+defaultFor.swift +/// VisionCamera +/// Copyright © 2025 Marc Rousavy @ Margelo +/// + +import AVFoundation +import Foundation +import NitroModules + +extension AVCaptureDevice { + static func `default`(for 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. + guard #available(iOS 17.0, *) else { + return nil + } + return AVCaptureDevice.default(.external, for: .video, position: .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 5cdc026b20..013833ca5f 100644 --- a/packages/react-native-vision-camera/ios/Hybrid Objects/HybridCameraDeviceFactory.swift +++ b/packages/react-native-vision-camera/ios/Hybrid Objects/HybridCameraDeviceFactory.swift @@ -74,29 +74,12 @@ final class HybridCameraDeviceFactory: HybridCameraDeviceFactorySpec { } func getDefaultCamera(position: TargetCameraPosition) throws -> (any HybridCameraDeviceSpec)? { - guard let device = getDefaultAVCaptureDevice(at: position) else { + guard let device = AVCaptureDevice.default(for: 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. - guard #available(iOS 17.0, *) else { - return nil - } - return AVCaptureDevice.default(.external, for: .video, position: .unspecified) - } - } - func getSupportedExtensions(camera: any HybridCameraDeviceSpec) throws -> Promise< [any HybridCameraExtensionSpec] > { From b6182450e8a0a778a201f49a86642abe420092a1 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Tue, 30 Jun 2026 13:30:08 +0200 Subject: [PATCH 03/11] implement Swift first idea --- .../AVCaptureDevice+resolve.swift | 26 +++++++++++++++ .../AVCaptureSession+addConnection.swift | 2 +- ...ureSession+addInputWithNoConnections.swift | 12 +++---- .../AVCaptureSession+containsConnection.swift | 2 +- .../AVCaptureSession+containsInput.swift | 16 +++------- .../AVCaptureSession+findDevice.swift | 11 +++---- .../CameraSessionConnections+contains.swift | 8 ++--- .../Constraints/ConstraintResolver.swift | 32 +++++++++++++++++-- .../HybridCameraController.swift | 17 ++++++---- 9 files changed, 85 insertions(+), 41 deletions(-) create mode 100644 packages/react-native-vision-camera/ios/Extensions/AVFoundation/AVCaptureDevice+resolve.swift diff --git a/packages/react-native-vision-camera/ios/Extensions/AVFoundation/AVCaptureDevice+resolve.swift b/packages/react-native-vision-camera/ios/Extensions/AVFoundation/AVCaptureDevice+resolve.swift new file mode 100644 index 0000000000..6cb9ab9ec8 --- /dev/null +++ b/packages/react-native-vision-camera/ios/Extensions/AVFoundation/AVCaptureDevice+resolve.swift @@ -0,0 +1,26 @@ +/// +/// AVCaptureDevice+defaultFor.swift +/// VisionCamera +/// Copyright © 2025 Marc Rousavy @ Margelo +/// + +import AVFoundation +import Foundation +import NitroModules + +extension AVCaptureDevice { + static func resolve(value: CameraDeviceOrPosition) throws -> AVCaptureDevice { + switch value { + case .first(let hybridCameraDeviceSpec): + guard let device = hybridCameraDeviceSpec as? any NativeCameraDevice else { + throw RuntimeError("CameraDevice \"\(hybridCameraDeviceSpec)\" is not of type `NativeCameraDevice`!") + } + return device.device + case .second(let targetCameraPosition): + guard let device = AVCaptureDevice.default(for: targetCameraPosition) else { + throw RuntimeError("No CameraDevice exists at position \"\(targetCameraPosition)\"!") + } + return device + } + } +} diff --git a/packages/react-native-vision-camera/ios/Extensions/AVFoundation/AVCaptureSession+addConnection.swift b/packages/react-native-vision-camera/ios/Extensions/AVFoundation/AVCaptureSession+addConnection.swift index a58e4c2c31..5b579d2be2 100644 --- a/packages/react-native-vision-camera/ios/Extensions/AVFoundation/AVCaptureSession+addConnection.swift +++ b/packages/react-native-vision-camera/ios/Extensions/AVFoundation/AVCaptureSession+addConnection.swift @@ -14,7 +14,7 @@ import NitroModules // `NativeCameraDevice` directly, and only unwrap once in the caller? extension AVCaptureSession { - func addConnection(input: any HybridCameraDeviceSpec, output: any HybridCameraOutputSpec) throws { + func addConnection(input: CameraDeviceOrPosition, output: any HybridCameraOutputSpec) throws { // 1. Get the `AVCaptureDeviceInput` for our `input` let deviceInput = try findDevice(for: input) diff --git a/packages/react-native-vision-camera/ios/Extensions/AVFoundation/AVCaptureSession+addInputWithNoConnections.swift b/packages/react-native-vision-camera/ios/Extensions/AVFoundation/AVCaptureSession+addInputWithNoConnections.swift index b3bf24cdf1..3c3adca4b1 100644 --- a/packages/react-native-vision-camera/ios/Extensions/AVFoundation/AVCaptureSession+addInputWithNoConnections.swift +++ b/packages/react-native-vision-camera/ios/Extensions/AVFoundation/AVCaptureSession+addInputWithNoConnections.swift @@ -9,17 +9,13 @@ import Foundation import NitroModules extension AVCaptureSession { - func addInputWithNoConnections(_ deviceSpec: any HybridCameraDeviceSpec) throws + func addInputWithNoConnections(_ deviceOrPosition: CameraDeviceOrPosition) throws -> AVCaptureDeviceInput { - guard let input = deviceSpec as? any NativeCameraDevice else { - throw RuntimeError.error( - withMessage: "Input \"\(deviceSpec)\" is not of type `NativeCameraDevice`!") - } - let deviceInput = try AVCaptureDeviceInput(device: input.device) + let device = try AVCaptureDevice.resolve(value: deviceOrPosition) + let deviceInput = try AVCaptureDeviceInput(device: device) guard canAddInput(deviceInput) else { - throw RuntimeError.error( - withMessage: "Input \"\(deviceInput)\" cannot be added to Camera Session!") + throw RuntimeError("Input \"\(deviceInput)\" cannot be added to Camera Session!") } logger.info("Adding input \(deviceInput)...") addInputWithNoConnections(deviceInput) diff --git a/packages/react-native-vision-camera/ios/Extensions/AVFoundation/AVCaptureSession+containsConnection.swift b/packages/react-native-vision-camera/ios/Extensions/AVFoundation/AVCaptureSession+containsConnection.swift index fa967e3957..c6ce7c1870 100644 --- a/packages/react-native-vision-camera/ios/Extensions/AVFoundation/AVCaptureSession+containsConnection.swift +++ b/packages/react-native-vision-camera/ios/Extensions/AVFoundation/AVCaptureSession+containsConnection.swift @@ -9,7 +9,7 @@ import Foundation import NitroModules extension AVCaptureSession { - func containsConnection(input: any HybridCameraDeviceSpec, output: any HybridCameraOutputSpec) + func containsConnection(input: CameraDeviceOrPosition, output: any HybridCameraOutputSpec) throws -> Bool { // 1. Get the `AVCaptureDeviceInput` for our given `input` - this throws if it isn't attached yet. diff --git a/packages/react-native-vision-camera/ios/Extensions/AVFoundation/AVCaptureSession+containsInput.swift b/packages/react-native-vision-camera/ios/Extensions/AVFoundation/AVCaptureSession+containsInput.swift index 20834abdaa..713ffe06c0 100644 --- a/packages/react-native-vision-camera/ios/Extensions/AVFoundation/AVCaptureSession+containsInput.swift +++ b/packages/react-native-vision-camera/ios/Extensions/AVFoundation/AVCaptureSession+containsInput.swift @@ -9,19 +9,13 @@ import Foundation import NitroModules extension AVCaptureSession { - func containsInput(_ deviceSpec: any HybridCameraDeviceSpec) throws -> Bool { - guard let nativeInput = deviceSpec as? any NativeCameraDevice else { - throw RuntimeError.error( - withMessage: "Input \"\(deviceSpec)\" is not of type `NativeCameraDevice`!") - } - for input in self.inputs { + func containsInput(_ deviceOrPosition: CameraDeviceOrPosition) throws -> Bool { + let device = try AVCaptureDevice.resolve(value: deviceOrPosition) + return self.inputs.contains { input in guard let deviceInput = input as? AVCaptureDeviceInput else { - continue - } - if deviceInput.device == nativeInput.device { - return true + return false } + return deviceInput.device == device } - return false } } diff --git a/packages/react-native-vision-camera/ios/Extensions/AVFoundation/AVCaptureSession+findDevice.swift b/packages/react-native-vision-camera/ios/Extensions/AVFoundation/AVCaptureSession+findDevice.swift index 431e93a82e..bec7c50f57 100644 --- a/packages/react-native-vision-camera/ios/Extensions/AVFoundation/AVCaptureSession+findDevice.swift +++ b/packages/react-native-vision-camera/ios/Extensions/AVFoundation/AVCaptureSession+findDevice.swift @@ -14,18 +14,15 @@ import NitroModules // `NativeCameraDevice` directly, and only unwrap once in the caller? extension AVCaptureSession { - func findDevice(for input: any HybridCameraDeviceSpec) throws -> AVCaptureDeviceInput { - // 1. Downcast input - guard let input = input as? any NativeCameraDevice else { - throw RuntimeError.error( - withMessage: "Input \"\(input)\" is not of type `NativeCameraDevice`!") - } + func findDevice(for input: CameraDeviceOrPosition) throws -> AVCaptureDeviceInput { + // 1. Resolve input to a AVCaptureDevice + let device = try AVCaptureDevice.resolve(value: input) // 2. Get a device-input for attachedInput in self.inputs { guard let attachedInput = attachedInput as? AVCaptureDeviceInput else { continue } - if attachedInput.device == input.device { + if attachedInput.device == device { // 3. We found it! Return return attachedInput } diff --git a/packages/react-native-vision-camera/ios/Extensions/CameraSessionConnections+contains.swift b/packages/react-native-vision-camera/ios/Extensions/CameraSessionConnections+contains.swift index 7bffdb441d..e71f04f5ae 100644 --- a/packages/react-native-vision-camera/ios/Extensions/CameraSessionConnections+contains.swift +++ b/packages/react-native-vision-camera/ios/Extensions/CameraSessionConnections+contains.swift @@ -28,10 +28,10 @@ extension Array where Element == CameraSessionConnection { } return self.contains { connection in - guard let input = connection.input as? any NativeCameraDevice else { + guard let device = try? AVCaptureDevice.resolve(value: connection.input) else { return false } - return input.device == targetInput.device + return device == targetInput.device } } @@ -62,7 +62,7 @@ extension Array where Element == CameraSessionConnection { // It's a connection to a camera device - compare `input` and `output`: return self.contains { connection in - guard let input = connection.input as? any NativeCameraDevice else { + guard let device = try? AVCaptureDevice.resolve(value: connection.input) else { return false } @@ -71,7 +71,7 @@ extension Array where Element == CameraSessionConnection { guard let deviceInput = port.input as? AVCaptureDeviceInput else { return false } - return input.device == deviceInput.device + return device == deviceInput.device } if !containsInput { return false diff --git a/packages/react-native-vision-camera/ios/Hybrid Objects/Constraints/ConstraintResolver.swift b/packages/react-native-vision-camera/ios/Hybrid Objects/Constraints/ConstraintResolver.swift index 92f2bec4e3..22a5793079 100644 --- a/packages/react-native-vision-camera/ios/Hybrid Objects/Constraints/ConstraintResolver.swift +++ b/packages/react-native-vision-camera/ios/Hybrid Objects/Constraints/ConstraintResolver.swift @@ -13,6 +13,20 @@ import NitroModules // .yuv4208BiVideo formats. When it's `'native'`, we shouldn't care. Like .any enum ConstraintResolver { + static func resolveConstraints( + for device: CameraDeviceOrPosition, + constraints: [Constraint], + outputs: [any HybridCameraOutputSpec], + isMultiCam: Bool + ) throws -> ResolvedConstraints { + let device = try AVCaptureDevice.resolve(value: device) + return try resolveConstraints( + for: device, + constraints: constraints, + outputs: outputs, + isMultiCam: isMultiCam) + } + static func resolveConstraints( for device: any HybridCameraDeviceSpec, constraints: [Constraint], @@ -20,9 +34,21 @@ enum ConstraintResolver { isMultiCam: Bool ) throws -> ResolvedConstraints { guard let device = device as? any NativeCameraDevice else { - throw RuntimeError.error( - withMessage: "The given `device` is not of type `NativeCameraDevice`!") + throw RuntimeError("The given `device` is not of type `NativeCameraDevice`!") } + return try resolveConstraints( + for: device.device, + constraints: constraints, + outputs: outputs, + isMultiCam: isMultiCam) + } + + static func resolveConstraints( + for device: AVCaptureDevice, + constraints: [Constraint], + outputs: [any HybridCameraOutputSpec], + isMultiCam: Bool + ) throws -> ResolvedConstraints { let outputs = try outputs.compactMap { output in switch output { case let output as any NativeCameraOutput: @@ -37,7 +63,7 @@ enum ConstraintResolver { } } return try resolveConstraints( - for: device.device, + for: device, constraints: constraints, outputs: outputs, isMultiCam: isMultiCam) diff --git a/packages/react-native-vision-camera/ios/Hybrid Objects/HybridCameraController.swift b/packages/react-native-vision-camera/ios/Hybrid Objects/HybridCameraController.swift index 6b8ac9b850..b249fd6b3f 100644 --- a/packages/react-native-vision-camera/ios/Hybrid Objects/HybridCameraController.swift +++ b/packages/react-native-vision-camera/ios/Hybrid Objects/HybridCameraController.swift @@ -14,13 +14,18 @@ final class HybridCameraController: HybridCameraControllerSpec, NativeCameraCont let captureDevice: AVCaptureDevice private var activeMeteringTask: MeteringTask? = nil - init(device: any HybridCameraDeviceSpec, queue: DispatchQueue) throws { - guard let hybridDevice = device as? HybridCameraDevice else { - throw RuntimeError.error( - withMessage: "Device \"\(device)\" is not of type `HybridCameraDevice`!") + init(device: CameraDeviceOrPosition, queue: DispatchQueue) throws { + let captureDevice = try AVCaptureDevice.resolve(value: device) + self.captureDevice = captureDevice + switch device { + case .first(let hybridCameraDeviceSpec): + // Re-use the existing HybridObject + self.device = hybridCameraDeviceSpec + default: + // Create a new HybridObject otherwise + self.device = HybridCameraDevice(device: captureDevice) } - self.captureDevice = hybridDevice.device - self.device = device + // TODO: Is it best practice to use the same `queue` as our HybridCameraSession? self.queue = queue } From 665892c2bd53bce88982b178a788b43b4ad800f9 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Wed, 1 Jul 2026 11:40:40 +0200 Subject: [PATCH 04/11] fix: Resolve Connections upfront (#4059) * fix: Resolve connections upfront * WIP * ughhhh * rename * Resolve for input * Update HybridCameraSession.swift * Restructure ResolvedCameraSessionConnection to map 1:1 * uhm? * Update HybridCameraController.swift * it builds * custom config type ig * Move out * extract * chore: Lint * remove dead code * impl android hihi * Update ResolvableConstraint+AVAutoFocusSystem.swift * Update CameraSessionConnection+getCameraInfo.kt --- .../ios/HybridBarcodeScannerOutput.swift | 2 +- .../ios/HybridLocationManager.swift | 1 - .../nitro/camera/HybridCameraDeviceFactory.kt | 13 +- .../CameraSessionConnection+getCameraInfo.kt | 22 ++- .../ProcessCameraProvider+getDefaultCamera.kt | 23 +++ .../camera/hybrids/HybridCameraSession.kt | 4 +- .../AVCaptureConnection+init.swift | 16 +- .../AVCaptureConnection+isConnectedTo.swift | 9 ++ .../AVCaptureDevice+resolve.swift | 2 +- .../AVCaptureSession+addConnection.swift | 6 +- ...reSession+addOutputWithNoConnections.swift | 25 ++-- .../AVCaptureSession+containsConnection.swift | 21 +-- .../AVCaptureSession+containsOutput.swift | 20 +-- .../AVCaptureSession+findDevice.swift | 14 +- .../CameraSessionConnections+contains.swift | 12 -- ...solvableConstraint+AVAutoFocusSystem.swift | 13 -- .../HybridCameraController.swift | 14 +- .../Hybrid Objects/HybridCameraSession.swift | 116 ++++++++------- .../HybridCameraDepthFrameOutput.swift | 2 +- .../Outputs/HybridCameraFrameOutput.swift | 2 +- .../Outputs/HybridCameraObjectOutput.swift | 2 +- .../Outputs/HybridCameraPhotoOutput.swift | 2 +- .../Outputs/HybridCameraPreviewOutput.swift | 2 +- .../HybridCameraVideoFrameOutput.swift | 2 +- .../Outputs/HybridCameraVideoOutput.swift | 2 +- .../ios/Public/NativeCameraOutput.swift | 2 +- .../ios/Public/NativePreviewViewOutput.swift | 4 +- ...SessionConfig.swift => OutputConfig.swift} | 6 +- .../ResolvedCameraSessionConnection.swift | 137 ++++++++++++++++++ 29 files changed, 317 insertions(+), 179 deletions(-) create mode 100644 packages/react-native-vision-camera/android/src/main/java/com/margelo/nitro/camera/extensions/ProcessCameraProvider+getDefaultCamera.kt rename packages/react-native-vision-camera/ios/Public/{SessionConfig.swift => OutputConfig.swift} (54%) create mode 100644 packages/react-native-vision-camera/ios/Utils/ResolvedCameraSessionConnection.swift diff --git a/packages/react-native-vision-camera-barcode-scanner/ios/HybridBarcodeScannerOutput.swift b/packages/react-native-vision-camera-barcode-scanner/ios/HybridBarcodeScannerOutput.swift index c817ba031b..bb243ef322 100644 --- a/packages/react-native-vision-camera-barcode-scanner/ios/HybridBarcodeScannerOutput.swift +++ b/packages/react-native-vision-camera-barcode-scanner/ios/HybridBarcodeScannerOutput.swift @@ -84,7 +84,7 @@ final class HybridBarcodeScannerOutput: HybridCameraOutputSpec, NativeCameraOutp } } - func configure(config: CameraOutputConfiguration) { + func configure(config: OutputConfiguration) { guard let connection = self.output.connection(with: .video) else { return } diff --git a/packages/react-native-vision-camera-location/ios/HybridLocationManager.swift b/packages/react-native-vision-camera-location/ios/HybridLocationManager.swift index 038f6d0bb8..9a5085cd7f 100644 --- a/packages/react-native-vision-camera-location/ios/HybridLocationManager.swift +++ b/packages/react-native-vision-camera-location/ios/HybridLocationManager.swift @@ -11,7 +11,6 @@ import VisionCamera final class HybridLocationManager: HybridLocationManagerSpec { private let manager = CLLocationManager() private let delegate = LocationDelegate() - private var permissionPromises: [Promise] = [] var lastKnownLocation: (any HybridLocationSpec)? { guard let location = manager.location else { return nil 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 f398d4c7c6..faa0b52dcb 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 @@ -15,6 +15,7 @@ import androidx.camera.lifecycle.ProcessCameraProvider import androidx.core.content.edit import com.facebook.react.bridge.ReactApplicationContext import com.margelo.nitro.NitroModules +import com.margelo.nitro.camera.extensions.getDefaultCamera import com.margelo.nitro.camera.extensions.mapToArray import com.margelo.nitro.camera.hybrids.inputs.HybridCameraDevice import com.margelo.nitro.camera.hybrids.inputs.HybridCameraExtension @@ -112,18 +113,8 @@ class HybridCameraDeviceFactory( @OptIn(ExperimentalLensFacing::class) override fun getDefaultCamera(position: TargetCameraPosition): HybridCameraDeviceSpec? { - val selector = - when (position) { - TargetCameraPosition.FRONT -> CameraSelector.DEFAULT_FRONT_CAMERA - TargetCameraPosition.BACK -> CameraSelector.DEFAULT_BACK_CAMERA - TargetCameraPosition.EXTERNAL -> - CameraSelector - .Builder() - .requireLensFacing(CameraSelector.LENS_FACING_EXTERNAL) - .build() - } try { - val defaultCamera = cameraProvider.getCameraInfo(selector) + val defaultCamera = cameraProvider.getDefaultCamera(position) return HybridCameraDevice(defaultCamera) } catch (e: Throwable) { Log.e(TAG, "No default ${position.name} Camera found!", e) diff --git a/packages/react-native-vision-camera/android/src/main/java/com/margelo/nitro/camera/extensions/CameraSessionConnection+getCameraInfo.kt b/packages/react-native-vision-camera/android/src/main/java/com/margelo/nitro/camera/extensions/CameraSessionConnection+getCameraInfo.kt index 879fd1a0c6..34058b4fd5 100644 --- a/packages/react-native-vision-camera/android/src/main/java/com/margelo/nitro/camera/extensions/CameraSessionConnection+getCameraInfo.kt +++ b/packages/react-native-vision-camera/android/src/main/java/com/margelo/nitro/camera/extensions/CameraSessionConnection+getCameraInfo.kt @@ -1,12 +1,24 @@ package com.margelo.nitro.camera.extensions import androidx.camera.core.CameraInfo +import androidx.camera.lifecycle.ProcessCameraProvider import com.margelo.nitro.camera.CameraSessionConnection import com.margelo.nitro.camera.public.NativeCameraDevice -fun CameraSessionConnection.getCameraInfo(): CameraInfo { - val cameraDevice = - input as? NativeCameraDevice - ?: throw Error("CameraDevice $input is not of type `NativeCameraDevice`!") - return cameraDevice.cameraInfo +fun CameraSessionConnection.getCameraInfo(provider: ProcessCameraProvider): CameraInfo { + val cameraInfo = + input.match( + { deviceSpec -> + // unwrap CameraInfo + val device = + deviceSpec as? NativeCameraDevice + ?: throw Error("CameraDevice $input is not of type `NativeCameraDevice`!") + return@match device.cameraInfo + }, + { position -> + // "back" | "front" | "external" -> CameraInfo + return@match provider.getDefaultCamera(position) + }, + ) + return cameraInfo } diff --git a/packages/react-native-vision-camera/android/src/main/java/com/margelo/nitro/camera/extensions/ProcessCameraProvider+getDefaultCamera.kt b/packages/react-native-vision-camera/android/src/main/java/com/margelo/nitro/camera/extensions/ProcessCameraProvider+getDefaultCamera.kt new file mode 100644 index 0000000000..9213755cb4 --- /dev/null +++ b/packages/react-native-vision-camera/android/src/main/java/com/margelo/nitro/camera/extensions/ProcessCameraProvider+getDefaultCamera.kt @@ -0,0 +1,23 @@ +package com.margelo.nitro.camera.extensions + +import androidx.annotation.OptIn +import androidx.camera.core.CameraInfo +import androidx.camera.core.CameraSelector +import androidx.camera.core.ExperimentalLensFacing +import androidx.camera.lifecycle.ProcessCameraProvider +import com.margelo.nitro.camera.TargetCameraPosition + +@OptIn(ExperimentalLensFacing::class) +fun ProcessCameraProvider.getDefaultCamera(position: TargetCameraPosition): CameraInfo { + val selector = + when (position) { + TargetCameraPosition.FRONT -> CameraSelector.DEFAULT_FRONT_CAMERA + TargetCameraPosition.BACK -> CameraSelector.DEFAULT_BACK_CAMERA + TargetCameraPosition.EXTERNAL -> + CameraSelector + .Builder() + .requireLensFacing(CameraSelector.LENS_FACING_EXTERNAL) + .build() + } + return getCameraInfo(selector) +} diff --git a/packages/react-native-vision-camera/android/src/main/java/com/margelo/nitro/camera/hybrids/HybridCameraSession.kt b/packages/react-native-vision-camera/android/src/main/java/com/margelo/nitro/camera/hybrids/HybridCameraSession.kt index 042dfe16bd..3463e3b27b 100644 --- a/packages/react-native-vision-camera/android/src/main/java/com/margelo/nitro/camera/hybrids/HybridCameraSession.kt +++ b/packages/react-native-vision-camera/android/src/main/java/com/margelo/nitro/camera/hybrids/HybridCameraSession.kt @@ -78,7 +78,7 @@ class HybridCameraSession( 1 -> { // Single Camera Session val connection = connections.single() - val cameraInfo = connection.getCameraInfo() + val cameraInfo = connection.getCameraInfo(cameraProvider) val outputConfigurations = connection.outputs val config = ConstraintResolver.resolveConstraints(cameraInfo, outputConfigurations, connection.constraints) Log.i(TAG, "Binding use-cases: ${config.sessionConfig.useCases}") @@ -105,7 +105,7 @@ class HybridCameraSession( val allPreparedUseCases = mutableListOf() val configs = connections.map { connection -> - val cameraInfo = connection.getCameraInfo() + val cameraInfo = connection.getCameraInfo(cameraProvider) val outputs = connection.outputs.map { it.output as? NativeCameraOutput diff --git a/packages/react-native-vision-camera/ios/Extensions/AVFoundation/AVCaptureConnection+init.swift b/packages/react-native-vision-camera/ios/Extensions/AVFoundation/AVCaptureConnection+init.swift index 6853723623..c3fe69b4b3 100644 --- a/packages/react-native-vision-camera/ios/Extensions/AVFoundation/AVCaptureConnection+init.swift +++ b/packages/react-native-vision-camera/ios/Extensions/AVFoundation/AVCaptureConnection+init.swift @@ -9,7 +9,7 @@ import Foundation import NitroModules extension AVCaptureConnection { - convenience init(input: AVCaptureDeviceInput, output: any HybridCameraOutputSpec) throws { + convenience init(input: AVCaptureDeviceInput, output: ResolvedCameraSessionConnection.Output) throws { let mediaType = output.mediaType.toAVMediaType() let targetPorts = input.ports.filter { $0.mediaType == mediaType } guard !targetPorts.isEmpty else { @@ -20,19 +20,13 @@ extension AVCaptureConnection { } switch output { - case let hybridOutput as any NativeCameraOutput: + case .output(let output): // a) It's a normal AVCaptureSessionOutput - connect it to all its desired ports - self.init(inputPorts: targetPorts, output: hybridOutput.output) - case let hybridPreview as any NativePreviewViewOutput: + self.init(inputPorts: targetPorts, output: output.output) + case .preview(let preview): // b) It's a preview AVCapturePreviewLayer let port = targetPorts.first! - self.init(inputPort: port, videoPreviewLayer: hybridPreview.previewLayer) - default: - // c) It's a bird? It's a plane? no idea - throw RuntimeError.error( - withMessage: - "Connection output \"\(output)\" is not of type `NativeCameraOutput` or `NativePreviewViewOutput`!" - ) + self.init(inputPort: port, videoPreviewLayer: preview.previewLayer) } } } diff --git a/packages/react-native-vision-camera/ios/Extensions/AVFoundation/AVCaptureConnection+isConnectedTo.swift b/packages/react-native-vision-camera/ios/Extensions/AVFoundation/AVCaptureConnection+isConnectedTo.swift index c68f1ee2f9..a1a1a0a2c5 100644 --- a/packages/react-native-vision-camera/ios/Extensions/AVFoundation/AVCaptureConnection+isConnectedTo.swift +++ b/packages/react-native-vision-camera/ios/Extensions/AVFoundation/AVCaptureConnection+isConnectedTo.swift @@ -22,4 +22,13 @@ extension AVCaptureConnection { return false } } + + func isConnectedTo(output: ResolvedCameraSessionConnection.Output) -> Bool { + switch output { + case .output(let output): + return self.output == output.output + case .preview(let preview): + return self.videoPreviewLayer == preview.previewLayer + } + } } diff --git a/packages/react-native-vision-camera/ios/Extensions/AVFoundation/AVCaptureDevice+resolve.swift b/packages/react-native-vision-camera/ios/Extensions/AVFoundation/AVCaptureDevice+resolve.swift index 6cb9ab9ec8..d53f39291a 100644 --- a/packages/react-native-vision-camera/ios/Extensions/AVFoundation/AVCaptureDevice+resolve.swift +++ b/packages/react-native-vision-camera/ios/Extensions/AVFoundation/AVCaptureDevice+resolve.swift @@ -9,7 +9,7 @@ import Foundation import NitroModules extension AVCaptureDevice { - static func resolve(value: CameraDeviceOrPosition) throws -> AVCaptureDevice { + static func resolve(value: CameraDeviceOrPosition) throws(RuntimeError) -> AVCaptureDevice { switch value { case .first(let hybridCameraDeviceSpec): guard let device = hybridCameraDeviceSpec as? any NativeCameraDevice else { diff --git a/packages/react-native-vision-camera/ios/Extensions/AVFoundation/AVCaptureSession+addConnection.swift b/packages/react-native-vision-camera/ios/Extensions/AVFoundation/AVCaptureSession+addConnection.swift index 5b579d2be2..4ddf5e837e 100644 --- a/packages/react-native-vision-camera/ios/Extensions/AVFoundation/AVCaptureSession+addConnection.swift +++ b/packages/react-native-vision-camera/ios/Extensions/AVFoundation/AVCaptureSession+addConnection.swift @@ -14,9 +14,9 @@ import NitroModules // `NativeCameraDevice` directly, and only unwrap once in the caller? extension AVCaptureSession { - func addConnection(input: CameraDeviceOrPosition, output: any HybridCameraOutputSpec) throws { - // 1. Get the `AVCaptureDeviceInput` for our `input` - let deviceInput = try findDevice(for: input) + func addConnection(input device: AVCaptureDevice, output: ResolvedCameraSessionConnection.Output) throws { + // 1. Get the `AVCaptureDeviceInput` for our `device` + let deviceInput = try findDevice(for: device) // 2. Create the `AVCaptureConnection` (either output or preview, we have an extension init) let connection = try AVCaptureConnection(input: deviceInput, output: output) diff --git a/packages/react-native-vision-camera/ios/Extensions/AVFoundation/AVCaptureSession+addOutputWithNoConnections.swift b/packages/react-native-vision-camera/ios/Extensions/AVFoundation/AVCaptureSession+addOutputWithNoConnections.swift index 24a5a0d45a..f76d97abd3 100644 --- a/packages/react-native-vision-camera/ios/Extensions/AVFoundation/AVCaptureSession+addOutputWithNoConnections.swift +++ b/packages/react-native-vision-camera/ios/Extensions/AVFoundation/AVCaptureSession+addOutputWithNoConnections.swift @@ -9,11 +9,11 @@ import Foundation import NitroModules extension AVCaptureSession { - func addOutputWithNoConnections(_ outputSpec: any HybridCameraOutputSpec) throws { - switch outputSpec { - case let hybridOutput as any NativeCameraOutput: + func addOutputWithNoConnections(_ output: ResolvedCameraSessionConnection.Output) throws { + switch output { + case .output(let output): // It's a normal AVCaptureOutput - let output = hybridOutput.output + let output = output.output guard output.connections.isEmpty else { throw RuntimeError.error(withMessage: "The given Output \"\(output)\" is already connected to a different Camera Session!") } @@ -23,18 +23,15 @@ extension AVCaptureSession { } logger.info("Adding Output \(output)...") addOutputWithNoConnections(output) - case let hybridPreview as any NativePreviewViewOutput: + + case .preview(let preview): // It's an AVVideoPreviewLayer - we need to set its .session - guard hybridPreview.previewLayer.session == nil else { - throw RuntimeError.error(withMessage: "The given Preview Output \"\(hybridPreview)\" is already connected to a different Camera Session!") + let previewLayer = preview.previewLayer + guard previewLayer.session == nil else { + throw RuntimeError.error(withMessage: "The given Preview Output \"\(previewLayer)\" is already connected to a different Camera Session!") } - logger.info("Adding Preview \(hybridPreview.previewLayer)...") - hybridPreview.previewLayer.setSessionWithNoConnection(self) - default: - throw RuntimeError.error( - withMessage: - "Output \"\(outputSpec)\" is not of type `NativeCameraOutput` or `NativePreviewViewOutput`!" - ) + logger.info("Adding Preview \(previewLayer)...") + previewLayer.setSessionWithNoConnection(self) } } } diff --git a/packages/react-native-vision-camera/ios/Extensions/AVFoundation/AVCaptureSession+containsConnection.swift b/packages/react-native-vision-camera/ios/Extensions/AVFoundation/AVCaptureSession+containsConnection.swift index c6ce7c1870..e345b547e3 100644 --- a/packages/react-native-vision-camera/ios/Extensions/AVFoundation/AVCaptureSession+containsConnection.swift +++ b/packages/react-native-vision-camera/ios/Extensions/AVFoundation/AVCaptureSession+containsConnection.swift @@ -9,17 +9,18 @@ import Foundation import NitroModules extension AVCaptureSession { - func containsConnection(input: CameraDeviceOrPosition, output: any HybridCameraOutputSpec) - throws -> Bool - { - // 1. Get the `AVCaptureDeviceInput` for our given `input` - this throws if it isn't attached yet. - let deviceInput = try findDevice(for: input) - // 2. Find out if we currently have a connection from this input to this output + // TODO: Wait I think this is wrong and doesn't cover all outputs..? + func contains(connection: ResolvedCameraSessionConnection) -> Bool { + return self.connections.contains { c in + return connection.contains(connection: c) + } + } + func containsConnection( + input: AVCaptureDevice, + output: ResolvedCameraSessionConnection.Output + ) -> Bool { return self.connections.contains { connection in - let isConnectedToInput = connection.inputPorts.contains { port in - return port.input == deviceInput - } - return isConnectedToInput && connection.isConnectedTo(output: output) + return connection.deviceInput == input && connection.isConnectedTo(output: output) } } } diff --git a/packages/react-native-vision-camera/ios/Extensions/AVFoundation/AVCaptureSession+containsOutput.swift b/packages/react-native-vision-camera/ios/Extensions/AVFoundation/AVCaptureSession+containsOutput.swift index c5761d7d52..85c2250b04 100644 --- a/packages/react-native-vision-camera/ios/Extensions/AVFoundation/AVCaptureSession+containsOutput.swift +++ b/packages/react-native-vision-camera/ios/Extensions/AVFoundation/AVCaptureSession+containsOutput.swift @@ -9,20 +9,12 @@ import Foundation import NitroModules extension AVCaptureSession { - func containsOutput(_ outputSpec: any HybridCameraOutputSpec) throws -> Bool { - switch outputSpec { - case let hybridOutput as any NativeCameraOutput: - // It's a normal AVCaptureOutput - return outputs.contains(hybridOutput.output) - case let hybridPreview as any NativePreviewViewOutput: - // It's an AVVideoPreviewLayer - this is a bit different than normal outputs: - // We "add" it by setting its .session property. - return hybridPreview.previewLayer.session == self - default: - throw RuntimeError.error( - withMessage: - "Output \"\(outputSpec)\" is not of type `NativeCameraOutput` or `NativePreviewViewOutput`!" - ) + func containsOutput(_ output: ResolvedCameraSessionConnection.Output) -> Bool { + switch output { + case .output(let output): + return outputs.contains(output.output) + case .preview(let previewLayer): + return previewLayer.previewLayer.session == self } } } diff --git a/packages/react-native-vision-camera/ios/Extensions/AVFoundation/AVCaptureSession+findDevice.swift b/packages/react-native-vision-camera/ios/Extensions/AVFoundation/AVCaptureSession+findDevice.swift index bec7c50f57..dd48dc154e 100644 --- a/packages/react-native-vision-camera/ios/Extensions/AVFoundation/AVCaptureSession+findDevice.swift +++ b/packages/react-native-vision-camera/ios/Extensions/AVFoundation/AVCaptureSession+findDevice.swift @@ -15,9 +15,11 @@ import NitroModules extension AVCaptureSession { func findDevice(for input: CameraDeviceOrPosition) throws -> AVCaptureDeviceInput { - // 1. Resolve input to a AVCaptureDevice let device = try AVCaptureDevice.resolve(value: input) - // 2. Get a device-input + return try findDevice(for: device) + } + + func findDevice(for device: AVCaptureDevice) throws -> AVCaptureDeviceInput { for attachedInput in self.inputs { guard let attachedInput = attachedInput as? AVCaptureDeviceInput else { continue @@ -27,10 +29,8 @@ extension AVCaptureSession { return attachedInput } } - // 4. We didn't find the device because it is not attached to the `CameraSession` yet. Throw. - throw RuntimeError.error( - withMessage: - "The given input \"\(input)\" is not yet attached to the `CameraSession` - cannot form a connection yet!" - ) + + // We didn't find it! + throw RuntimeError("The given device \"\(device)\" is not yet attached to the `CameraSession` - cannot form a connection yet!") } } diff --git a/packages/react-native-vision-camera/ios/Extensions/CameraSessionConnections+contains.swift b/packages/react-native-vision-camera/ios/Extensions/CameraSessionConnections+contains.swift index e71f04f5ae..ec3b337838 100644 --- a/packages/react-native-vision-camera/ios/Extensions/CameraSessionConnections+contains.swift +++ b/packages/react-native-vision-camera/ios/Extensions/CameraSessionConnections+contains.swift @@ -9,18 +9,6 @@ import Foundation import NitroModules extension Array where Element == CameraSessionConnection { - var containsOutputThatRequiresAudioInput: Bool { - return self.contains { connection in - return connection.outputs.contains { outputConfig in - if let output = outputConfig.output as? any NativeCameraOutput { - return output.requiresAudioInput - } else { - return false - } - } - } - } - func contains(input targetInput: AVCaptureInput) -> Bool { guard let targetInput = targetInput as? AVCaptureDeviceInput else { // We currently only support AVCaptureDeviceInput. I don't even know if there are other subclasses. diff --git a/packages/react-native-vision-camera/ios/Hybrid Objects/Constraints/ResolvableConstraint/ResolvableConstraint+AVAutoFocusSystem.swift b/packages/react-native-vision-camera/ios/Hybrid Objects/Constraints/ResolvableConstraint/ResolvableConstraint+AVAutoFocusSystem.swift index 4e68e5cb08..1cf973cddc 100644 --- a/packages/react-native-vision-camera/ios/Hybrid Objects/Constraints/ResolvableConstraint/ResolvableConstraint+AVAutoFocusSystem.swift +++ b/packages/react-native-vision-camera/ios/Hybrid Objects/Constraints/ResolvableConstraint/ResolvableConstraint+AVAutoFocusSystem.swift @@ -9,19 +9,6 @@ import AVFoundation extension AVCaptureDevice.Format.AutoFocusSystem: ResolvableConstraint { typealias ResolvedValue = Void - private var rank: Int { - switch self { - case .none: - return 0 - case .contrastDetection: - return 1 - case .phaseDetection: - return 2 - @unknown default: - return 3 - } - } - func resolve(for format: AVCaptureDevice.Format) -> ConstraintResolution { let targetAutoFocusSystem = self let actualAutoFocusSystem = format.autoFocusSystem diff --git a/packages/react-native-vision-camera/ios/Hybrid Objects/HybridCameraController.swift b/packages/react-native-vision-camera/ios/Hybrid Objects/HybridCameraController.swift index b249fd6b3f..ff48fd9f4f 100644 --- a/packages/react-native-vision-camera/ios/Hybrid Objects/HybridCameraController.swift +++ b/packages/react-native-vision-camera/ios/Hybrid Objects/HybridCameraController.swift @@ -14,17 +14,9 @@ final class HybridCameraController: HybridCameraControllerSpec, NativeCameraCont let captureDevice: AVCaptureDevice private var activeMeteringTask: MeteringTask? = nil - init(device: CameraDeviceOrPosition, queue: DispatchQueue) throws { - let captureDevice = try AVCaptureDevice.resolve(value: device) - self.captureDevice = captureDevice - switch device { - case .first(let hybridCameraDeviceSpec): - // Re-use the existing HybridObject - self.device = hybridCameraDeviceSpec - default: - // Create a new HybridObject otherwise - self.device = HybridCameraDevice(device: captureDevice) - } + init(device: any HybridCameraDeviceSpec & NativeCameraDevice, queue: DispatchQueue) throws { + self.device = device + self.captureDevice = device.device // TODO: Is it best practice to use the same `queue` as our HybridCameraSession? self.queue = queue diff --git a/packages/react-native-vision-camera/ios/Hybrid Objects/HybridCameraSession.swift b/packages/react-native-vision-camera/ios/Hybrid Objects/HybridCameraSession.swift index b7b0c3cf3c..01112938b7 100644 --- a/packages/react-native-vision-camera/ios/Hybrid Objects/HybridCameraSession.swift +++ b/packages/react-native-vision-camera/ios/Hybrid Objects/HybridCameraSession.swift @@ -46,16 +46,17 @@ final class HybridCameraSession: HybridCameraSessionSpec { let isMultiCam = connections.count > 1 if isMultiCam { guard self.session is AVCaptureMultiCamSession else { - throw RuntimeError.error( - withMessage: - "Cannot add multiple inputs (\(connections)) to a single-cam CameraSession! " - + "Create your CameraSession as a multi-cam session (`enableMultiCamSupport = true`) to add multiple camera inputs." - ) + throw RuntimeError( + "Cannot add multiple inputs (\(connections)) to a single-cam CameraSession! " + + "Create your CameraSession as a multi-cam session (`enableMultiCamSupport = true`) to add multiple camera inputs.") } } logger.info("Reconfiguring CameraSession with \(connections.count) connection(s)...") + // Resolve flat Connections upfront to AVCaptureDevice, AVOutput, etc etc + let connections = try connections.map { try ResolvedCameraSessionConnection(connection: $0) } + // Wrap the configuration in a batch self.session.beginConfiguration() defer { @@ -76,24 +77,11 @@ final class HybridCameraSession: HybridCameraSessionSpec { for connection in connections { // connection: try self.configureConnection(connection, isMultiCam: isMultiCam) - // outputs: - for outputConfiguration in connection.outputs { - switch outputConfiguration.output { - case let output as any NativeCameraOutput: - // Configure AVCaptureOutput - output.configure(config: outputConfiguration) - case let previewOutput as any NativePreviewViewOutput: - // Configure AVCaptureVideoPreviewLayer - previewOutput.configure(config: outputConfiguration) - default: - // wrong type! - break - } - } + self.configureOutputs(connection) } - // Apply any changes to the session connections + // If we have any Dynamic Range Constraints, we disable automatic color space selection on the AVCaptureSession. let hasCustomDynamicRangeConstraint = connections.contains { connection in return connection.constraints.contains { constraint in // If the user specified a custom DynamicRange, we cannot automatically adjust the ColorSpace @@ -103,8 +91,8 @@ final class HybridCameraSession: HybridCameraSessionSpec { self.session.automaticallyConfiguresCaptureDeviceForWideColor = !hasCustomDynamicRangeConstraint // Background Audio Playback - if let allowBackgroundAudioPlayback = config?.allowBackgroundAudioPlayback { - if #available(iOS 18.0, *) { + if #available(iOS 18.0, *) { + if let allowBackgroundAudioPlayback = config?.allowBackgroundAudioPlayback { self.session.configuresApplicationAudioSessionToMixWithOthers = allowBackgroundAudioPlayback } } @@ -117,9 +105,9 @@ final class HybridCameraSession: HybridCameraSessionSpec { } // pragma MARK: SessionConfiguration - private func configureConnection(_ connection: CameraSessionConnection, isMultiCam: Bool) throws { + private func configureConnection(_ connection: ResolvedCameraSessionConnection, isMultiCam: Bool) throws { // Get a handle to the input - let outputs = connection.outputs.map { $0.output } + let outputs = connection.outputs.map { $0.output.hybrid } let resolved = try ConstraintResolver.resolveConstraints( for: connection.input, constraints: connection.constraints, @@ -201,6 +189,18 @@ final class HybridCameraSession: HybridCameraSessionSpec { } } + private func configureOutputs(_ connection: ResolvedCameraSessionConnection) { + for outputConfiguration in connection.outputs { + let outputConfig = OutputConfiguration(mirrorMode: outputConfiguration.mirrorMode) + switch outputConfiguration.output { + case .output(let output): + output.configure(config: outputConfig) + case .preview(let preview): + preview.configure(config: outputConfig) + } + } + } + // pragma MARK: Start/Stop func start() -> Promise { return Promise.parallel(Self.queue) { @@ -225,19 +225,24 @@ final class HybridCameraSession: HybridCameraSessionSpec { * Adds all inputs on the given [targetConnections] if they haven't been added yet, * and removes all current inputs that aren't listed in the [connections] array. */ - private func updateInputs(_ targetConnections: [CameraSessionConnection]) throws { + private func updateInputs(_ targetConnections: [ResolvedCameraSessionConnection]) throws { + let requiresAudioInput = targetConnections.contains { $0.requiresAudioInput } + // 1. Loop through all existing inputs in the session - if it's not in our `connections` array, we remove it for currentlyAttachedInput in self.session.inputs { if currentlyAttachedInput.isMicrophone { // It's a microphone/audio input - let's check if we want audio in any connection. - if !targetConnections.containsOutputThatRequiresAudioInput { + if !requiresAudioInput { // 1.1.a. We don't want any audio input - remove it! logger.info("Removing audio input \(currentlyAttachedInput)...") self.session.removeInput(currentlyAttachedInput) } } else { // It's a normal camera device - let's check if we want it in connections. - if !targetConnections.contains(input: currentlyAttachedInput) { + let containsCurrentlyAttachedInput = targetConnections.contains { connection in + return connection.isConnectedTo(input: currentlyAttachedInput) + } + if !containsCurrentlyAttachedInput { // 1.1.b. We don't want this input - remove it! logger.info("Removing input \(currentlyAttachedInput)...") self.session.removeInput(currentlyAttachedInput) @@ -246,21 +251,28 @@ final class HybridCameraSession: HybridCameraSessionSpec { } // 2. Loop through all connection inputs - if it's not yet attached to the session, add it for connection in targetConnections { - let containsInput = try self.session.containsInput(connection.input) + let device = connection.input.device + let containsInput = self.session.inputs.contains { input in + guard let deviceInput = input as? AVCaptureDeviceInput else { + return false + } + return deviceInput.device == device + } if !containsInput { // 2.1. It doesn't exist yet - add it to the session.. - let input = try self.session.addInputWithNoConnections(connection.input) + let deviceInput = try AVCaptureDeviceInput(device: device) + self.session.addInputWithNoConnections(deviceInput) // 2.2. Configure the input with initial settings if connection.initialZoom != nil || connection.initialExposureBias != nil { try applyInitialConfig( - device: input.device, + device: device, initialZoom: connection.initialZoom, initialExposureBias: connection.initialExposureBias) } } } // 3. If we need an audio input, add it now - if targetConnections.containsOutputThatRequiresAudioInput { + if requiresAudioInput { let containsAudioInput = self.session.inputs.contains { $0.isMicrophone } if !containsAudioInput { guard AVCaptureDevice.authorizationStatus(for: .audio) == .authorized else { @@ -275,10 +287,13 @@ final class HybridCameraSession: HybridCameraSessionSpec { * and removes all current outputs that aren't listed in the [targetConnections] array. * This includes special handling for `NativePreviewViewOutput`. */ - private func updateOutputs(_ targetConnections: [CameraSessionConnection]) throws { + private func updateOutputs(_ targetConnections: [ResolvedCameraSessionConnection]) throws { // 1. Loop through all current CameraSession outputs - if it's not in our array, we remove it for currentlyAttachedOutput in self.session.outputs { - if !targetConnections.contains(output: currentlyAttachedOutput) { + let containsAttachedOutput = targetConnections.contains { connection in + return connection.isConnectedTo(output: currentlyAttachedOutput) + } + if !containsAttachedOutput { // 1.1. We don't want this output - remove it! logger.info("Removing output \(currentlyAttachedOutput)...") self.session.removeOutput(currentlyAttachedOutput) @@ -286,19 +301,18 @@ final class HybridCameraSession: HybridCameraSessionSpec { } // 2. Loop through all connections for connection in targetConnections { - // 3. Loop through all outputs - for outputConfiguration in connection.outputs { - let output = outputConfiguration.output - let containsOutput = try self.session.containsOutput(output) + // 2.1. Loop through each output + for output in connection.outputs { + let containsOutput = self.session.containsOutput(output.output) if !containsOutput { - // 3.1. It doesn't exist yet - add it to the session.. - try session.addOutputWithNoConnections(output) + // 2.2. It doesn't exist yet - add it to the session.. + try session.addOutputWithNoConnections(output.output) } } } } - private func updateConnections(_ targetConnections: [CameraSessionConnection]) throws { + private func updateConnections(_ targetConnections: [ResolvedCameraSessionConnection]) throws { // By this time, `updateInputs(...)` and `updateOutputs(...)` has already been called. // This ensures that all unwanted inputs or outputs have been removed - and if an input // or output is removed from the session, the connection will also automatically be removed. @@ -306,7 +320,8 @@ final class HybridCameraSession: HybridCameraSessionSpec { // 1. Loop through all current CameraSession connections - if it's not in our array, we remove it for currentConnection in self.session.connections { - if !targetConnections.contains(connection: currentConnection) { + let containsConnection = targetConnections.contains { $0.contains(connection: currentConnection) } + if !containsConnection { // 1.1. We don't want this connection - remove it! logger.info("Removing connection \(currentConnection)...") self.session.removeConnection(currentConnection) @@ -315,19 +330,20 @@ final class HybridCameraSession: HybridCameraSessionSpec { // 2. Add all new connections that we don't have yet: for connection in targetConnections { + // 2.1. Iterate through all outputs for outputConfiguration in connection.outputs { - // 2.1. Add the camera -> output connection - let containsConnection = try self.session.containsConnection( - input: connection.input, + // 2.2. Add the camera -> output connection + let containsConnection = self.session.containsConnection( + input: connection.input.device, output: outputConfiguration.output) if !containsConnection { try self.session.addConnection( - input: connection.input, + input: connection.input.device, output: outputConfiguration.output) } // 2.2. If this `output` requires audio, add this connection too - if let output = outputConfiguration.output as? any NativeCameraOutput { + if case .output(let output) = outputConfiguration.output { if output.requiresAudioInput { let hasAudioConnection = self.session.containsAudioConnection(to: output.output) if !hasAudioConnection { @@ -340,10 +356,10 @@ final class HybridCameraSession: HybridCameraSessionSpec { } private func applyInitialConfig( - device: AVCaptureDevice, initialZoom: Double?, initialExposureBias: Double? - ) - throws - { + device: AVCaptureDevice, + initialZoom: Double?, + initialExposureBias: Double? + ) throws { try device.lockForConfiguration() defer { device.unlockForConfiguration() } if let initialZoom = initialZoom { diff --git a/packages/react-native-vision-camera/ios/Hybrid Objects/Outputs/HybridCameraDepthFrameOutput.swift b/packages/react-native-vision-camera/ios/Hybrid Objects/Outputs/HybridCameraDepthFrameOutput.swift index e600a6eac6..0be7d9d315 100644 --- a/packages/react-native-vision-camera/ios/Hybrid Objects/Outputs/HybridCameraDepthFrameOutput.swift +++ b/packages/react-native-vision-camera/ios/Hybrid Objects/Outputs/HybridCameraDepthFrameOutput.swift @@ -66,7 +66,7 @@ final class HybridCameraDepthFrameOutput: HybridCameraDepthFrameOutputSpec, Nati } } - func configure(config: CameraOutputConfiguration) { + func configure(config: OutputConfiguration) { // TODO: Somehow attach this to `connection`, so we dont have race conditions // where we read `self.mirrorMode` for an old Frame in `getMediaSampleMetadata` self.mirrorMode = config.mirrorMode diff --git a/packages/react-native-vision-camera/ios/Hybrid Objects/Outputs/HybridCameraFrameOutput.swift b/packages/react-native-vision-camera/ios/Hybrid Objects/Outputs/HybridCameraFrameOutput.swift index afba918669..17c7a5f13c 100644 --- a/packages/react-native-vision-camera/ios/Hybrid Objects/Outputs/HybridCameraFrameOutput.swift +++ b/packages/react-native-vision-camera/ios/Hybrid Objects/Outputs/HybridCameraFrameOutput.swift @@ -80,7 +80,7 @@ final class HybridCameraFrameOutput: HybridCameraFrameOutputSpec, NativeCameraOu } } - func configure(config: CameraOutputConfiguration) { + func configure(config: OutputConfiguration) { // TODO: Somehow attach this to `connection`, so we dont have race conditions // where we read `self.mirrorMode` for an old Frame in `getMediaSampleMetadata` self.mirrorMode = config.mirrorMode diff --git a/packages/react-native-vision-camera/ios/Hybrid Objects/Outputs/HybridCameraObjectOutput.swift b/packages/react-native-vision-camera/ios/Hybrid Objects/Outputs/HybridCameraObjectOutput.swift index ebde0a6d94..9e9270ae99 100644 --- a/packages/react-native-vision-camera/ios/Hybrid Objects/Outputs/HybridCameraObjectOutput.swift +++ b/packages/react-native-vision-camera/ios/Hybrid Objects/Outputs/HybridCameraObjectOutput.swift @@ -39,7 +39,7 @@ final class HybridCameraObjectOutput: HybridCameraObjectOutputSpec, NativeCamera output.setMetadataObjectsDelegate(delegate, queue: queue) } - func configure(config: CameraOutputConfiguration) { + func configure(config: OutputConfiguration) { guard let connection = output.connection(with: .metadataObject) else { return } diff --git a/packages/react-native-vision-camera/ios/Hybrid Objects/Outputs/HybridCameraPhotoOutput.swift b/packages/react-native-vision-camera/ios/Hybrid Objects/Outputs/HybridCameraPhotoOutput.swift index 1f5a54cf18..5b82535074 100644 --- a/packages/react-native-vision-camera/ios/Hybrid Objects/Outputs/HybridCameraPhotoOutput.swift +++ b/packages/react-native-vision-camera/ios/Hybrid Objects/Outputs/HybridCameraPhotoOutput.swift @@ -75,7 +75,7 @@ final class HybridCameraPhotoOutput: HybridCameraPhotoOutputSpec, NativeCameraOu try? prepareDefaultPhotoSettings() } - func configure(config: CameraOutputConfiguration) { + func configure(config: OutputConfiguration) { guard let connection = output.connection(with: .video) else { return } diff --git a/packages/react-native-vision-camera/ios/Hybrid Objects/Outputs/HybridCameraPreviewOutput.swift b/packages/react-native-vision-camera/ios/Hybrid Objects/Outputs/HybridCameraPreviewOutput.swift index bc95c1f6d3..559829f4e6 100644 --- a/packages/react-native-vision-camera/ios/Hybrid Objects/Outputs/HybridCameraPreviewOutput.swift +++ b/packages/react-native-vision-camera/ios/Hybrid Objects/Outputs/HybridCameraPreviewOutput.swift @@ -52,7 +52,7 @@ final class HybridCameraPreviewOutput: HybridCameraPreviewOutputSpec, NativePrev orientationManager.stopOrientationUpdates() } - func configure(config: CameraOutputConfiguration) { + func configure(config: OutputConfiguration) { guard let connection = previewLayer.connection else { return } diff --git a/packages/react-native-vision-camera/ios/Hybrid Objects/Outputs/HybridCameraVideoFrameOutput.swift b/packages/react-native-vision-camera/ios/Hybrid Objects/Outputs/HybridCameraVideoFrameOutput.swift index 4abe05cbea..ddf5895cd3 100644 --- a/packages/react-native-vision-camera/ios/Hybrid Objects/Outputs/HybridCameraVideoFrameOutput.swift +++ b/packages/react-native-vision-camera/ios/Hybrid Objects/Outputs/HybridCameraVideoFrameOutput.swift @@ -78,7 +78,7 @@ final class HybridCameraVideoFrameOutput: HybridCameraVideoOutputSpec, NativeCam } } - func configure(config: CameraOutputConfiguration) { + func configure(config: OutputConfiguration) { // TODO: Set orientation via video metadata flags to avoid physically rotating buffers. // The problem with that is that we need to support switching Camera devices on the fly, // which rotates buffer later on somehow. diff --git a/packages/react-native-vision-camera/ios/Hybrid Objects/Outputs/HybridCameraVideoOutput.swift b/packages/react-native-vision-camera/ios/Hybrid Objects/Outputs/HybridCameraVideoOutput.swift index 6a9ebafd5d..a7c8f74c7b 100644 --- a/packages/react-native-vision-camera/ios/Hybrid Objects/Outputs/HybridCameraVideoOutput.swift +++ b/packages/react-native-vision-camera/ios/Hybrid Objects/Outputs/HybridCameraVideoOutput.swift @@ -58,7 +58,7 @@ final class HybridCameraVideoOutput: HybridCameraVideoOutputSpec, NativeCameraOu self.output.metadata = metadata } - func configure(config: CameraOutputConfiguration) { + func configure(config: OutputConfiguration) { guard let connection = output.connection(with: .video) else { return } diff --git a/packages/react-native-vision-camera/ios/Public/NativeCameraOutput.swift b/packages/react-native-vision-camera/ios/Public/NativeCameraOutput.swift index b1a1db6b75..d15d3eb9c2 100644 --- a/packages/react-native-vision-camera/ios/Public/NativeCameraOutput.swift +++ b/packages/react-native-vision-camera/ios/Public/NativeCameraOutput.swift @@ -55,5 +55,5 @@ public protocol NativeCameraOutput: AnyObject, ResolutionNegotiationParticipant * The `NativeCameraOutput` is expected to apply all configs * such as orientation or mirroring settings in here. */ - func configure(config: CameraOutputConfiguration) + func configure(config: OutputConfiguration) } diff --git a/packages/react-native-vision-camera/ios/Public/NativePreviewViewOutput.swift b/packages/react-native-vision-camera/ios/Public/NativePreviewViewOutput.swift index 8834e2a99d..865189ee69 100644 --- a/packages/react-native-vision-camera/ios/Public/NativePreviewViewOutput.swift +++ b/packages/react-native-vision-camera/ios/Public/NativePreviewViewOutput.swift @@ -11,11 +11,11 @@ public protocol NativePreviewViewOutput: AnyObject, ResolutionNegotiationPartici var previewLayer: AVCaptureVideoPreviewLayer { get } /** - * Called whenever the `CameraOutputConfiguration` might + * Called whenever the `OutputConfiguration` might * change, in a `beginConfiguration()`/`commitConfiguration()` * batch. * The `NativePreviewViewOutput` is expected to apply all configs * such as orientation or mirroring settings in here. */ - func configure(config: CameraOutputConfiguration) + func configure(config: OutputConfiguration) } diff --git a/packages/react-native-vision-camera/ios/Public/SessionConfig.swift b/packages/react-native-vision-camera/ios/Public/OutputConfig.swift similarity index 54% rename from packages/react-native-vision-camera/ios/Public/SessionConfig.swift rename to packages/react-native-vision-camera/ios/Public/OutputConfig.swift index efaaa31e64..b8b71b8d6f 100644 --- a/packages/react-native-vision-camera/ios/Public/SessionConfig.swift +++ b/packages/react-native-vision-camera/ios/Public/OutputConfig.swift @@ -1,5 +1,5 @@ /// -/// SessionConfig.swift +/// OutputConfiguration.swift /// VisionCamera /// Copyright © 2025 Marc Rousavy @ Margelo /// @@ -7,6 +7,6 @@ import AVFoundation import Foundation -public protocol SessionConfig { - var enablePhotoHDR: Bool { get } +public struct OutputConfiguration { + let mirrorMode: MirrorMode } diff --git a/packages/react-native-vision-camera/ios/Utils/ResolvedCameraSessionConnection.swift b/packages/react-native-vision-camera/ios/Utils/ResolvedCameraSessionConnection.swift new file mode 100644 index 0000000000..9c162d76c0 --- /dev/null +++ b/packages/react-native-vision-camera/ios/Utils/ResolvedCameraSessionConnection.swift @@ -0,0 +1,137 @@ +// +// ResolvedCameraSessionConnection.swift +// VisionCamera +// +// Created by Marc Rousavy on 07.01.26. +// + +import AVFoundation +import NitroModules + +/// Represents a resolved `CameraSessionConnection`. +struct ResolvedCameraSessionConnection { + let input: any HybridCameraDeviceSpec & NativeCameraDevice + let outputs: [OutputConfiguration] + let initialExposureBias: Double? + let initialZoom: Double? + let onSessionConfigSelected: ((_ config: (any HybridCameraSessionConfigSpec)) -> Void)? + let constraints: [Constraint] + + init(connection: CameraSessionConnection) throws { + self.input = try Self.resolveInput(connection.input) + self.outputs = try connection.outputs.map { try Self.resolveOutput($0) } + self.initialExposureBias = connection.initialExposureBias + self.initialZoom = connection.initialZoom + self.onSessionConfigSelected = connection.onSessionConfigSelected + self.constraints = connection.constraints + } + + var requiresAudioInput: Bool { + return outputs.contains { $0.output.requiresAudioInput } + } + + func isConnectedTo(input: AVCaptureInput) -> Bool { + guard let deviceInput = input as? AVCaptureDeviceInput else { + return false + } + return deviceInput.device == self.input.device + } + func isConnectedTo(output avOutput: AVCaptureOutput) -> Bool { + return outputs.contains { outputConfiguration in + switch outputConfiguration.output { + case .output(let output): + return output.output == avOutput + case .preview: + return false + } + } + } + func isConnectedTo(preview previewLayer: AVCaptureVideoPreviewLayer) -> Bool { + return outputs.contains { outputConfiguration in + switch outputConfiguration.output { + case .output: + return false + case .preview(let preview): + return preview.previewLayer == previewLayer + } + } + } + func contains(connection: AVCaptureConnection) -> Bool { + guard let input = connection.deviceInput else { + return false + } + if let output = connection.output { + return isConnectedTo(input: input) && isConnectedTo(output: output) + } else if let preview = connection.videoPreviewLayer { + return isConnectedTo(input: input) && isConnectedTo(preview: preview) + } else { + fatalError("AVCaptureConnection does not have .output or .videoPreviewLayer!") + } + } + + struct OutputConfiguration { + let output: Output + let mirrorMode: MirrorMode + } + enum Output { + case output(any HybridCameraOutputSpec & NativeCameraOutput) + case preview(any HybridCameraOutputSpec & NativePreviewViewOutput) + + var requiresAudioInput: Bool { + switch self { + case .output(let output): + return output.requiresAudioInput + case .preview: + return false + } + } + + var mediaType: MediaType { + switch self { + case .output(let output): + return output.mediaType + case .preview(let preview): + return preview.mediaType + } + } + + var hybrid: any HybridCameraOutputSpec { + switch self { + case .output(let output): + return output + case .preview(let preview): + return preview + } + } + } + + private static func resolveInput(_ input: CameraDeviceOrPosition) throws -> any HybridCameraDeviceSpec & NativeCameraDevice { + switch input { + case .first(let hybridCameraDeviceSpec): + guard let input = hybridCameraDeviceSpec as? any HybridCameraDeviceSpec & NativeCameraDevice else { + throw RuntimeError("The given CameraDevice \"\(hybridCameraDeviceSpec)\" does not conform to `NativeCameraDevice`!") + } + return input + case .second(let targetCameraPosition): + guard let device = AVCaptureDevice.default(for: targetCameraPosition) else { + throw RuntimeError("No CameraDevice exists at position \"\(targetCameraPosition)\"!") + } + return HybridCameraDevice(device: device) + } + } + private static func resolveOutput(_ outputConfiguration: CameraOutputConfiguration) throws -> OutputConfiguration { + switch outputConfiguration.output { + case let hybridOutput as any HybridCameraOutputSpec & NativeCameraOutput: + // It's a normal AVCaptureOutput + return OutputConfiguration( + output: .output(hybridOutput), + mirrorMode: outputConfiguration.mirrorMode) + case let hybridPreview as any HybridCameraOutputSpec & NativePreviewViewOutput: + return OutputConfiguration( + output: .preview(hybridPreview), + mirrorMode: outputConfiguration.mirrorMode) + default: + throw RuntimeError("Output \"\(outputConfiguration.output)\" is not of type `NativeCameraOutput` or `NativePreviewViewOutput`!") + } + } +} From 1d8d937280a2d65819564621907e6fbad5738829 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Wed, 1 Jul 2026 12:02:34 +0200 Subject: [PATCH 05/11] chore: Add test for `input: TargetCameraPosition` --- .../__tests__/visioncamera.session.harness.ts | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/apps/simple-camera/__tests__/visioncamera.session.harness.ts b/apps/simple-camera/__tests__/visioncamera.session.harness.ts index 65e32ded8b..bdcde17b9f 100644 --- a/apps/simple-camera/__tests__/visioncamera.session.harness.ts +++ b/apps/simple-camera/__tests__/visioncamera.session.harness.ts @@ -5,7 +5,10 @@ import { it, waitUntil, } from 'react-native-harness' -import type { CameraDeviceFactory } from 'react-native-vision-camera' +import type { + CameraDeviceFactory, + TargetCameraPosition, +} from 'react-native-vision-camera' import { CommonResolutions, VisionCamera } from 'react-native-vision-camera' import { provider as workletsProvider } from 'react-native-vision-camera-worklets' import { scheduleOnRN } from 'react-native-worklets' @@ -71,6 +74,38 @@ describe('VisionCamera - Session', () => { } }) + it('configures a session directly from each target camera position', async () => { + const positions: TargetCameraPosition[] = ['back', 'front', 'external'] + + for (const position of positions) { + const hasDeviceAtPosition = factory.cameraDevices.some( + (device) => device.position === position, + ) + const session = await VisionCamera.createCameraSession(false) + + try { + const configurePromise = session.configure([ + { + input: position, + outputs: [], + constraints: [], + }, + ]) + + if (!hasDeviceAtPosition) { + await expect(configurePromise).rejects.toThrow() + continue + } + + const controllers = await configurePromise + expect(controllers).toHaveLength(1) + expect(controllers[0]?.device.position).toBe(position) + } finally { + await session.stop() + } + } + }) + it('fires onStarted/onStopped exactly once per lifecycle', async () => { const device = factory.getDefaultCamera('back') expect(device).toBeDefined() From c03fef972edd8f05e1923291940e561d045ce3b4 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Wed, 1 Jul 2026 12:03:32 +0200 Subject: [PATCH 06/11] re-use CameraSession --- .../__tests__/visioncamera.session.harness.ts | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/apps/simple-camera/__tests__/visioncamera.session.harness.ts b/apps/simple-camera/__tests__/visioncamera.session.harness.ts index bdcde17b9f..d4a6427dde 100644 --- a/apps/simple-camera/__tests__/visioncamera.session.harness.ts +++ b/apps/simple-camera/__tests__/visioncamera.session.harness.ts @@ -76,14 +76,13 @@ describe('VisionCamera - Session', () => { it('configures a session directly from each target camera position', async () => { const positions: TargetCameraPosition[] = ['back', 'front', 'external'] + const session = await VisionCamera.createCameraSession(false) - for (const position of positions) { - const hasDeviceAtPosition = factory.cameraDevices.some( - (device) => device.position === position, - ) - const session = await VisionCamera.createCameraSession(false) - - try { + try { + for (const position of positions) { + const hasDeviceAtPosition = factory.cameraDevices.some( + (device) => device.position === position, + ) const configurePromise = session.configure([ { input: position, @@ -100,9 +99,9 @@ describe('VisionCamera - Session', () => { const controllers = await configurePromise expect(controllers).toHaveLength(1) expect(controllers[0]?.device.position).toBe(position) - } finally { - await session.stop() } + } finally { + await session.stop() } }) From 8ace0bb044e81cdbee7379c00e1f4c0e7fac4177 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Wed, 1 Jul 2026 12:04:53 +0200 Subject: [PATCH 07/11] Add outputs to the test too --- .../__tests__/visioncamera.session.harness.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/apps/simple-camera/__tests__/visioncamera.session.harness.ts b/apps/simple-camera/__tests__/visioncamera.session.harness.ts index d4a6427dde..5c6612fd96 100644 --- a/apps/simple-camera/__tests__/visioncamera.session.harness.ts +++ b/apps/simple-camera/__tests__/visioncamera.session.harness.ts @@ -77,6 +77,13 @@ describe('VisionCamera - Session', () => { it('configures a session directly from each target camera position', async () => { const positions: TargetCameraPosition[] = ['back', 'front', 'external'] const session = await VisionCamera.createCameraSession(false) + const previewOutput = VisionCamera.createPreviewOutput() + const photoOutput = VisionCamera.createPhotoOutput({ + containerFormat: 'native', + quality: 1, + qualityPrioritization: 'balanced', + targetResolution: CommonResolutions.HD_4_3, + }) try { for (const position of positions) { @@ -86,7 +93,13 @@ describe('VisionCamera - Session', () => { const configurePromise = session.configure([ { input: position, - outputs: [], + outputs: [ + { + output: previewOutput, + mirrorMode: position === 'front' ? 'on' : 'auto', + }, + { output: photoOutput, mirrorMode: 'auto' }, + ], constraints: [], }, ]) From 19f1ee9fc48301896ed7e37e6c42332ce031bef9 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Wed, 1 Jul 2026 12:06:40 +0200 Subject: [PATCH 08/11] docs --- .../src/specs/session/CameraSession.nitro.ts | 29 +++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/packages/react-native-vision-camera/src/specs/session/CameraSession.nitro.ts b/packages/react-native-vision-camera/src/specs/session/CameraSession.nitro.ts index 91d77addd3..02f4cd26a0 100644 --- a/packages/react-native-vision-camera/src/specs/session/CameraSession.nitro.ts +++ b/packages/react-native-vision-camera/src/specs/session/CameraSession.nitro.ts @@ -37,7 +37,17 @@ export type InterruptionReason = * per output, or multi-cam {@linkcode CameraSession}s. * * @example - * Create a Camera Session using the hooks: + * Create a Camera Session with the default 'back' CameraDevice using the hooks API: + * ```ts + * const photoOutput = ... + * const camera = useCamera({ + * isActive: true, + * input: 'back', + * outputs: [photoOutput] + * }) + * ``` + * @example + * Create a Camera Session with a custom CameraDevice using the hooks API: * ```ts * const device = ... * const photoOutput = ... @@ -142,7 +152,22 @@ export interface CameraSession * be used together - see {@linkcode CameraDeviceFactory.supportedMultiCamDeviceCombinations} * * @example - * Creating a simple Preview + Photo connection: + * Creating a simple Preview + Photo connection for the default 'back' CameraDevice: + * ```ts + * const session = ... + * const [controller] = await session.configure([ + * { + * input: 'back', + * outputs: [ + * { output: previewOutput, mirrorMode: 'auto' }, + * { output: photoOutput, mirrorMode: 'auto' }, + * ], + * constraints: [] + * } + * ]) + * ``` + * @example + * Creating a simple Preview + Photo connection with a custom CameraDevice: * ```ts * const session = ... * const device = ... From adfa0cedb0bcd7daac8c495890afcd75e27563f8 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Wed, 1 Jul 2026 12:31:28 +0200 Subject: [PATCH 09/11] fix: Make `useCameraDeviceExtensions` use `useCameraDeviceFactory()` --- .../src/hooks/useCameraDeviceExtensions.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/react-native-vision-camera/src/hooks/useCameraDeviceExtensions.ts b/packages/react-native-vision-camera/src/hooks/useCameraDeviceExtensions.ts index 8a435c9b6a..1b5cb39029 100644 --- a/packages/react-native-vision-camera/src/hooks/useCameraDeviceExtensions.ts +++ b/packages/react-native-vision-camera/src/hooks/useCameraDeviceExtensions.ts @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react' -import { getSupportedExtensions } from '../CameraDevices' import type { CameraDevice } from '../specs/inputs/CameraDevice.nitro' import type { CameraExtension } from '../specs/inputs/CameraExtension.nitro' +import { useCameraDeviceFactory } from './internal/useCameraDeviceFactory' /** * Get a list of all available {@linkcode CameraExtension}s for this specific @@ -16,16 +16,19 @@ import type { CameraExtension } from '../specs/inputs/CameraExtension.nitro' export function useCameraDeviceExtensions( device: CameraDevice | undefined, ): CameraExtension[] | undefined { + const factory = useCameraDeviceFactory() const [extensions, setExtensions] = useState([]) useEffect(() => { if (device == null) return + if (factory == null) return + const load = async () => { - const exts = await getSupportedExtensions(device) + const exts = await factory.getSupportedExtensions(device) setExtensions(exts) } load() - }, [device]) + }, [device, factory]) return extensions } From 266e1d6089d6d39d2fc8787e32dd3171451b98cd Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Wed, 1 Jul 2026 12:35:04 +0200 Subject: [PATCH 10/11] Update docs to remove deprecated APIs --- docs/content/docs/camera-extensions.mdx | 8 +++++--- docs/content/docs/camera-outputs.mdx | 6 ++---- docs/content/docs/constraints.mdx | 9 +++------ docs/content/docs/depth-output.mdx | 3 +-- docs/content/docs/devices.mdx | 24 +++++++++++++----------- docs/content/docs/fps.mdx | 3 +-- docs/content/docs/frame-output.mdx | 3 +-- docs/content/docs/low-light-boost.mdx | 3 ++- docs/content/docs/object-output.mdx | 3 +-- docs/content/docs/performance.mdx | 6 +++--- docs/content/docs/photo-output.mdx | 3 +-- docs/content/docs/preview-output.mdx | 3 +-- docs/content/docs/video-output.mdx | 3 +-- 13 files changed, 35 insertions(+), 42 deletions(-) diff --git a/docs/content/docs/camera-extensions.mdx b/docs/content/docs/camera-extensions.mdx index b989ee2e59..9bdfc9496f 100644 --- a/docs/content/docs/camera-extensions.mdx +++ b/docs/content/docs/camera-extensions.mdx @@ -10,7 +10,7 @@ Some [`CameraDevice`](/api/react-native-vision-camera/hybrid-objects/CameraDevic ### Getting available Extensions -To get all available [`CameraExtension`](/api/react-native-vision-camera/hybrid-objects/CameraExtension)s, use [`getSupportedExtensions(...)`](/api/react-native-vision-camera/functions/getSupportedExtensions): +To get all available [`CameraExtension`](/api/react-native-vision-camera/hybrid-objects/CameraExtension)s, use [`useCameraDeviceExtensions(...)`](/api/react-native-vision-camera/functions/useCameraDeviceExtensions) or [`CameraDeviceFactory.getSupportedExtensions(...)`](/api/react-native-vision-camera/hybrid-objects/CameraDeviceFactory#getsupportedextensions): @@ -22,7 +22,8 @@ const extensions = useCameraDeviceExtensions(device) ```ts const device = ... -const extensions = await getSupportedExtensions(device) +const deviceFactory = await VisionCamera.createDeviceFactory() +const extensions = await deviceFactory.getSupportedExtensions(device) ``` @@ -71,7 +72,8 @@ function App() { ```tsx const device = ... const session = ... -const extensions = await getSupportedExtensions(device) +const deviceFactory = await VisionCamera.createDeviceFactory() +const extensions = await deviceFactory.getSupportedExtensions(device) const extension = extensions.find((e) => e.type === 'night') const controllers = await session.configure([ diff --git a/docs/content/docs/camera-outputs.mdx b/docs/content/docs/camera-outputs.mdx index f35565076a..02c31c6dce 100644 --- a/docs/content/docs/camera-outputs.mdx +++ b/docs/content/docs/camera-outputs.mdx @@ -55,13 +55,12 @@ function App() { ```tsx const session = await VisionCamera.createCameraSession(false) -const device = await getDefaultCameraDevice('back') const photoOutput = ... const videoOutput = ... await session.configure([ { - input: device, + input: 'back', // [!code ++] outputs: [ { output: photoOutput, mirrorMode: 'auto' }, @@ -154,12 +153,11 @@ function App() { ```tsx const session = await VisionCamera.createCameraSession(false) -const device = await getDefaultCameraDevice('back') const output = ... await session.configure([ { - input: device, + input: 'back', outputs: [ { output: output, mirrorMode: 'auto' } ], diff --git a/docs/content/docs/constraints.mdx b/docs/content/docs/constraints.mdx index 2d78532008..8f9629553a 100644 --- a/docs/content/docs/constraints.mdx +++ b/docs/content/docs/constraints.mdx @@ -61,11 +61,10 @@ const session = await VisionCamera.createCameraSession(false) const videoOutput = createVideoOutput({ targetResolution: CommonResolutions.UHD_16_9 }) -const device = await getDefaultCameraDevice('back') await session.configure([ { - input: device, + input: 'back', outputs: [videoOutput], // [!code ++:4] constraints: [ @@ -136,11 +135,10 @@ const session = await VisionCamera.createCameraSession(false) const videoOutput = createVideoOutput({ targetResolution: CommonResolutions.UHD_16_9 }) -const device = await getDefaultCameraDevice('back') await session.configure([ { - input: device, + input: 'back', outputs: [videoOutput], // [!code ++:4] constraints: [ @@ -217,11 +215,10 @@ const session = await VisionCamera.createCameraSession(false) const videoOutput = createVideoOutput({ targetResolution: CommonResolutions.UHD_16_9 }) -const device = await getDefaultCameraDevice('back') await session.configure([ { - input: device, + input: 'back', outputs: [videoOutput], // [!code ++:3] constraints: [ diff --git a/docs/content/docs/depth-output.mdx b/docs/content/docs/depth-output.mdx index a284af6e27..4a6703736d 100644 --- a/docs/content/docs/depth-output.mdx +++ b/docs/content/docs/depth-output.mdx @@ -62,7 +62,6 @@ function App() { ```tsx const session = await VisionCamera.createCameraSession(false) -const device = await getDefaultCameraDevice('back') // [!code ++:9] const depthOutput = VisionCamera.createDepthFrameOutput({ /* options */ }) const workletRuntime = createWorkletRuntimeForThread(depthOutput.thread) @@ -76,7 +75,7 @@ scheduleOnRuntime(workletRuntime, () => { await session.configure([ { - input: device, + input: 'back', outputs: [ // [!code ++] { output: depthOutput, mirrorMode: 'auto' } diff --git a/docs/content/docs/devices.mdx b/docs/content/docs/devices.mdx index 6b193bfbae..c02fe505db 100644 --- a/docs/content/docs/devices.mdx +++ b/docs/content/docs/devices.mdx @@ -17,7 +17,8 @@ const device = useCameraDevice("back") ```ts -const device = await getDefaultCameraDevice("back") +const deviceFactory = await VisionCamera.createDeviceFactory() +const device = deviceFactory.getDefaultCamera("back") ``` @@ -79,8 +80,8 @@ const device = useCameraDevice("back", { ```ts -const devices = getAllCameraDevices() -const device = getCameraDevice(devices, "back", { +const deviceFactory = await VisionCamera.createDeviceFactory() +const device = getCameraDevice(deviceFactory, "back", { physicalDevices: ['ultra-wide-angle', 'wide-angle', 'telephoto'] }) ``` @@ -111,8 +112,9 @@ const devices = useCameraDevices() ```ts -let devices = getAllCameraDevices() -addOnCameraDevicesChangedListener((d) => { +const deviceFactory = await VisionCamera.createDeviceFactory() +let devices = deviceFactory.cameraDevices +deviceFactory.addOnCameraDevicesChangedListener((d) => { devices = d }) ``` @@ -134,14 +136,14 @@ As an external Camera is plugged in or plugged out from the device, the [`useCam ```ts -const devices = getAllCameraDevices() -let device = getCameraDevice(devices, "external") -addOnCameraDevicesChangedListener((d) => { - device = getCameraDevice(d, "external") +const deviceFactory = await VisionCamera.createDeviceFactory() +let device = getCameraDevice(deviceFactory, "external") +deviceFactory.addOnCameraDevicesChangedListener(() => { + device = getCameraDevice(deviceFactory, "external") }) ``` -As an external Camera is plugged in or plugged out from the device, the [`addOnCameraDevicesChangedListener(...)`](/api/react-native-vision-camera/functions/addOnCameraDevicesChangedListener) listener fires and you can call [`getCameraDevice(...)`](/api/react-native-vision-camera/functions/getCameraDevice) again to possibly detect new devices, or avoid using removed devices. +As an external Camera is plugged in or plugged out from the device, the [`CameraDeviceFactory.addOnCameraDevicesChangedListener(...)`](/api/react-native-vision-camera/hybrid-objects/CameraDeviceFactory#addoncameradeviceschangedlistener) listener fires and you can call [`getCameraDevice(...)`](/api/react-native-vision-camera/functions/getCameraDevice) again to possibly detect new devices, or avoid using removed devices. @@ -182,7 +184,7 @@ function App() { ```tsx const session = await VisionCamera.createCameraSession(false) -const device = await getDefaultCameraDevice('back') +const device = ... await session.configure([ { // [!code ++] diff --git a/docs/content/docs/fps.mdx b/docs/content/docs/fps.mdx index 2077d6d642..a75633da7e 100644 --- a/docs/content/docs/fps.mdx +++ b/docs/content/docs/fps.mdx @@ -64,10 +64,9 @@ function App() { ```tsx const session = await VisionCamera.createCameraSession(false) -const device = await getDefaultCameraDevice('back') await session.configure([ { - input: device, + input: 'back', constraints: [ // [!code ++] { fps: 60 } diff --git a/docs/content/docs/frame-output.mdx b/docs/content/docs/frame-output.mdx index b0a51d1d6f..dde5197e0c 100644 --- a/docs/content/docs/frame-output.mdx +++ b/docs/content/docs/frame-output.mdx @@ -65,7 +65,6 @@ function App() { ```tsx const session = await VisionCamera.createCameraSession(false) -const device = await getDefaultCameraDevice('back') // [!code ++:9] const frameOutput = VisionCamera.createFrameOutput({ /* options */ }) const workletRuntime = createWorkletRuntimeForThread(frameOutput.thread) @@ -79,7 +78,7 @@ scheduleOnRuntime(workletRuntime, () => { await session.configure([ { - input: device, + input: 'back', outputs: [ // [!code ++] { output: frameOutput, mirrorMode: 'auto' } diff --git a/docs/content/docs/low-light-boost.mdx b/docs/content/docs/low-light-boost.mdx index 35570b7b4c..ff0cfd5991 100644 --- a/docs/content/docs/low-light-boost.mdx +++ b/docs/content/docs/low-light-boost.mdx @@ -117,7 +117,8 @@ function App() { ```tsx const device = ... const session = ... -const extensions = await getSupportedExtensions(device) +const deviceFactory = await VisionCamera.createDeviceFactory() +const extensions = await deviceFactory.getSupportedExtensions(device) const extension = extensions.find((e) => e.type === 'night') const controllers = await session.configure([ diff --git a/docs/content/docs/object-output.mdx b/docs/content/docs/object-output.mdx index eebb403412..c7f30a6689 100644 --- a/docs/content/docs/object-output.mdx +++ b/docs/content/docs/object-output.mdx @@ -59,7 +59,6 @@ function App() { ```tsx const session = await VisionCamera.createCameraSession(false) -const device = await getDefaultCameraDevice('back') // [!code ++:6] const objectOutput = VisionCamera.createObjectOutput({ enabledObjectTypes: ['qr'] @@ -70,7 +69,7 @@ objectOutput.setOnObjectsScannedCallback((objects) => { await session.configure([ { - input: device, + input: 'back', outputs: [ // [!code ++] { output: objectOutput, mirrorMode: 'auto' } diff --git a/docs/content/docs/performance.mdx b/docs/content/docs/performance.mdx index ab2aa8b30d..ec5da6bd6e 100644 --- a/docs/content/docs/performance.mdx +++ b/docs/content/docs/performance.mdx @@ -29,11 +29,11 @@ const slowerDevice = useCameraDevice('back', { ```ts -const devices = getAllCameraDevices() -const fasterDevice = getCameraDevice(devices, 'back', { +const deviceFactory = await VisionCamera.createDeviceFactory() +const fasterDevice = getCameraDevice(deviceFactory, 'back', { physicalDevices: ['wide-angle-camera'], }) -const slowerDevice = getCameraDevice(devices, 'back', { +const slowerDevice = getCameraDevice(deviceFactory, 'back', { physicalDevices: ['ultra-wide-angle-camera', 'wide-angle-camera', 'telephoto-camera'], }) ``` diff --git a/docs/content/docs/photo-output.mdx b/docs/content/docs/photo-output.mdx index 76990b0b66..bde7128f86 100644 --- a/docs/content/docs/photo-output.mdx +++ b/docs/content/docs/photo-output.mdx @@ -48,13 +48,12 @@ function App() { ```tsx const session = await VisionCamera.createCameraSession(false) -const device = await getDefaultCameraDevice('back') // [!code ++] const photoOutput = VisionCamera.createPhotoOutput({ /* options */ }) await session.configure([ { - input: device, + input: 'back', outputs: [ // [!code ++] { output: photoOutput, mirrorMode: 'auto' } diff --git a/docs/content/docs/preview-output.mdx b/docs/content/docs/preview-output.mdx index 060ee51535..a8f43b2cb7 100644 --- a/docs/content/docs/preview-output.mdx +++ b/docs/content/docs/preview-output.mdx @@ -33,13 +33,12 @@ function App() { ```tsx const session = await VisionCamera.createCameraSession(false) -const device = await getDefaultCameraDevice('back') // [!code ++] const previewOutput = VisionCamera.createPreviewOutput() await session.configure([ { - input: device, + input: 'back', outputs: [ // [!code ++] { output: previewOutput, mirrorMode: 'auto' } diff --git a/docs/content/docs/video-output.mdx b/docs/content/docs/video-output.mdx index 74d6e0cf3c..254d558d5d 100644 --- a/docs/content/docs/video-output.mdx +++ b/docs/content/docs/video-output.mdx @@ -48,13 +48,12 @@ function App() { ```tsx const session = await VisionCamera.createCameraSession(false) -const device = await getDefaultCameraDevice('back') // [!code ++] const videoOutput = VisionCamera.createVideoOutput({ /* options */ }) await session.configure([ { - input: device, + input: 'back', outputs: [ // [!code ++] { output: videoOutput, mirrorMode: 'auto' } From 42882aee459193d0e4f46e4449b8084c70cfe9c9 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Wed, 1 Jul 2026 12:35:08 +0200 Subject: [PATCH 11/11] clean up --- .../main/java/com/margelo/nitro/camera/HybridCameraFactory.kt | 2 +- .../com/margelo/nitro/camera/hybrids/HybridCameraSession.kt | 4 +--- .../margelo/nitro/camera/hybrids/HybridCameraSessionConfig.kt | 2 -- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/react-native-vision-camera/android/src/main/java/com/margelo/nitro/camera/HybridCameraFactory.kt b/packages/react-native-vision-camera/android/src/main/java/com/margelo/nitro/camera/HybridCameraFactory.kt index d707795790..c67339c9f4 100644 --- a/packages/react-native-vision-camera/android/src/main/java/com/margelo/nitro/camera/HybridCameraFactory.kt +++ b/packages/react-native-vision-camera/android/src/main/java/com/margelo/nitro/camera/HybridCameraFactory.kt @@ -78,7 +78,7 @@ class VisionCamera : HybridCameraFactorySpec() { device as? NativeCameraDevice ?: throw Error("The given `device` was not of type `NativeCameraDevice`!") val config = ConstraintResolver.resolveConstraints(device.cameraInfo, outputConfigurations, constraints) - return@async HybridCameraSessionConfig(device.cameraInfo, config.sessionConfig, config.resolvedConfig) + return@async HybridCameraSessionConfig(config.sessionConfig, config.resolvedConfig) } } diff --git a/packages/react-native-vision-camera/android/src/main/java/com/margelo/nitro/camera/hybrids/HybridCameraSession.kt b/packages/react-native-vision-camera/android/src/main/java/com/margelo/nitro/camera/hybrids/HybridCameraSession.kt index 3463e3b27b..ab21e76c67 100644 --- a/packages/react-native-vision-camera/android/src/main/java/com/margelo/nitro/camera/hybrids/HybridCameraSession.kt +++ b/packages/react-native-vision-camera/android/src/main/java/com/margelo/nitro/camera/hybrids/HybridCameraSession.kt @@ -4,7 +4,6 @@ import android.annotation.SuppressLint import android.util.Log import androidx.annotation.UiThread import androidx.camera.core.Camera -import androidx.camera.core.CameraState import androidx.camera.core.ConcurrentCamera import androidx.camera.core.UseCaseGroup import androidx.camera.lifecycle.ProcessCameraProvider @@ -50,7 +49,6 @@ class HybridCameraSession( private var onErrorListeners = arrayListOf<(Throwable) -> Unit>() private var onInterruptionStartedListeners = arrayListOf<(InterruptionReason) -> Unit>() private var onInterruptionEndedListeners = arrayListOf<() -> Unit>() - private var currentCameraState = CameraState.Type.CLOSED @SuppressLint("RestrictedApi") override fun configure( @@ -85,7 +83,7 @@ class HybridCameraSession( if (connection.onSessionConfigSelected != null) { // Notify JS callback that we resolved the constraints to a specific `config` - val hybridConfig = HybridCameraSessionConfig(cameraInfo, config.sessionConfig, config.resolvedConfig) + val hybridConfig = HybridCameraSessionConfig(config.sessionConfig, config.resolvedConfig) connection.onSessionConfigSelected(hybridConfig) } diff --git a/packages/react-native-vision-camera/android/src/main/java/com/margelo/nitro/camera/hybrids/HybridCameraSessionConfig.kt b/packages/react-native-vision-camera/android/src/main/java/com/margelo/nitro/camera/hybrids/HybridCameraSessionConfig.kt index 05233485c4..24a192e6b2 100644 --- a/packages/react-native-vision-camera/android/src/main/java/com/margelo/nitro/camera/hybrids/HybridCameraSessionConfig.kt +++ b/packages/react-native-vision-camera/android/src/main/java/com/margelo/nitro/camera/hybrids/HybridCameraSessionConfig.kt @@ -1,6 +1,5 @@ package com.margelo.nitro.camera.hybrids -import androidx.camera.core.CameraInfo import androidx.camera.core.SessionConfig import com.margelo.nitro.camera.AutoFocusSystem import com.margelo.nitro.camera.HybridCameraSessionConfigSpec @@ -11,7 +10,6 @@ import com.margelo.nitro.camera.public.NativeCameraOutput import com.margelo.nitro.camera.public.NativeCameraSessionConfig class HybridCameraSessionConfig( - private val cameraInfo: CameraInfo, override val sessionConfig: SessionConfig, private val resolvedConfig: NativeCameraOutput.Config, ) : HybridCameraSessionConfigSpec(),