Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions apps/simple-camera/__tests__/visioncamera.devices.harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`)
}
})
})
55 changes: 55 additions & 0 deletions apps/simple-camera/__tests__/visioncamera.session.harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}]`)
}
})
})
59 changes: 50 additions & 9 deletions docs/content/docs/multi-camera.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -19,14 +19,35 @@ 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:
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
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')
```

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({})
const backPreviewOutput = VisionCamera.createPreviewOutput()
const backPhotoOutput = VisionCamera.createPhotoOutput({})

const [frontController, backController] = await session.configure([
// Front Camera
Expand All @@ -51,6 +72,26 @@ const [frontController, backController] = await session.configure([
await session.start()
```

Then, ensure you display both `frontPreview` and `backPreview` in separate [`<NativePreviewView />`](/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.

Then, ensure you display both `frontPreviewOutput` and `backPreviewOutput` in separate [`<NativePreviewView />`](/api/react-native-vision-camera/views/NativePreviewView) views:

```tsx
function App() {
const frontPreviewOutput = ...
const backPreviewOutput = ...

Each returned [`CameraController`](/api/react-native-vision-camera/hybrid-objects/CameraController) correlates to the connection at that index.
return (
<View style={StyleSheet.absoluteFill}>
<NativePreviewView
style={{ flex: 1 }}
previewOutput={frontPreviewOutput}
/>
<NativePreviewView
style={{ flex: 1 }}
previewOutput={backPreviewOutput}
/>
</View>
)
}
```
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@ class HybridCameraDeviceFactory(
override val cameraDevices: Array<HybridCameraDeviceSpec>
get() = cameraProvider.availableCameraInfos.mapToArray { HybridCameraDevice(it) }

override val supportedMultiCamDeviceCombinations: Array<Array<HybridCameraDeviceSpec>>
get() {
return cameraProvider.availableConcurrentCameraInfos.mapToArray { devices ->
return@mapToArray devices.mapToArray { HybridCameraDevice(it) }
}
}

override var userPreferredCamera: HybridCameraDeviceSpec?
get() {
val preferredCameraId =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand 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 {
Expand All @@ -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
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading