Skip to content
Open
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
5 changes: 4 additions & 1 deletion apps/simple-camera/__tests__/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@ Tests are split by domain. Each file tests one slice of the imperative `VisionCa
| [visioncamera.frame.harness.ts](visioncamera.frame.harness.ts) | `createFrameOutput`, worklet install via `react-native-vision-camera-worklets`, YUV / RGB / native pixel formats, `scheduleOnRN`, `createSynchronizable`, `setOnFrameDroppedCallback`, `enablePreviewSizedOutputBuffers` |
| [visioncamera.multi-output.harness.ts](visioncamera.multi-output.harness.ts) | Multi-output sessions that combine photo, video, and frame outputs, output replacement while other outputs stay attached, persistent recording across session restarts |
| [visioncamera.constraints.harness.ts](visioncamera.constraints.harness.ts) | `VisionCamera.resolveConstraints` + `onSessionConfigSelected`, FPS / HDR / stabilization / binned / pixelFormat / resolutionBias constraints |
| [visioncamera.controller.harness.ts](visioncamera.controller.harness.ts) | `CameraController` — zoom, torch, exposure bias, focus metering, low-light boost, subject area listener |
| [visioncamera.controller.zoom.harness.ts](visioncamera.controller.zoom.harness.ts) | `CameraController` zoom, zoom animation, initial zoom |
| [visioncamera.controller.torch.harness.ts](visioncamera.controller.torch.harness.ts) | `CameraController` torch mode and torch strength |
| [visioncamera.controller.exposure.harness.ts](visioncamera.controller.exposure.harness.ts) | `CameraController` exposure bias and exposure locking |
| [visioncamera.controller.focus.harness.ts](visioncamera.controller.focus.harness.ts) | `CameraController` focus metering, low-light boost, subject area listener, focus / white-balance locking, controller state |
| [visioncamera.coordinates.harness.ts](visioncamera.coordinates.harness.ts) | `Frame.convertFramePointToCameraPoint` / `convertCameraPointToFramePoint`, `PreviewView.convertViewPointToCameraPoint` / `convertCameraPointToViewPoint`, `PreviewView.createMeteringPoint`, `convertScannedObjectCoordinatesToViewCoordinates`, end-to-end Frame → Camera → View round-trip |
| [visioncamera.nativepreviewview.harness.tsx](visioncamera.nativepreviewview.harness.tsx) | Bare `NativePreviewView` lifecycle, layout-sensitive preview regression coverage, `resizeMode`, Android `implementationMode`, gesture controllers, multi-preview mounting, `PreviewView` ref methods, Android `takeSnapshot()` dimensions |
| [visioncamera.camera-view.harness.tsx](visioncamera.camera-view.harness.tsx) | High-level `<Camera>` preview lifecycle, photo output integration, controller props, native gestures, `CameraRef` methods, `isActive`, mount / unmount / replacement behavior |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import { beforeAll, describe, expect, it } from 'react-native-harness'
import type { CameraDevice } from 'react-native-vision-camera'
import { CommonResolutions, VisionCamera } from 'react-native-vision-camera'

describe('VisionCamera - Controller - Exposure', () => {
let backDevice: CameraDevice

beforeAll(async () => {
await VisionCamera.requestCameraPermission()
expect(VisionCamera.cameraPermissionStatus).toBe('authorized')
const factory = await VisionCamera.createDeviceFactory()
const back = factory.getDefaultCamera('back')
expect(back).toBeDefined()
if (back == null) throw new Error('no back camera')
backDevice = back
})

it('sets exposure bias to min/max when the device supports it', async (context) => {
if (!backDevice.supportsExposureBias) {
return context.skip('exposureBias: not supported on this device')
}
const session = await VisionCamera.createCameraSession(false)
const photoOutput = VisionCamera.createPhotoOutput({
targetResolution: CommonResolutions.HD_4_3,
containerFormat: 'jpeg',
quality: 0.8,
qualityPrioritization: 'balanced',
})
const [controller] = await session.configure([
{
input: backDevice,
outputs: [{ output: photoOutput, mirrorMode: 'auto' }],
constraints: [],
},
])
if (controller == null) throw new Error('no controller')
await session.start()

try {
// AVCaptureDevice quantizes the requested bias to a discrete rational
// step, so the readback may differ from the request by a tiny epsilon.
await controller.setExposureBias(backDevice.maxExposureBias)
expect(controller.exposureBias).toBeCloseTo(backDevice.maxExposureBias, 4)

await controller.setExposureBias(backDevice.minExposureBias)
expect(controller.exposureBias).toBeCloseTo(backDevice.minExposureBias, 4)

await controller.setExposureBias(0)
expect(controller.exposureBias).toBeCloseTo(0, 4)
} finally {
await session.stop()
}
})

it('rejects setExposureBias outside the device range', async (context) => {
if (!backDevice.supportsExposureBias) {
return context.skip(
'exposureBias out-of-range: not supported on this device',
)
}
const session = await VisionCamera.createCameraSession(false)
const photoOutput = VisionCamera.createPhotoOutput({
targetResolution: CommonResolutions.HD_4_3,
containerFormat: 'jpeg',
quality: 0.8,
qualityPrioritization: 'balanced',
})
const [controller] = await session.configure([
{
input: backDevice,
outputs: [{ output: photoOutput, mirrorMode: 'auto' }],
constraints: [],
},
])
if (controller == null) throw new Error('no controller')
await session.start()

try {
const tooHighExposureBias = backDevice.maxExposureBias + 1
const tooLowExposureBias = backDevice.minExposureBias - 1
await expect(
controller.setExposureBias(tooHighExposureBias),
).rejects.toThrow()
await expect(
controller.setExposureBias(tooLowExposureBias),
).rejects.toThrow()
} finally {
await session.stop()
}
})

// TODO(Android): Re-enable once initial CameraX control config is applied after the camera is active.
it.skip('honors initialExposureBias passed to configure', async (context) => {
if (!backDevice.supportsExposureBias) {
return context.skip(
'initialExposureBias: device does not support exposure bias',
)
}
const session = await VisionCamera.createCameraSession(false)
const photoOutput = VisionCamera.createPhotoOutput({
targetResolution: CommonResolutions.HD_4_3,
containerFormat: 'jpeg',
quality: 0.8,
qualityPrioritization: 'balanced',
})
const initial = backDevice.maxExposureBias
const [controller] = await session.configure([
{
input: backDevice,
outputs: [{ output: photoOutput, mirrorMode: 'auto' }],
constraints: [],
initialExposureBias: initial,
},
])
if (controller == null) throw new Error('no controller')
await session.start()

try {
expect(controller.exposureBias).toBeCloseTo(initial, 4)
} finally {
await session.stop()
}
})

it('locks exposure to a specific duration/ISO when the device supports it', async (context) => {
if (!backDevice.supportsExposureLocking) {
return context.skip('exposure locking: not supported on this device')
}
const session = await VisionCamera.createCameraSession(false)
const photoOutput = VisionCamera.createPhotoOutput({
targetResolution: CommonResolutions.HD_4_3,
containerFormat: 'jpeg',
quality: 0.8,
qualityPrioritization: 'balanced',
})
const [controller] = await session.configure([
{
input: backDevice,
outputs: [{ output: photoOutput, mirrorMode: 'auto' }],
constraints: [],
},
])
if (controller == null) throw new Error('no controller')
await session.start()

try {
// Pick a mid-range duration/ISO the device must accept. Hardware
// quantizes both, so only assert mode here.
const duration =
(controller.minExposureDuration + controller.maxExposureDuration) / 2
const iso = (controller.minISO + controller.maxISO) / 2

// iOS reports 'custom' for manual duration/ISO; Android may report
// 'locked'. Accept either so the test stays stable across platforms.
await controller.setExposureLocked(duration, iso)
expect(controller.exposureMode).toBeOneOf(['custom', 'locked'])

await controller.lockCurrentExposure()
expect(controller.exposureMode).toBeOneOf(['custom', 'locked'])
} finally {
await session.stop()
}
})
})
Loading
Loading