From c29c0e9f9e7c5e5db2cb4c941ba7ed192d917dfe Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Tue, 28 Apr 2026 15:54:34 +0200 Subject: [PATCH 1/9] feat: Add `supportedMultiCamDeviceCombinations` --- .../specs/inputs/CameraDeviceFactory.nitro.ts | 67 ++++++++++++++++++- 1 file changed, 65 insertions(+), 2 deletions(-) diff --git a/packages/react-native-vision-camera/src/specs/inputs/CameraDeviceFactory.nitro.ts b/packages/react-native-vision-camera/src/specs/inputs/CameraDeviceFactory.nitro.ts index 0a6f3c0852..6d04537536 100644 --- a/packages/react-native-vision-camera/src/specs/inputs/CameraDeviceFactory.nitro.ts +++ b/packages/react-native-vision-camera/src/specs/inputs/CameraDeviceFactory.nitro.ts @@ -1,4 +1,5 @@ import type { HybridObject } from 'react-native-nitro-modules' +import type { CameraFactory } from '../CameraFactory.nitro' import type { CameraPosition } from '../common-types/CameraPosition' import type { ListenerSubscription } from '../common-types/ListenerSubscription' import type { CameraSession } from '../session/CameraSession.nitro' @@ -12,8 +13,12 @@ import type { CameraExtension } from './CameraExtension.nitro' export interface CameraDeviceFactory extends HybridObject<{ ios: 'swift'; android: 'kotlin' }> { /** - * Get a list of all devices. - * This list may change as camera devices get plugged in/out. + * Get a list of all {@linkcode CameraDevice}s on this platform. + * + * This list may change as {@linkcode CameraDevice}s get plugged in/out (e.g. + * {@linkcode CameraPosition | 'external'} Cameras via USB/UVC), devices + * become available/unavailable (e.g. continuity Cameras), or Camera positions + * change (e.g. on foldable phones). */ readonly cameraDevices: CameraDevice[] /** @@ -22,6 +27,64 @@ export interface CameraDeviceFactory */ userPreferredCamera?: CameraDevice + /** + * A list of all {@linkcode CameraDevice} combinations that are supported + * in Multi-Cam {@linkcode CameraSession}s. + * + * This list always contains a subset of {@linkcode cameraDevices}, often + * less. + * + * @discussion + * For example, on many platforms only a {@linkcode CameraPosition | 'front'} + * and a {@linkcode CameraPosition | 'back'} {@linkcode CameraDevice} are + * supported to be used in a Multi-Cam {@linkcode CameraSession} - in this case, + * the returned 2D Array looks something like this: + * ``` + * [ + * [{ position: 'back', ... }, { position: 'front', ... }] + * ] + * ``` + * Two {@linkcode CameraPosition | 'back'}-, or two {@linkcode CameraPosition | 'front'} + * {@linkcode CameraDevice}s are often not supported together in a Multi-Cam + * {@linkcode CameraSession}. + * + * When creating a Multi-Cam {@linkcode CameraSession}, you must ensure + * that you are using Device combinations that are actually supported + * on the platform, otherwise the session might fail to configure. + * + * @discussion + * If the platform does not support Multi-Cam {@linkcode CameraSession}s, + * an empty array (`[]`) will be returned. + * + * + * @example + * ```ts + * if (VisionCamera.supportsMultiCamSessions) { + * const deviceFactory = await VisionCamera.createDeviceFactory() + * const deviceCombinations = deviceFactory.supportedMultiCamDeviceCombinations[0] + * if (deviceCombinations != null) { + * const connections = deviceCombinations.map((device) => { + * const previewOutput = VisionCamera.createPreviewOutput() + * return { + * input: device, + * outputs: [ + * { output: previewOutput, mirrorMode: 'auto' } + * ], + * constraints: [] + * } satisfies CameraSessionConnection + * }) + * + * const session = await VisionCamera.createCameraSession(true) + * const controllers = await session.configure(connections) + * await session.start() + * } + * } + * ``` + * + * @see {@linkcode CameraFactory.supportsMultiCamSessions} + */ + readonly supportedMultiCamDeviceCombinations: CameraDevice[][] + /** * Add a listener to be called whenever the * available {@linkcode cameraDevices} change, From 7714556fd1ee8545e50951d0472f6cbb9ff328ca Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Tue, 28 Apr 2026 16:00:39 +0200 Subject: [PATCH 2/9] docs --- .../src/specs/session/CameraSession.nitro.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) 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 6b19b9ab29..8390dac2b9 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 @@ -68,6 +68,10 @@ export type InterruptionReason = * and back-Camera using the imperative API: * ```ts * if (VisionCamera.supportsMultiCamSessions) { + * const deviceFactory = await VisionCamera.createDeviceFactory() + * const deviceCombinations = deviceFactory.supportedMultiCamDeviceCombinations + * // ... get `frontDevice` and `backDevice` from one of + * // the combinations in `deviceCombinations`. * const session = await VisionCamera.createCameraSession(true) * const [frontController, backController] = await session.configure([ * // Front Camera @@ -110,6 +114,12 @@ export interface CameraSession * from one {@linkcode CameraDevice} (the _input_) to multiple * {@linkcode CameraOutput}s (the _outputs_). * + * @note In a Multi-Cam Camera Session, the given {@linkcode connections}' input devices + * must be supported to be used together in a Multi-Cam Camera Session, as not every + * {@linkcode CameraDevice} can be used with every other {@linkcode CameraDevice} + * simultaneously. See {@linkcode CameraDeviceFactory.supportedMultiCamDeviceCombinations} + * for a full list of supported combinations. + * * @param connections The list of connections from one _input_ to * multiple _outputs_. If this {@linkcode CameraSession} was created * as a multi-cam session (see {@linkcode CameraFactory.createCameraSession | createCameraSession(enableMultiCam)}), @@ -122,6 +132,9 @@ export interface CameraSession * @throws If Camera Permission has not been granted - see {@linkcode CameraFactory.cameraPermissionStatus} * @throws If multiple {@linkcode connections} are added, but the * {@linkcode CameraSession} was not created as a multi-cam session. + * @throws If hardware resources are being exhausted by too large connection graphs. + * @throws If this is a Multi-Cam session, but the connection inputs are not supported to + * be used together - see {@linkcode CameraDeviceFactory.supportedMultiCamDeviceCombinations} * * @example * Creating a simple Preview + Photo connection: @@ -162,6 +175,10 @@ export interface CameraSession * Creating a multi-cam session with front- and back Camera: * ```ts * if (VisionCamera.supportsMultiCamSessions) { + * const deviceFactory = await VisionCamera.createDeviceFactory() + * const deviceCombinations = deviceFactory.supportedMultiCamDeviceCombinations + * // ... get `frontDevice` and `backDevice` from one of + * // the combinations in `deviceCombinations`. * const session = await VisionCamera.createCameraSession(true) * const [frontController, backController] = await session.configure([ * // Front Camera From aa76959c2e3996b637a865809ccd94516ec504d8 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Tue, 28 Apr 2026 16:03:52 +0200 Subject: [PATCH 3/9] feat: Implement it --- .../nitro/camera/HybridCameraDeviceFactory.kt | 7 ++++++ .../HybridCameraDeviceFactory.swift | 18 ++++++++++----- .../ios/Hybrid Objects/Recording/Track.swift | 2 +- .../c++/JHybridCameraDeviceFactorySpec.cpp | 23 +++++++++++++++++++ .../c++/JHybridCameraDeviceFactorySpec.hpp | 1 + .../camera/HybridCameraDeviceFactorySpec.kt | 4 ++++ .../ios/VisionCamera-Swift-Cxx-Bridge.hpp | 11 +++++++++ .../HybridCameraDeviceFactorySpecSwift.hpp | 4 ++++ .../swift/HybridCameraDeviceFactorySpec.swift | 1 + .../HybridCameraDeviceFactorySpec_cxx.swift | 22 ++++++++++++++++++ .../c++/HybridCameraDeviceFactorySpec.cpp | 1 + .../c++/HybridCameraDeviceFactorySpec.hpp | 1 + 12 files changed, 88 insertions(+), 7 deletions(-) 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 ce693d37ce..13aeb3e99a 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 @@ -38,6 +38,13 @@ class HybridCameraDeviceFactory( override val cameraDevices: Array get() = cameraProvider.availableCameraInfos.mapToArray { HybridCameraDevice(it) } + override val supportedMultiCamDeviceCombinations: Array> + get() { + return cameraProvider.concurrentCameraIds.mapToArray { devices -> + return@mapToArray { HybridCameraDevice(it) } + } + } + override var userPreferredCamera: HybridCameraDeviceSpec? get() { val preferredCameraId = 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 37af6deab6..42ef2f8014 100644 --- a/packages/react-native-vision-camera/ios/Hybrid Objects/HybridCameraDeviceFactory.swift +++ b/packages/react-native-vision-camera/ios/Hybrid Objects/HybridCameraDeviceFactory.swift @@ -9,11 +9,6 @@ import Foundation import NitroModules class HybridCameraDeviceFactory: HybridCameraDeviceFactorySpec { - let discoverySession: AVCaptureDevice.DiscoverySession - var cameraDevices: [any HybridCameraDeviceSpec] { - return discoverySession.devices.map { HybridCameraDevice(device: $0) } - } - override init() { self.discoverySession = AVCaptureDevice.DiscoverySession( deviceTypes: AVCaptureDevice.DeviceType.all, @@ -22,6 +17,17 @@ class HybridCameraDeviceFactory: HybridCameraDeviceFactorySpec { super.init() } + let discoverySession: AVCaptureDevice.DiscoverySession + var cameraDevices: [any HybridCameraDeviceSpec] { + return discoverySession.devices.map { HybridCameraDevice(device: $0) } + } + + var supportedMultiCamDeviceCombinations: [[any HybridCameraDeviceSpec]] { + return discoverySession.supportedMultiCamDeviceSets.map { devices in + return devices.map { HybridCameraDevice(device: $0) } + } + } + var userPreferredCamera: (any HybridCameraDeviceSpec)? { get { guard #available(iOS 17.0, *) else { @@ -36,7 +42,7 @@ class HybridCameraDeviceFactory: HybridCameraDeviceFactorySpec { guard #available(iOS 17.0, *) else { return } - guard let hybridDevice = newValue as? HybridCameraDevice else { + guard let hybridDevice = newValue as? any NativeCameraDevice else { return } AVCaptureDevice.userPreferredCamera = hybridDevice.device diff --git a/packages/react-native-vision-camera/ios/Hybrid Objects/Recording/Track.swift b/packages/react-native-vision-camera/ios/Hybrid Objects/Recording/Track.swift index 3c9302ae31..80ebef71a4 100644 --- a/packages/react-native-vision-camera/ios/Hybrid Objects/Recording/Track.swift +++ b/packages/react-native-vision-camera/ios/Hybrid Objects/Recording/Track.swift @@ -25,7 +25,7 @@ final class Track { /** Gets whether the track has been marked as finished or not. - + A track is finished when stop() was called, and a buffer with a later timestamp than the one from the stop() call arrives. */ diff --git a/packages/react-native-vision-camera/nitrogen/generated/android/c++/JHybridCameraDeviceFactorySpec.cpp b/packages/react-native-vision-camera/nitrogen/generated/android/c++/JHybridCameraDeviceFactorySpec.cpp index 3f96d33a4d..f0256d7dd8 100644 --- a/packages/react-native-vision-camera/nitrogen/generated/android/c++/JHybridCameraDeviceFactorySpec.cpp +++ b/packages/react-native-vision-camera/nitrogen/generated/android/c++/JHybridCameraDeviceFactorySpec.cpp @@ -88,6 +88,29 @@ namespace margelo::nitro::camera { static const auto method = _javaPart->javaClassStatic()->getMethod /* userPreferredCamera */)>("setUserPreferredCamera"); method(_javaPart, userPreferredCamera.has_value() ? std::dynamic_pointer_cast(userPreferredCamera.value())->getJavaPart() : nullptr); } + std::vector>> JHybridCameraDeviceFactorySpec::getSupportedMultiCamDeviceCombinations() { + static const auto method = _javaPart->javaClassStatic()->getMethod>>()>("getSupportedMultiCamDeviceCombinations"); + auto __result = method(_javaPart); + return [&]() { + size_t __size = __result->size(); + std::vector>> __vector; + __vector.reserve(__size); + for (size_t __i = 0; __i < __size; __i++) { + auto __element = __result->getElement(__i); + __vector.push_back([&]() { + size_t __size = __element->size(); + std::vector> __vector; + __vector.reserve(__size); + for (size_t __i = 0; __i < __size; __i++) { + auto __element = __element->getElement(__i); + __vector.push_back(__element->getJHybridCameraDeviceSpec()); + } + return __vector; + }()); + } + return __vector; + }(); + } // Methods ListenerSubscription JHybridCameraDeviceFactorySpec::addOnCameraDevicesChangedListener(const std::function>& /* newDevices */)>& listener) { diff --git a/packages/react-native-vision-camera/nitrogen/generated/android/c++/JHybridCameraDeviceFactorySpec.hpp b/packages/react-native-vision-camera/nitrogen/generated/android/c++/JHybridCameraDeviceFactorySpec.hpp index df9b7b23c1..19a60bea95 100644 --- a/packages/react-native-vision-camera/nitrogen/generated/android/c++/JHybridCameraDeviceFactorySpec.hpp +++ b/packages/react-native-vision-camera/nitrogen/generated/android/c++/JHybridCameraDeviceFactorySpec.hpp @@ -53,6 +53,7 @@ namespace margelo::nitro::camera { std::vector> getCameraDevices() override; std::optional> getUserPreferredCamera() override; void setUserPreferredCamera(const std::optional>& userPreferredCamera) override; + std::vector>> getSupportedMultiCamDeviceCombinations() override; public: // Methods diff --git a/packages/react-native-vision-camera/nitrogen/generated/android/kotlin/com/margelo/nitro/camera/HybridCameraDeviceFactorySpec.kt b/packages/react-native-vision-camera/nitrogen/generated/android/kotlin/com/margelo/nitro/camera/HybridCameraDeviceFactorySpec.kt index f610aeb748..b41306e76c 100644 --- a/packages/react-native-vision-camera/nitrogen/generated/android/kotlin/com/margelo/nitro/camera/HybridCameraDeviceFactorySpec.kt +++ b/packages/react-native-vision-camera/nitrogen/generated/android/kotlin/com/margelo/nitro/camera/HybridCameraDeviceFactorySpec.kt @@ -35,6 +35,10 @@ abstract class HybridCameraDeviceFactorySpec: HybridObject() { @set:DoNotStrip @set:Keep abstract var userPreferredCamera: HybridCameraDeviceSpec? + + @get:DoNotStrip + @get:Keep + abstract val supportedMultiCamDeviceCombinations: Array> // Methods abstract fun addOnCameraDevicesChangedListener(listener: (newDevices: Array) -> Unit): ListenerSubscription 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 5bde895e9d..5abcd00279 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 @@ -1426,6 +1426,17 @@ namespace margelo::nitro::camera::bridge::swift { return Result::withError(error); } + // pragma MARK: std::vector>> + /** + * Specialized version of `std::vector>>`. + */ + using std__vector_std__vector_std__shared_ptr_HybridCameraDeviceSpec___ = std::vector>>; + inline std::vector>> create_std__vector_std__vector_std__shared_ptr_HybridCameraDeviceSpec___(size_t size) noexcept { + std::vector>> vector; + vector.reserve(size); + return vector; + } + // pragma MARK: std::function>& /* newDevices */)> /** * Specialized version of `std::function>&)>`. diff --git a/packages/react-native-vision-camera/nitrogen/generated/ios/c++/HybridCameraDeviceFactorySpecSwift.hpp b/packages/react-native-vision-camera/nitrogen/generated/ios/c++/HybridCameraDeviceFactorySpecSwift.hpp index a13436fca5..f166161e87 100644 --- a/packages/react-native-vision-camera/nitrogen/generated/ios/c++/HybridCameraDeviceFactorySpecSwift.hpp +++ b/packages/react-native-vision-camera/nitrogen/generated/ios/c++/HybridCameraDeviceFactorySpecSwift.hpp @@ -89,6 +89,10 @@ namespace margelo::nitro::camera { inline void setUserPreferredCamera(const std::optional>& userPreferredCamera) noexcept override { _swiftPart.setUserPreferredCamera(userPreferredCamera); } + inline std::vector>> getSupportedMultiCamDeviceCombinations() noexcept override { + auto __result = _swiftPart.getSupportedMultiCamDeviceCombinations(); + return __result; + } public: // Methods diff --git a/packages/react-native-vision-camera/nitrogen/generated/ios/swift/HybridCameraDeviceFactorySpec.swift b/packages/react-native-vision-camera/nitrogen/generated/ios/swift/HybridCameraDeviceFactorySpec.swift index 62708a1149..ce0ba0d904 100644 --- a/packages/react-native-vision-camera/nitrogen/generated/ios/swift/HybridCameraDeviceFactorySpec.swift +++ b/packages/react-native-vision-camera/nitrogen/generated/ios/swift/HybridCameraDeviceFactorySpec.swift @@ -12,6 +12,7 @@ public protocol HybridCameraDeviceFactorySpec_protocol: HybridObject { // Properties var cameraDevices: [(any HybridCameraDeviceSpec)] { get } var userPreferredCamera: (any HybridCameraDeviceSpec)? { get set } + var supportedMultiCamDeviceCombinations: [[(any HybridCameraDeviceSpec)]] { get } // Methods func addOnCameraDevicesChangedListener(listener: @escaping (_ newDevices: [(any HybridCameraDeviceSpec)]) -> Void) throws -> ListenerSubscription diff --git a/packages/react-native-vision-camera/nitrogen/generated/ios/swift/HybridCameraDeviceFactorySpec_cxx.swift b/packages/react-native-vision-camera/nitrogen/generated/ios/swift/HybridCameraDeviceFactorySpec_cxx.swift index 658132f915..ae6fb283ad 100644 --- a/packages/react-native-vision-camera/nitrogen/generated/ios/swift/HybridCameraDeviceFactorySpec_cxx.swift +++ b/packages/react-native-vision-camera/nitrogen/generated/ios/swift/HybridCameraDeviceFactorySpec_cxx.swift @@ -167,6 +167,28 @@ open class HybridCameraDeviceFactorySpec_cxx { }() } } + + public final var supportedMultiCamDeviceCombinations: bridge.std__vector_std__vector_std__shared_ptr_HybridCameraDeviceSpec___ { + @inline(__always) + get { + return { () -> bridge.std__vector_std__vector_std__shared_ptr_HybridCameraDeviceSpec___ in + var __vector = bridge.create_std__vector_std__vector_std__shared_ptr_HybridCameraDeviceSpec___(self.__implementation.supportedMultiCamDeviceCombinations.count) + for __item in self.__implementation.supportedMultiCamDeviceCombinations { + __vector.push_back({ () -> bridge.std__vector_std__shared_ptr_HybridCameraDeviceSpec__ in + var __vector = bridge.create_std__vector_std__shared_ptr_HybridCameraDeviceSpec__(__item.count) + for __item in __item { + __vector.push_back({ () -> bridge.std__shared_ptr_HybridCameraDeviceSpec_ in + let __cxxWrapped = __item.getCxxWrapper() + return __cxxWrapped.getCxxPart() + }()) + } + return __vector + }()) + } + return __vector + }() + } + } // Methods @inline(__always) diff --git a/packages/react-native-vision-camera/nitrogen/generated/shared/c++/HybridCameraDeviceFactorySpec.cpp b/packages/react-native-vision-camera/nitrogen/generated/shared/c++/HybridCameraDeviceFactorySpec.cpp index d50c1b5a4f..f0435f7356 100644 --- a/packages/react-native-vision-camera/nitrogen/generated/shared/c++/HybridCameraDeviceFactorySpec.cpp +++ b/packages/react-native-vision-camera/nitrogen/generated/shared/c++/HybridCameraDeviceFactorySpec.cpp @@ -17,6 +17,7 @@ namespace margelo::nitro::camera { prototype.registerHybridGetter("cameraDevices", &HybridCameraDeviceFactorySpec::getCameraDevices); prototype.registerHybridGetter("userPreferredCamera", &HybridCameraDeviceFactorySpec::getUserPreferredCamera); prototype.registerHybridSetter("userPreferredCamera", &HybridCameraDeviceFactorySpec::setUserPreferredCamera); + prototype.registerHybridGetter("supportedMultiCamDeviceCombinations", &HybridCameraDeviceFactorySpec::getSupportedMultiCamDeviceCombinations); prototype.registerHybridMethod("addOnCameraDevicesChangedListener", &HybridCameraDeviceFactorySpec::addOnCameraDevicesChangedListener); prototype.registerHybridMethod("getCameraForId", &HybridCameraDeviceFactorySpec::getCameraForId); prototype.registerHybridMethod("getSupportedExtensions", &HybridCameraDeviceFactorySpec::getSupportedExtensions); diff --git a/packages/react-native-vision-camera/nitrogen/generated/shared/c++/HybridCameraDeviceFactorySpec.hpp b/packages/react-native-vision-camera/nitrogen/generated/shared/c++/HybridCameraDeviceFactorySpec.hpp index 823a998870..cde15be396 100644 --- a/packages/react-native-vision-camera/nitrogen/generated/shared/c++/HybridCameraDeviceFactorySpec.hpp +++ b/packages/react-native-vision-camera/nitrogen/generated/shared/c++/HybridCameraDeviceFactorySpec.hpp @@ -63,6 +63,7 @@ namespace margelo::nitro::camera { virtual std::vector> getCameraDevices() = 0; virtual std::optional> getUserPreferredCamera() = 0; virtual void setUserPreferredCamera(const std::optional>& userPreferredCamera) = 0; + virtual std::vector>> getSupportedMultiCamDeviceCombinations() = 0; public: // Methods From dd44ade26d5517aad845533938b56212f8e9f958 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Tue, 28 Apr 2026 16:09:31 +0200 Subject: [PATCH 4/9] docs: Add docs for multi cam constraints --- docs/content/docs/multi-camera.mdx | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/docs/content/docs/multi-camera.mdx b/docs/content/docs/multi-camera.mdx index 891037c7ac..26471888f1 100644 --- a/docs/content/docs/multi-camera.mdx +++ b/docs/content/docs/multi-camera.mdx @@ -19,14 +19,27 @@ if (VisionCamera.supportsMultiCamSessions) { ### Using multiple Connections -With a multi-cam [`CameraSession`](/api/react-native-vision-camera/hybrid-objects/CameraSession), you can now attach multiple [`CameraSessionConnection`](/api/react-native-vision-camera/interfaces/CameraSessionConnection)s - for example to stream and capture from the Front- and Back-Camera at the same time, attach both devices: +With a multi-cam [`CameraSession`](/api/react-native-vision-camera/hybrid-objects/CameraSession), you can now attach multiple [`CameraSessionConnection`](/api/react-native-vision-camera/interfaces/CameraSessionConnection)s - for example to stream and capture from the Front- and Back-Camera at the same time. +Due to hardware constraints, not every [`CameraDevice`](/api/react-native-vision-camera/hybrid-objects/CameraDevice) can be used with every other [`CameraDevice`](/api/react-native-vision-camera/hybrid-objects/CameraDevice) - therefore VisionCamera exposes a fixed array of supported combinations via [`CameraDeviceFactory.supportedMultiCamDeviceCombinations`](/api/react-native-vision-camera/hybrid-objects/CameraDeviceFactory#supportedmulticamdevicecombinations) upfront: ```ts -const session = ... -const frontDevice = useCameraDevice('front') -const backDevice = useCameraDevice('back') -const frontPreview = usePreviewOutput() -const backPreview = usePreviewOutput() +if (!VisionCamera.supportsMultiCamSessions) + return + +const deviceFactory = await VisionCamera.createDeviceFactory() +const frontAndBackCombination = deviceFactory.supportedMultiCamDeviceCombinations.find((devices) => { + return devices.some((d) => d.position === 'front') && devices.some((d) => d.position === 'back') +}) +if (frontAndBackCombination == null) + return +const frontDevice = frontAndBackCombination.find((d) => d.position === 'front') +const backDevice = frontAndBackCombination.find((d) => d.position === 'back') + +const session = await VisionCamera.createCameraSession(true) +const frontPreviewOutput = VisionCamera.createPreviewOutput() +const frontPhotoOutput = VisionCamera.createPhotoOutput({}) +const backPreviewOutput = VisionCamera.createPreviewOutput() +const backPhotoOutput = VisionCamera.createPhotoOutput({}) const [frontController, backController] = await session.configure([ // Front Camera @@ -51,6 +64,6 @@ const [frontController, backController] = await session.configure([ await session.start() ``` -Then, ensure you display both `frontPreview` and `backPreview` in separate [``](/api/react-native-vision-camera/views/NativePreviewView) views. +Then, ensure you display both `frontPreviewOutput` and `backPreviewOutput` in separate [``](/api/react-native-vision-camera/views/NativePreviewView) views. Each returned [`CameraController`](/api/react-native-vision-camera/hybrid-objects/CameraController) correlates to the connection at that index. From 4c190db714d5ded7e238d6950f9189aadc2f0363 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Tue, 28 Apr 2026 16:32:37 +0200 Subject: [PATCH 5/9] More docs --- docs/content/docs/multi-camera.mdx | 44 ++++++++++++++++++++++++------ 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/docs/content/docs/multi-camera.mdx b/docs/content/docs/multi-camera.mdx index 26471888f1..e0b2abf273 100644 --- a/docs/content/docs/multi-camera.mdx +++ b/docs/content/docs/multi-camera.mdx @@ -5,7 +5,7 @@ description: Using multiple Camera Devices in a single Camera Session import { Tab, Tabs } from 'fumadocs-ui/components/tabs' -A [`CameraSession`](/api/react-native-vision-camera/hybrid-objects/CameraSession) allows attaching multiple connections to stream from multiple [`CameraDevice`](/api/react-native-vision-camera/hybrid-objects/CameraDevice)s at the same time - if the system supports it. +A [`CameraSession`](/api/react-native-vision-camera/hybrid-objects/CameraSession) allows attaching multiple connections to stream from multiple [`CameraDevice`](/api/react-native-vision-camera/hybrid-objects/CameraDevice)s at the same time (e.g. Picture-in-Picture mode via front + back Camera) - if the system supports it. ### Creating a Multi-Camera Session @@ -19,22 +19,30 @@ if (VisionCamera.supportsMultiCamSessions) { ### Using multiple Connections -With a multi-cam [`CameraSession`](/api/react-native-vision-camera/hybrid-objects/CameraSession), you can now attach multiple [`CameraSessionConnection`](/api/react-native-vision-camera/interfaces/CameraSessionConnection)s - for example to stream and capture from the Front- and Back-Camera at the same time. -Due to hardware constraints, not every [`CameraDevice`](/api/react-native-vision-camera/hybrid-objects/CameraDevice) can be used with every other [`CameraDevice`](/api/react-native-vision-camera/hybrid-objects/CameraDevice) - therefore VisionCamera exposes a fixed array of supported combinations via [`CameraDeviceFactory.supportedMultiCamDeviceCombinations`](/api/react-native-vision-camera/hybrid-objects/CameraDeviceFactory#supportedmulticamdevicecombinations) upfront: +Due to hardware constraints, not every [`CameraDevice`](/api/react-native-vision-camera/hybrid-objects/CameraDevice) can be paired with every other [`CameraDevice`](/api/react-native-vision-camera/hybrid-objects/CameraDevice) - therefore VisionCamera exposes a fixed array of supported combinations via [`CameraDeviceFactory.supportedMultiCamDeviceCombinations`](/api/react-native-vision-camera/hybrid-objects/CameraDeviceFactory#supportedmulticamdevicecombinations) upfront: ```ts if (!VisionCamera.supportsMultiCamSessions) return const deviceFactory = await VisionCamera.createDeviceFactory() -const frontAndBackCombination = deviceFactory.supportedMultiCamDeviceCombinations.find((devices) => { - return devices.some((d) => d.position === 'front') && devices.some((d) => d.position === 'back') -}) +const frontAndBackCombination = + deviceFactory.supportedMultiCamDeviceCombinations.find((devices) => { + return ( + devices.some((d) => d.position === 'front') && + devices.some((d) => d.position === 'back') + ) + }) if (frontAndBackCombination == null) return + const frontDevice = frontAndBackCombination.find((d) => d.position === 'front') const backDevice = frontAndBackCombination.find((d) => d.position === 'back') +``` +Then, knowing `frontDevice` and `backDevice` can be used simultaneously in a Multi-Cam session, create the [`CameraSession`](/api/react-native-vision-camera/hybrid-objects/CameraSession), and attach the [`CameraSessionConnection`](/api/react-native-vision-camera/interfaces/CameraSessionConnection)s: + +```ts const session = await VisionCamera.createCameraSession(true) const frontPreviewOutput = VisionCamera.createPreviewOutput() const frontPhotoOutput = VisionCamera.createPhotoOutput({}) @@ -64,6 +72,26 @@ const [frontController, backController] = await session.configure([ await session.start() ``` -Then, ensure you display both `frontPreviewOutput` and `backPreviewOutput` in separate [``](/api/react-native-vision-camera/views/NativePreviewView) views. +Each returned [`CameraController`](/api/react-native-vision-camera/hybrid-objects/CameraController) correlates to the connection at that index - e.g. `frontController` allows zooming/exposure/focus the `frontDevice`, and vice-versa. -Each returned [`CameraController`](/api/react-native-vision-camera/hybrid-objects/CameraController) correlates to the connection at that index. +Then, ensure you display both `frontPreviewOutput` and `backPreviewOutput` in separate [``](/api/react-native-vision-camera/views/NativePreviewView) views: + +```tsx +function App() { + const frontPreviewOutput = ... + const backPreviewOutput = ... + + return ( + + + + + ) +} +``` From 17f63caf4d3602f06493583b39fc7375d9ccbc86 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Tue, 28 Apr 2026 16:34:00 +0200 Subject: [PATCH 6/9] fix android build --- .../com/margelo/nitro/camera/HybridCameraDeviceFactory.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 13aeb3e99a..5e700b83ac 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 @@ -40,8 +40,8 @@ class HybridCameraDeviceFactory( override val supportedMultiCamDeviceCombinations: Array> get() { - return cameraProvider.concurrentCameraIds.mapToArray { devices -> - return@mapToArray { HybridCameraDevice(it) } + return cameraProvider.availableConcurrentCameraInfos.mapToArray { devices -> + return@mapToArray devices.mapToArray { HybridCameraDevice(it) } } } From 4d95749f710a1b3ae694ae8ab21e8a77da3deef2 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Tue, 28 Apr 2026 16:40:44 +0200 Subject: [PATCH 7/9] Update Track.swift --- .../ios/Hybrid Objects/Recording/Track.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-native-vision-camera/ios/Hybrid Objects/Recording/Track.swift b/packages/react-native-vision-camera/ios/Hybrid Objects/Recording/Track.swift index 80ebef71a4..3c9302ae31 100644 --- a/packages/react-native-vision-camera/ios/Hybrid Objects/Recording/Track.swift +++ b/packages/react-native-vision-camera/ios/Hybrid Objects/Recording/Track.swift @@ -25,7 +25,7 @@ final class Track { /** Gets whether the track has been marked as finished or not. - + A track is finished when stop() was called, and a buffer with a later timestamp than the one from the stop() call arrives. */ From f0ae592560271e9797f4114a44ebfca72be453e2 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Tue, 28 Apr 2026 16:41:53 +0200 Subject: [PATCH 8/9] test: Add Harness tests for `supportedMultiCamDeviceCombinations` --- .../__tests__/visioncamera.devices.harness.ts | 50 +++++++++++++++++ .../__tests__/visioncamera.session.harness.ts | 55 +++++++++++++++++++ 2 files changed, 105 insertions(+) diff --git a/apps/simple-camera/__tests__/visioncamera.devices.harness.ts b/apps/simple-camera/__tests__/visioncamera.devices.harness.ts index a88366a12e..414019cc6e 100644 --- a/apps/simple-camera/__tests__/visioncamera.devices.harness.ts +++ b/apps/simple-camera/__tests__/visioncamera.devices.harness.ts @@ -151,4 +151,54 @@ describe('VisionCamera - Devices', () => { expect(factory.userPreferredCamera?.id).toBe(back.id) factory.userPreferredCamera = previous }) + + it('returns an empty supportedMultiCamDeviceCombinations array when the platform does not support multi-cam', () => { + if (VisionCamera.supportsMultiCamSessions) { + console.log( + '[SKIP] supportedMultiCamDeviceCombinations empty: platform supports multi-cam', + ) + return + } + expect(factory.supportedMultiCamDeviceCombinations.length).toBe(0) + }) + + it('returns at least one supportedMultiCamDeviceCombinations combination when the platform supports multi-cam', () => { + if (!VisionCamera.supportsMultiCamSessions) { + console.log( + '[SKIP] supportedMultiCamDeviceCombinations non-empty: multi-cam not supported on this platform', + ) + return + } + expect(factory.supportedMultiCamDeviceCombinations.length).toBeGreaterThanOrEqual(1) + }) + + it('every device in a supportedMultiCamDeviceCombinations combination is also present in cameraDevices', () => { + const combinations = factory.supportedMultiCamDeviceCombinations + if (combinations.length === 0) { + console.log( + '[SKIP] supportedMultiCamDeviceCombinations device lookup: no combinations on this platform', + ) + return + } + const knownIds = new Set(factory.cameraDevices.map((d) => d.id)) + for (const combination of combinations) { + expect(combination.length).toBeGreaterThan(0) + for (const device of combination) { + expect(knownIds.has(device.id)).toBe(true) + } + } + }) + + it('logs every supported multi-cam device combination', () => { + const combinations = factory.supportedMultiCamDeviceCombinations + console.log( + `supportedMultiCamDeviceCombinations: ${combinations.length} combinations`, + ) + for (const [index, combination] of combinations.entries()) { + const description = combination + .map((d) => `${d.position}:${d.id}`) + .join(', ') + console.log(` [${index}] ${description}`) + } + }) }) diff --git a/apps/simple-camera/__tests__/visioncamera.session.harness.ts b/apps/simple-camera/__tests__/visioncamera.session.harness.ts index 919202fc06..e08f296118 100644 --- a/apps/simple-camera/__tests__/visioncamera.session.harness.ts +++ b/apps/simple-camera/__tests__/visioncamera.session.harness.ts @@ -254,4 +254,59 @@ describe('VisionCamera - Session', () => { await session.stop() sub.remove() }) + + it('configures, starts and stops a multi-cam session for every supported device combination', async () => { + if (!VisionCamera.supportsMultiCamSessions) { + console.log( + '[SKIP] multi-cam combinations: not supported on this platform', + ) + return + } + const combinations = factory.supportedMultiCamDeviceCombinations + if (combinations.length === 0) { + console.log( + '[SKIP] multi-cam combinations: no combinations reported on this device', + ) + return + } + + for (const combination of combinations) { + const session = await VisionCamera.createCameraSession(true) + const connections = combination.map((device) => ({ + input: device, + outputs: [ + { + output: VisionCamera.createPhotoOutput({ + targetResolution: CommonResolutions.HD_4_3, + containerFormat: 'jpeg' as const, + quality: 0.8, + qualityPrioritization: 'balanced' as const, + }), + mirrorMode: 'auto' as const, + }, + ], + constraints: [], + })) + + const controllers = await session.configure(connections) + expect(controllers.length).toBe(combination.length) + for (let i = 0; i < combination.length; i++) { + expect(controllers[i]?.device.id).toBe(combination[i]?.id) + } + + let started = false + const sub = session.addOnStartedListener(() => { + started = true + }) + await session.start() + await waitUntil(() => started, { timeout: 15_000 }) + await session.stop() + sub.remove() + + const description = combination + .map((d) => `${d.position}:${d.id}`) + .join(', ') + console.log(`multi-cam session ok: [${description}]`) + } + }) }) From 2e83e8c12ac0cb886e96cbd14809fd889ef77d08 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Tue, 28 Apr 2026 17:10:09 +0200 Subject: [PATCH 9/9] fix build error temp --- .../generated/android/c++/JHybridCameraDeviceFactorySpec.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react-native-vision-camera/nitrogen/generated/android/c++/JHybridCameraDeviceFactorySpec.cpp b/packages/react-native-vision-camera/nitrogen/generated/android/c++/JHybridCameraDeviceFactorySpec.cpp index f0256d7dd8..8476b942c7 100644 --- a/packages/react-native-vision-camera/nitrogen/generated/android/c++/JHybridCameraDeviceFactorySpec.cpp +++ b/packages/react-native-vision-camera/nitrogen/generated/android/c++/JHybridCameraDeviceFactorySpec.cpp @@ -102,8 +102,8 @@ namespace margelo::nitro::camera { std::vector> __vector; __vector.reserve(__size); for (size_t __i = 0; __i < __size; __i++) { - auto __element = __element->getElement(__i); - __vector.push_back(__element->getJHybridCameraDeviceSpec()); + auto __element2 = __element->getElement(__i); + __vector.push_back(__element2->getJHybridCameraDeviceSpec()); } return __vector; }());