references/advanced-features.md
# Advanced features
Depth, multi-cam, Skia previews, the GPU Resizer, barcode scanning (separate package), GPS/location metadata, and writing a custom native output.
## Depth frames (LiDAR / ToF / disparity)
```tsx
import { Camera, useDepthOutput, useCameraDevice } from 'react-native-vision-camera'
const device = useCameraDevice('back')
const depthOutput = useDepthOutput({
// useDepthOutput options: targetResolution, enableFiltering, dropFramesWhileBusy, allowDeferredStart, onDepth, onDepthFrameDropped.
// There is NO pixelFormat option — read the resolved format from `depth.pixelFormat` (a DepthPixelFormat):
// 'depth-16-bit' | 'depth-32-bit' | 'depth-point-cloud-32-bit' | 'disparity-16-bit' | 'disparity-32-bit' | 'unknown'
// source: useDepthOutput.ts:70-77 (options); DepthPixelFormat.ts:18-24
onDepth(depth) {
'worklet'
try {
// depth.width, depth.height, depth.pixelFormat, depth.orientation, depth.timestamp
// depth.depthDataAccuracy, depth.depthDataQuality, depth.isDepthDataFiltered
// depth.isMirrored, depth.isValid, depth.bytesPerRow, depth.cameraCalibrationData (iOS)
// depth.getDepthData(), depth.getNativeBuffer()
// depth.convert('depth-32-bit') / depth.convertAsync(...)
} finally {
depth.dispose() // REQUIRED — same buffer-pool rule as Frame
}
},
})
<Camera device={device} isActive={true} outputs={[depthOutput]} />
```
- Requires `react-native-vision-camera-worklets` + `react-native-worklets`.
- Not every device exposes depth. There is **no** `supportsDepthCapture` flag — gate on `device.mediaTypes.includes('depth')`; depth-capable virtual cameras report `['video', 'depth']`. <!-- source: CameraDevice.nitro.ts:386-388 (`readonly mediaTypes: MediaType[]`); no supportsDepthCapture exists -->.
- Two sources:
- LiDAR / ToF / Infrared → true depth frames (`depth-16-bit`, `depth-32-bit`, `depth-point-cloud-32-bit`).
- Dual or triple virtual cameras → disparity frames synthesized from stereo (`disparity-16-bit`, `disparity-32-bit`).
- `depth.convert(target)` / `depth.convertAsync(target)` return a **new** derivative `Depth` in the target format (must be one of `depth.availableDepthPixelFormats`); the original is untouched. <!-- source: Depth.nitro.ts:194,201,133 -->
- Native plugins: same Nitro pattern as Frame, but use the `Depth` spec type and cast to `NativeDepth` (iOS: `AVDepthData`).
## Multi-camera sessions
Front + back simultaneously (or any combination the device supports). iOS 13+ and supported Android devices only.
```ts
// Probe first — multi-cam is a platform-level capability, not a CameraDevice flag:
if (!VisionCamera.supportsMultiCamSessions) return
// then pick a valid input pair from `deviceFactory.supportedMultiCamDeviceCombinations`
// source: CameraFactory.nitro.ts:71; CameraSession.nitro.ts:70-74,182-186
// Imperative only — no declarative shorthand for multi-cam.
const session = await VisionCamera.createCameraSession(/* isMultiCam */ true)
const backDevice = await getDefaultCameraDevice('back')
const frontDevice = await getDefaultCameraDevice('front')
const backPreview = VisionCamera.createPreviewOutput() // takes no args — source: CameraFactory.nitro.ts:186
const frontPreview = VisionCamera.createPreviewOutput()
const backVideo = VisionCamera.createVideoOutput({ enableAudio: true })
const [backController, frontController] = await session.configure([
{
input: backDevice,
outputs: [
{ output: backPreview, mirrorMode: 'auto' },
{ output: backVideo, mirrorMode: 'auto' },
],
constraints: [{ fps: 30 }],
},
{
input: frontDevice,
outputs: [{ output: frontPreview, mirrorMode: 'auto' }],
constraints: [],
},
], {})
await session.start()
```
Render each preview with `<NativePreviewView />`, bound to its own `CameraPreviewOutput`. For picture-in-picture UX, render the front preview as a smaller absolutely-positioned view on top.
## Skia previews — shaders and live effects
```tsx
import { SkiaCamera } from 'react-native-vision-camera-skia'
<SkiaCamera
device={device}
isActive={true}
pixelFormat="yuv"
onFrame={(frame, render) => {
'worklet'
render(({ frameTexture, canvas }) => {
canvas.drawImage(frameTexture, 0, 0)
// draw overlays, apply a Skia ImageFilter, etc.
})
frame.dispose()
}}
/>
```
- `<SkiaCamera />` replaces `<Camera />`'s preview with a Skia canvas and **always** attaches a frame output (you can't opt out).
- Pixel formats: `'native'` / `'yuv'` / `'rgb'`. The SkiaCamera default is `'yuv'` on iOS and `'native'` on Android. With `'native'`, verify Skia compatibility (RAW or `'private'` may not be supported). <!-- source: SkiaCamera.tsx:200-203 (DEFAULT_PIXEL_FORMAT Platform.select), :170 -->
- `focusTo` works with SkiaCamera in v5.
- For manual rendering without the wrapper: use `NativeBuffer` + Skia's `MakeImageFromNativeBuffer()`.
- Peer deps: `@shopify/react-native-skia`.
## GPU Resizer — the ML fast-path
`react-native-vision-camera-resizer` is Margelo's GPU-accelerated replacement for the CPU-based `vision-camera-resize-plugin`. It runs on Metal (iOS) and Vulkan + `AHardwareBuffer` (newer Android versions), and returns a pooled `GPUFrame`. <!-- source: ResizerFactory.nitro.ts:83-87 (Metal/Vulkan, isAvailable()); GPUFrame.nitro.ts. The often-quoted "~5×" multiplier is a blog claim, not in source. -->
```ts
import { useResizer } from 'react-native-vision-camera-resizer'
// useResizer returns { state: 'loading' | 'ready' | 'error', resizer, error }.
// There is NO isResizerAvailable() export — gate on `state` / `resizer != null`, and inspect `error`.
const { resizer } = useResizer({
width: 128,
height: 128,
channelOrder: 'rgb', // 'rgb' | 'bgr'
dataType: 'float32', // 'int8' | 'uint8' | 'float16' | 'float32'
scaleMode: 'cover', // 'cover' | 'contain'
pixelLayout: 'planar', // 'planar' = NCHW ([1,3,H,W]); 'interleaved' = NHWC ([1,H,W,3])
})
const frameOutput = useFrameOutput({
pixelFormat: 'yuv', // resizer is happiest with YUV input
onFrame(frame) {
'worklet'
if (resizer == null) { frame.dispose(); return } // still loading or errored
const resized = resizer.resize(frame)
const pixels = resized.getPixelBuffer() // native buffer ready for ONNX/TFLite
try { /* model.run(pixels) */ }
finally {
resized.dispose()
frame.dispose()
}
},
})
```
<!-- source: react-native-vision-camera-resizer/src/index.ts (no isResizerAvailable export); useResizer.ts:13-16,55-94 (ResizerState + guarded example); OutputFormat.ts (ChannelOrder='rgb'|'bgr', DataType, PixelLayout); ResizerFactory.nitro.ts:16,50 (ScaleMode) -->
Gate on the hook's `state` / `resizer` (there is no `isResizerAvailable()`; the underlying capability check is `ResizerFactory.isAvailable()`). Provide a CPU fallback when `state === 'error'`. Dispose the returned `GPUFrame` like a regular Frame — it's a pooled GPU resource. <!-- source: useResizer.ts:13-16; ResizerFactory.nitro.ts:87 (isAvailable); GPUFrame.nitro.ts -->
## Barcode scanner — `react-native-vision-camera-barcode-scanner`
MLKit on both platforms, so format behavior matches across iOS and Android. The classic v4 `'ean-13' ↔ UPC-A` iOS quirk is gone.
### Easiest — drop-in view
```tsx
import { CodeScanner } from 'react-native-vision-camera-barcode-scanner'
<CodeScanner
style={{ flex: 1 }} // required — CodeScannerOptions.style is not optional. source: CodeScanner.tsx:20
isActive
barcodeFormats={['qr-code', 'ean-13']}
onBarcodeScanned={(barcodes) => console.log(barcodes[0]?.rawValue)}
onError={(e) => console.error(e)}
/>
```
### Integrated — Camera output
```tsx
import { useBarcodeScannerOutput } from 'react-native-vision-camera-barcode-scanner'
const barcodeOutput = useBarcodeScannerOutput({
barcodeFormats: ['qr-code'],
onBarcodeScanned: (barcodes) => {},
})
<Camera outputs={[photoOutput, barcodeOutput]} /* ... */ />
```
### Frame-processor — full control
```tsx
import { useBarcodeScanner } from 'react-native-vision-camera-barcode-scanner'
const scanner = useBarcodeScanner({ barcodeFormats: ['qr-code'] })
const frameOutput = useFrameOutput({
onFrame(frame) {
'worklet'
try {
const codes = scanner.scanCodes(frame)
if (codes.length) found.value = codes
} finally { frame.dispose() }
},
})
```
Performance rule: list only the formats you need.
## iOS-only native object output (no ML dep)
If all you need is QR + face + body detection on iOS, skip MLKit and use the native `AVCaptureMetadataOutput` path:
```tsx
import { useObjectOutput, isScannedCode, isScannedFace } from 'react-native-vision-camera'
const objectOutput = useObjectOutput({
types: ['qr', 'face', 'human-body'],
onObjectsScanned: (objects) => {
for (const o of objects) {
if (isScannedCode(o)) console.log('code:', o.value)
else if (isScannedFace(o)) console.log('face:', o.faceID)
}
},
})
<Camera outputs={[objectOutput]} />
```
Android has no native equivalent — use the MLKit barcode scanner there.
## GPS / location metadata — `react-native-vision-camera-location`
```tsx
import { useLocation } from 'react-native-vision-camera-location'
const loc = useLocation({})
useEffect(() => { if (!loc.hasPermission) loc.requestPermission() }, [loc.hasPermission])
// Attach to photo:
const photo = await photoOutput.capturePhoto({ location: loc.currentLocation }, {})
// Attach to video recorder:
const recorder = await videoOutput.createRecorder({ location: loc.currentLocation })
```
Adds EXIF GPS tags to JPEGs and location metadata to mp4/mov. Imperative variant: `createLocationManager(...)` + `addOnLocationChangedListener`.
## Custom native `CameraOutput` (extensibility)
V5 exposes `NativeCameraOutput` so plugin authors can ship a fully custom output (e.g. a proprietary HDR pipeline, a ML streaming output) as a separate Nitro Module without forking the library.
```ts
// Your spec
export interface MyOutput extends HybridObject<{ ios: 'swift', android: 'kotlin' }> {
// ... methods + events your output exposes
}
```
Implement `NativeCameraOutput` (iOS) / its Android equivalent, expose a factory function in your Nitro module, and consumers attach via the standard `outputs={[myOutput, ...]}` prop. Delegate scaffolding to the build-nitro-modules skill.
## Constraints for advanced features — quick recap
```ts
// Photo HDR
[{ photoHDR: true }]
// Video HDR 10-bit HLG
[{ videoDynamicRange: { bitDepth: 'hdr-10-bit', colorSpace: 'hlg-bt2020', colorRange: 'full' } }]
// Apple Log
[{ videoDynamicRange: { bitDepth: 'hdr-10-bit', colorSpace: 'apple-log', colorRange: 'full' } }]
// Cinematic stabilization
[{ videoStabilizationMode: 'cinematic-extended' }]
// Prefer binned sensor readout
[{ binned: true }]
// Optimize for the frame output's resolution
[{ resolutionBias: frameOutput }]
```
Always pair advanced features with `device.isSessionConfigSupported(config)` / `onSessionConfigSelected` so your UI reflects what the Camera actually picked. <!-- source: CameraDevice.nitro.ts:611; useCamera.ts:71 -->
## Pointers
- Docs — Depth Output: https://visioncamera.margelo.com/docs/depth-output
- Docs — Object Output (iOS): https://visioncamera.margelo.com/docs/object-output
- API — `Depth`: https://visioncamera.margelo.com/api/react-native-vision-camera/hybrid-objects/Depth
- API — `DepthPixelFormat`: https://visioncamera.margelo.com/api/react-native-vision-camera/type-aliases/DepthPixelFormat
- Core repo: https://github.com/mrousavy/react-native-vision-camera
- Barcode scanner (MLKit, both platforms): https://github.com/mrousavy/react-native-vision-camera/tree/main/packages/react-native-vision-camera-barcode-scanner
- GPU Resizer (Metal on iOS, Vulkan on Android): https://github.com/mrousavy/react-native-vision-camera/tree/main/packages/react-native-vision-camera-resizer
- Skia preview: https://github.com/mrousavy/react-native-vision-camera/tree/main/packages/react-native-vision-camera-skia
- Location (EXIF GPS): https://github.com/mrousavy/react-native-vision-camera/tree/main/packages/react-native-vision-camera-location
- Nitro scaffolding for custom `NativeCameraOutput`: use the `build-nitro-modules` skill.
- Related: [outputs-and-constraints.md](./outputs-and-constraints.md), [frame-processors.md](./frame-processors.md), [capture-and-controls.md](./capture-and-controls.md), [migration-v4-to-v5.md](./migration-v4-to-v5.md)
references/capture-and-controls.md
# Capture & Controls
All the user-facing "camera app" behavior: taking photos, recording video, zoom, focus, exposure, and manual 3A locks.
## Taking photos
### Default path — in-memory
```tsx
const photoOutput = usePhotoOutput({
previewImageTargetSize: { width: 150, height: 150 }, // optional thumbnail
qualityPrioritization: 'balanced',
})
const photo = await photoOutput.capturePhoto(
{
flashMode: 'auto', // 'on' | 'off' | 'auto'
enableRedEyeReduction: true,
enableShutterSound: false,
// qualityPrioritization is NOT a per-capture setting — set it on usePhotoOutput({ qualityPrioritization }).
// source: CapturePhotoSettings (CameraPhotoOutput.nitro.ts:137-225) has no qualityPrioritization; it lives on PhotoOutputOptions:68
// location: locationFromReact-native-vision-camera-location
},
{
onWillBeginCapture: () => {},
onWillCapturePhoto: () => {},
onDidCapturePhoto: () => {},
onPreviewImageAvailable: (image) => {
// Fire-and-forget thumbnail for instant UX — often arrives before the full Photo resolves
},
},
)
// photo: Photo (hybrid object, in-memory)
// photo.width / photo.height / photo.orientation / photo.isMirrored
// photo.timestamp / photo.isRawPhoto / photo.containerFormat / photo.hasPixelBuffer
// photo.calibrationData? (CameraCalibrationData, when available)
// photo.depth? (Depth, when captured with depth enabled)
// photo.getPixelBuffer() // ArrayBuffer
// photo.getFileData() / photo.getFileDataAsync() // encoded + EXIF bytes
// photo.toImage() / await photo.toImageAsync() // -> Image (react-native-nitro-image)
// await photo.saveToTemporaryFileAsync() // -> file path, .dng if RAW
// await photo.saveToFileAsync(path) // write to a specific path
```
### File path — if you really need it
```tsx
const { filePath } = await photoOutput.capturePhotoToFile({ flashMode: 'on' }, {})
```
### `takeSnapshot` — zero-shutter-lag preview grab (Android only)
On the Camera ref, `takeSnapshot()` (no arguments) asynchronously grabs the current preview contents and resolves to an `Image` (`react-native-nitro-image`) — there is no `quality` option and it is not a synchronous JPEG. Lower fidelity than `capturePhoto`, but near-instant. Good for burst / scanner UIs where the user can't tell the difference. Android only. <!-- source: PreviewView.nitro.ts:114 `takeSnapshot(): Promise<Image>` @platform Android; Camera.tsx:218 calls it with no args -->
### RAW
```tsx
const photoOutput = usePhotoOutput({
containerFormat: 'dng', // triggers RAW negotiation
})
// On supported Apple devices this may become Apple ProRAW automatically.
const photo = await photoOutput.capturePhoto({}, {})
const path = await photo.saveToTemporaryFileAsync() // .dng file
const pixels = photo.getPixelBuffer() // raw pixel data when available
```
Verify via `device.isSessionConfigSupported(config)` (synchronous, on the device) before exposing a RAW toggle. Preview image callbacks are especially important here because RAW write latency is high. <!-- source: CameraDevice.nitro.ts:611 -->
### HDR
Photo HDR fuses an under/normal/over exposure at the ISP:
```tsx
<Camera constraints={[{ photoHDR: true }]} />
```
On Android, vendors expose `CameraExtension`s (`'hdr'`, `'night'`, `'bokeh'`, `'face-retouch'`, `'auto'`), discoverable via `useCameraDeviceExtensions(device)` / `getSupportedExtensions(device)`. **However**, in v5.0.11 there is no `<Camera>` prop to apply one — the library refactored to the Constraints API and has not re-wired extension application yet, so for Photo HDR use the `{ photoHDR: true }` constraint instead.
```ts
const extensions = useCameraDeviceExtensions(device)
const hdrExtension = extensions.find((e) => e.type === 'hdr') // CameraExtensionType
// Applying an extension to <Camera> is NOT supported yet (see source TODO) — use constraints={[{ photoHDR: true }]}
```
<!-- source: CameraExtension.nitro.ts:9 ("Camera Extensions currently cannot be used in VisionCamera since I refactored to the Constraints API"); no cameraExtension prop in useCamera.ts CameraProps or Camera.tsx CameraViewProps; type at CameraExtension.nitro.ts:24-29 -->
## Recording video
```tsx
const videoOutput = useVideoOutput({
enableAudio: true,
enablePersistentRecorder: false, // true = survives device switching, costs some overhead
})
<Camera outputs={[videoOutput]} /* + photoOutput if you want both */ />
```
```tsx
// A Recorder is single-use. Always create a new one per recording.
const recorder = await videoOutput.createRecorder({
// settings: codec, bitrate, container, audio, location...
})
await recorder.startRecording(
(filePath, reason) => console.log('finished:', filePath, reason), // reason: 'stopped' | 'max-duration-reached' | 'max-file-size-reached'
(err) => console.error('error:', err),
() => console.log('paused'),
() => console.log('resumed'),
)
// Progress polling
const interval = setInterval(() => {
console.log(recorder.recordedFileSize, recorder.isRecording, recorder.isPaused, recorder.filePath)
}, 500)
await recorder.pauseRecording()
await recorder.resumeRecording()
await recorder.stopRecording() // resolves immediately; onFinished fires after flush
// or:
await recorder.cancelRecording() // deletes the partial file
```
### Video HDR / Log
```tsx
<Camera constraints={[{
videoDynamicRange: { bitDepth: 'hdr-10-bit', colorSpace: 'hlg-bt2020', colorRange: 'full' }
}]} />
// Apple Log: colorSpace: 'apple-log'
```
Bit depths: `'sdr-8-bit'` (default) vs `'hdr-10-bit'`. Color spaces: `'srgb'`, `'hlg-bt2020'`, `'apple-log'`. Probe support with `device.isSessionConfigSupported(config)`. <!-- source: DynamicRange.ts; CameraDevice.nitro.ts:611 -->
### Stabilization
```tsx
<Camera constraints={[{ videoStabilizationMode: 'cinematic-extended' }]} />
```
Introduces startup latency and adds post-stop flush time — disable for shutter-speed-sensitive UX.
## Zoom
```tsx
import { useSharedValue } from 'react-native-reanimated'
const zoom = useSharedValue(1) // 1 = natural default
<Camera zoom={zoom} enableNativeZoomGesture={true} device={device} /* ... */ />
// Clamp manually if you drive zoom from your own gesture:
const clamped = Math.min(Math.max(value, device.minZoom), device.maxZoom)
// Imperative:
await controller.setZoom(3)
await controller.startZoomAnimation(5, 2) // animate to 5x; 2nd arg is `rate`, not a duration in seconds. source: CameraController.nitro.ts:376
await controller.cancelZoomAnimation()
// Virtual-device switch points (e.g. 0.5x ↔ 1x ↔ 3x):
device.zoomLensSwitchFactors
// User-facing display value:
controller.displayableZoomFactor
```
## Tap to focus / focusTo
Simple path — the native gesture does it all:
```tsx
<Camera enableNativeTapToFocusGesture={true} />
```
Custom gesture → `CameraRef`:
```tsx
const onTap = async ({ x, y }: { x: number, y: number }) => {
await camera.current?.focusTo({ x, y }) // view-point → camera-point is handled for you
}
```
Full control → `CameraController`:
```ts
const point = previewView.createMeteringPoint(viewX, viewY)
// or normalized directly:
const point = VisionCamera.createNormalizedMeteringPoint(0.5, 0.5)
await controller.focusTo(point, {
modes: ['AE', 'AF'], // default is all three ['AE','AF','AWB']
adaptiveness: 'locked', // 'continuous' (default) keeps adjusting as scene changes
autoResetAfter: 10, // seconds; pass null to disable
responsiveness: 'steady', // 'snappy' (default) — steady is better for video
})
await controller.resetFocus() // return to center-based metering
```
`focusTo` works in `<SkiaCamera />` in v5 (did not in v4).
## Exposure bias
```tsx
import { useSharedValue } from 'react-native-reanimated'
const exposure = useSharedValue(0) // 0 = neutral
<Camera exposure={exposure} /* ... */ />
// Imperative:
if (device.supportsExposureBias) {
await controller.setExposureBias(clamp(v, device.minExposureBias, device.maxExposureBias))
}
console.log(controller.exposureBias)
```
## Manual AE / AF / AWB lock (pro controls)
New in v5 — no v4 equivalent:
```ts
// Exposure: lock to fixed duration (seconds) + ISO
await controller.setExposureLocked(duration, iso)
await controller.lockCurrentExposure() // freeze whatever auto-exposure chose
// Focus: lens position 0..1
await controller.setFocusLocked(0.3)
await controller.lockCurrentFocus()
// White balance: setWhiteBalanceLocked accepts ONLY WhiteBalanceGains ({ redGain, greenGain, blueGain }).
// To lock by temperature (K) + tint, convert first (iOS):
const gains = controller.convertWhiteBalanceTemperatureAndTintValues({ temperature: 5500, tint: 0 })
await controller.setWhiteBalanceLocked(gains)
await controller.setWhiteBalanceLocked({ redGain: 1.0, greenGain: 0.1, blueGain: 0.1 })
// source: CameraController.nitro.ts:670 (setWhiteBalanceLocked(WhiteBalanceGains)) + :655 (convertWhiteBalanceTemperatureAndTintValues); WhiteBalanceGains.ts
await controller.lockCurrentWhiteBalance()
// Back to auto:
await controller.resetFocus()
```
Typical temperature range 2500K–8000K; tint −150..150; lens position 0..1. Always gate on device support: `device.supportsExposureLocking`, `device.supportsFocusLocking`, `device.supportsWhiteBalanceLocking`. <!-- source: CameraDevice.nitro.ts:429/402/469 (note: `...Locking`, not `supportsExposureLock`) -->
## Orientation
```tsx
<Camera orientationSource="device" /* or 'interface' or 'custom' */ />
```
- `'interface'` — rotates only when UI rotates (Snapchat-style).
- `'device'` — rotates with the phone regardless of UI orientation (stock camera app).
- `'custom'` — you drive `CameraOutput.outputOrientation` yourself.
Output orientation is applied via EXIF for photos, mp4/mov metadata for videos, view transform for preview. Frames expose raw `frame.orientation` — interpret it yourself in the processor.
## Photo preview image (thumbnail for instant UX)
```tsx
const photoOutput = usePhotoOutput({
previewImageTargetSize: { width: 100, height: 150 },
})
await photoOutput.capturePhoto({}, {
onPreviewImageAvailable: (image) => {
// show this thumbnail immediately — the full Photo is usually ~100ms behind
}
})
```
Gate on `device.supportsPreviewImage`.
## Performance knobs (capture side)
- `qualityPrioritization: 'speed'` for burst / instant UX; `'quality'` for hero shots.
- `takeSnapshot()` (Android, no args, resolves to an `Image`) for true zero-shutter-lag preview grabs. <!-- source: PreviewView.nitro.ts:114 -->
- Disable Video HDR & stabilization when not needed — both add latency.
- Prefer in-memory `capturePhoto` over `capturePhotoToFile` unless the consumer actually wants a file (e.g. sharing intent).
- Pre-create the photo output once, not per-shot.
## Pointers
- Docs — Photo Output: https://visioncamera.margelo.com/docs/photo-output
- API — `Photo`: https://visioncamera.margelo.com/api/react-native-vision-camera/hybrid-objects/Photo
- API — `CameraPhotoOutput`: https://visioncamera.margelo.com/api/react-native-vision-camera/hybrid-objects/CameraPhotoOutput
- API — `CapturePhotoCallbacks`: https://visioncamera.margelo.com/api/react-native-vision-camera/interfaces/CapturePhotoCallbacks
- Repo: https://github.com/mrousavy/react-native-vision-camera
- Related: [outputs-and-constraints.md](./outputs-and-constraints.md), [advanced-features.md](./advanced-features.md) (RAW/HDR/location EXIF), [frame-processors.md](./frame-processors.md), [quickstart-v5.md](./quickstart-v5.md)
references/frame-processors.md
# Frame Processors — v5 (Nitro)
V5 frame processors are worklet callbacks attached to a `CameraFrameOutput`. The worklets engine is **`react-native-worklets` (Software Mansion)**, not `react-native-worklets-core`. Native plugins are **Nitro `HybridObject`s** — the v4 `FrameProcessorPlugin` subclass + `VISION_EXPORT_SWIFT_FRAME_PROCESSOR` macro is gone.
This reference covers API fundamentals. For production low-latency GPU, ML, CV, zero-copy interop, or frame-coupled overlays, use the separate `react-native-vision-camera-realtime` skill.
## Required dependencies
```sh
npm i react-native-vision-camera-worklets react-native-worklets
```
Do NOT install `react-native-worklets-core`. If a user reports "frame processor does nothing", check for this. Remove any `react-native-worklets-core/plugin` entry from `babel.config.js` and add `react-native-worklets/plugin` in its place (Reanimated 4 depends on `react-native-worklets` under the hood, so Expo SDK 54+ templates already include this plugin). See https://docs.swmansion.com/react-native-worklets/docs/.
## The basic shape
```tsx
import { Camera, useFrameOutput, useCameraDevice } from 'react-native-vision-camera'
const device = useCameraDevice('back')
const frameOutput = useFrameOutput({
pixelFormat: 'yuv', // NOTE: default is 'native' (zero-copy). 'yuv' = CPU-accessible YUV; 'rgb' forces conversion. source: useFrameOutput.ts:123
targetResolution: CommonResolutions.VGA_16_9, // start small
onFrame(frame) {
'worklet'
try {
// frame.width, frame.height, frame.pixelFormat, frame.orientation, frame.timestamp
// Call Nitro plugins here (see below).
} finally {
frame.dispose()
}
},
})
<Camera device={device} isActive={true} outputs={[frameOutput]} />
```
### Non-negotiable: `frame.dispose()`
The camera maintains a bounded GPU-backed buffer pool. A leaked frame stalls the pipeline and frames are dropped. **Always** dispose, even in error paths:
```ts
onFrame(frame) {
'worklet'
try { /* work */ }
catch (e) { /* log */ }
finally { frame.dispose() }
}
```
The same rule applies to `Depth` frames from `useDepthOutput`.
## Pixel format decision
| Format | When |
|---|---|
| `'native'` | **Default.** Zero-copy GPU path — streams the session's negotiated `nativePixelFormat`. Resolved format may be YUV, RGB, RAW, or `'private'` — verify via `frame.pixelFormat`. |
| `'yuv'` | Best CPU-accessible choice: OpenCV, native camera pipelines, MLKit, Skia. A 4K YUV frame is ~12MB vs ~31MB RGB. |
| `'rgb'` | ML frameworks that hard-require RGB and don't convert internally. Prefer the GPU **Resizer** over paying RGB conversion on every frame. |
<!-- source: useFrameOutput.ts:123 (default 'native'); CameraFrameOutput.nitro.ts:71-95; VideoPixelFormat.ts:52-62 -->
## Async frame work when stale results are acceptable
Use an `AsyncRunner` when non-visual work cannot keep up and the feature accepts results from older frames. For frame-coupled visual feedback, use the real-time skill and optimize synchronous same-frame processing first.
```tsx
import { useAsyncRunner } from 'react-native-vision-camera'
const asyncRunner = useAsyncRunner()
const frameOutput = useFrameOutput({
onFrame(frame) {
'worklet'
const accepted = asyncRunner.runAsync(() => {
'worklet'
try {
const detections = runMlModel(frame)
// update Reanimated SharedValues directly — no runOnJS needed in v5
detectionsShared.value = detections
} finally {
frame.dispose()
}
})
if (!accepted) frame.dispose() // runner full; drop and keep camera flowing
},
})
```
- `runAsync` returns `boolean`. `true` = accepted, dispose **inside** the async callback. `false` = busy, dispose **immediately**.
- There is no `runAtTargetFps` in v5. Throttle by counting inside the worklet (`count.value = (count.value + 1) % 3`) and skipping, or rely on backpressure through the async runner.
- Each `useAsyncRunner()` gets its own dedicated worklet runtime. Multiple stages → multiple runners.
## Reanimated integration
Worklets in v5 can mutate Reanimated `SharedValue`s directly. No `runOnJS` round-trip:
```tsx
import { useSharedValue } from 'react-native-reanimated'
const faces = useSharedValue<Face[]>([])
const frameOutput = useFrameOutput({
onFrame(frame) {
'worklet'
try {
faces.value = detectFaces(frame) // drives Reanimated animations on UI thread
} finally { frame.dispose() }
},
})
```
Use SharedValues for state or animation that may update asynchronously. Do not drive frame-locked bounding boxes, meshes, or masks through SharedValues; draw them from the matching frame with Skia or WebGPU as described by the real-time skill.
## Coordinate conversions for view-based overlays
When an asynchronous view overlay is acceptable, convert native frame coordinates into preview-view space rather than reimplementing orientation math:
```ts
// Inside a worklet:
const cameraPoint = frame.convertFramePointToCameraPoint(framePoint)
// Inside a regular JS context (with a PreviewViewMethods ref):
const viewPoint = previewView.convertCameraPointToViewPoint(cameraPoint)
// ScannedObject convenience:
const viewObj = previewView.convertScannedObjectCoordinatesToViewCoordinates(scannedObject)
```
## Creating native plugins — Nitro only
Native Frame processor plugin requires to be created in nitro modules. Use build-nitro-modules skill or ask user to install that
### C++ cross-platform plugin
Also supported. Access the underlying buffer via `frame.getNativeBuffer()` and work against a single C++ codebase.
### Depth variant
Same pattern: accept a `Depth` in the spec, cast to `NativeDepth` to get `AVDepthData` / Android's depth representation.
## Best-practice checklist
- [ ] `pixelFormat`: keep the default `'native'` (zero-copy) for GPU pipelines; pass `'yuv'` when you need CPU pixel access (MLKit/OpenCV); `'rgb'` only if a consumer hard-requires it. <!-- source: useFrameOutput.ts:123 -->
- [ ] `targetResolution` on the frame output — smaller is faster. VGA or 720p is enough for most ML models.
- [ ] Every `onFrame` wrapped in `try { ... } finally { frame.dispose() }`.
- [ ] When stale results are acceptable, offload over-budget work with `useAsyncRunner` and explicit accepted/rejected disposal.
- [ ] No `runOnJS` for ordinary Reanimated `SharedValue` mutations (v5 supports direct mutation); do not use them for frame-locked overlays.
- [ ] For a CPU-visible ML tensor, use `react-native-vision-camera-resizer` for GPU-accelerated resize and conversion instead of doing those operations on the CPU.
- [ ] For coordinate-space math: use the `convertFramePointToCameraPoint` / `convertCameraPointToViewPoint` pair — don't re-implement orientation math.
- [ ] Native plugin: must be a Nitro `HybridObject`; there is no other supported path in v5.
## Pointers
- Docs — Async Frame Processing: https://visioncamera.margelo.com/docs/async-frame-processing
- API — `Frame`: https://visioncamera.margelo.com/api/react-native-vision-camera/hybrid-objects/Frame
- Worklets (SWM, babel plugin `react-native-worklets/plugin`): https://docs.swmansion.com/react-native-worklets/docs/
- Repo: https://github.com/mrousavy/react-native-vision-camera
- Nitro scaffolding: use the `build-nitro-modules` skill.
- Related: [advanced-features.md](./advanced-features.md) (Depth, Resizer, Barcode, SkiaCamera), [outputs-and-constraints.md](./outputs-and-constraints.md), [capture-and-controls.md](./capture-and-controls.md), [migration-v4-to-v5.md](./migration-v4-to-v5.md)
references/migration-templates.md
# Migration templates — V4 → V5 (full before/after files)
Copy-paste-ready, whole-screen templates for the most common camera screens. Each shows the complete v4 file and its complete v5 equivalent so you can port a screen in one pass. Every v5 template uses only the real v5.0.11 API — each non-obvious surface carries a `// source:` pointer into the cloned library.
Pair this with [migration-v4-to-v5.md](./migration-v4-to-v5.md) for the conceptual mapping and per-API notes. Load this file when you want to *transplant a screen wholesale*, not reason about individual APIs.
Hard rules these templates bake in (don't undo them):
- `capturePhoto(settings, callbacks)` takes **two** arguments — pass `{}` for callbacks if you have none. <!-- source: CameraPhotoOutput.nitro.ts:310 -->
- A `Recorder` is **single-use** — `createRecorder(...)` again for every recording. <!-- source: CameraVideoOutput.nitro.ts:238-243 -->
- Every `Frame`/`Depth` must be `.dispose()`d, even on error paths. <!-- source: Frame.nitro.ts:97; AsyncRunner.ts:18-44 -->
- Keep the Camera mounted; toggle `isActive` (here via `useIsFocused()`). <!-- source: useCamera.ts:40 (isActive) -->
- `useFrameOutput` `pixelFormat` defaults to `'native'`; pass `'yuv'` for CPU/ML access. <!-- source: useFrameOutput.ts:123 -->
---
## Template A — Photo capture screen
### ❌ V4
```tsx
import React, { useEffect, useRef, useState } from 'react'
import { StyleSheet, View, Image, Pressable } from 'react-native'
import {
Camera,
useCameraDevice,
useCameraPermission,
} from 'react-native-vision-camera'
export function PhotoScreen() {
const camera = useRef<Camera>(null)
const device = useCameraDevice('back')
const { hasPermission, requestPermission } = useCameraPermission()
const [uri, setUri] = useState<string>()
useEffect(() => { if (!hasPermission) requestPermission() }, [hasPermission])
const onShutter = async () => {
const file = await camera.current?.takePhoto({ flash: 'auto' })
if (file != null) setUri(`file://${file.path}`)
}
if (!hasPermission || device == null) return null
return (
<View style={StyleSheet.absoluteFill}>
<Camera ref={camera} style={StyleSheet.absoluteFill} device={device} isActive photo />
{uri && <Image source={{ uri }} style={styles.preview} />}
<Pressable style={styles.shutter} onPress={onShutter} />
</View>
)
}
```
### ✅ V5
```tsx
import React, { useEffect, useState } from 'react'
import { StyleSheet, View, Pressable } from 'react-native'
import { useIsFocused } from '@react-navigation/native'
import { NitroImage } from 'react-native-nitro-image'
import type { Image } from 'react-native-nitro-image'
import {
Camera,
useCameraDevice,
useCameraPermission,
usePhotoOutput,
} from 'react-native-vision-camera'
export function PhotoScreen() {
const device = useCameraDevice('back')
const { hasPermission, requestPermission } = useCameraPermission()
const isFocused = useIsFocused()
const [image, setImage] = useState<Image>()
// Output is created once and passed via `outputs`. Capture lives on the output, not a ref.
const photoOutput = usePhotoOutput({ qualityPrioritization: 'balanced' }) // source: usePhotoOutput.ts:31
useEffect(() => { if (!hasPermission) requestPermission() }, [hasPermission, requestPermission])
const onShutter = async () => {
// Two args required: settings, callbacks. Returns an in-memory Photo (no temp file). source: CameraPhotoOutput.nitro.ts:310
const photo = await photoOutput.capturePhoto({ flashMode: 'auto' }, {})
const img = await photo.toImageAsync() // source: Photo.nitro.ts:195
setImage(img)
photo.dispose() // free native memory. source: Photo.nitro.ts:28-29
}
if (!hasPermission || device == null) return null
return (
<View style={StyleSheet.absoluteFill}>
<Camera
style={StyleSheet.absoluteFill}
device={device}
isActive={isFocused}
outputs={[photoOutput]}
/>
{image && <NitroImage image={image} style={styles.preview} />}
<Pressable style={styles.shutter} onPress={onShutter} />
</View>
)
}
```
What changed: `photo` boolean → `usePhotoOutput()` in `outputs`; `takePhoto` (Camera ref, wrote a file) → `photoOutput.capturePhoto(settings, {})` (in-memory `Photo`); `flash` → `flashMode`; render via `toImageAsync()` + `<NitroImage />` instead of a `file://` URI; unmount-to-hide → `isActive={isFocused}`.
---
## Template B — Video recording screen
### ❌ V4
```tsx
import React, { useRef, useState } from 'react'
import { StyleSheet, Pressable } from 'react-native'
import { Camera, useCameraDevice } from 'react-native-vision-camera'
export function VideoScreen() {
const camera = useRef<Camera>(null)
const device = useCameraDevice('back')
const [recording, setRecording] = useState(false)
const start = () => {
setRecording(true)
camera.current?.startRecording({
onRecordingFinished: (video) => console.log('finished', video.path),
onRecordingError: (e) => console.error(e),
})
}
const stop = async () => {
await camera.current?.stopRecording()
setRecording(false)
}
if (device == null) return null
return (
<>
<Camera ref={camera} style={StyleSheet.absoluteFill} device={device} isActive video audio />
<Pressable style={styles.shutter} onPress={recording ? stop : start} />
</>
)
}
```
### ✅ V5
```tsx
import React, { useRef, useState } from 'react'
import { StyleSheet, Pressable } from 'react-native'
import { useIsFocused } from '@react-navigation/native'
import {
Camera,
useCameraDevice,
useVideoOutput,
} from 'react-native-vision-camera'
import type { Recorder } from 'react-native-vision-camera'
export function VideoScreen() {
const device = useCameraDevice('back')
const isFocused = useIsFocused()
const recorderRef = useRef<Recorder>(null) // a Recorder is single-use; hold the active one
const [recording, setRecording] = useState(false)
const videoOutput = useVideoOutput({ enableAudio: true }) // audio is OFF by default. source: CameraVideoOutput.nitro.ts:36-45
const start = async () => {
// Create a fresh Recorder per recording. source: CameraVideoOutput.nitro.ts:243
const recorder = await videoOutput.createRecorder({})
recorderRef.current = recorder
setRecording(true)
// startRecording(onFinished(filePath, reason), onError, onPaused?, onResumed?). source: Recorder.nitro.ts:80-88
await recorder.startRecording(
(filePath, reason) => console.log('finished', filePath, reason),
(e) => console.error(e),
)
}
const stop = async () => {
await recorderRef.current?.stopRecording() // resolves immediately; onFinished fires after flush. source: Recorder.nitro.ts:98
recorderRef.current = null
setRecording(false)
}
if (device == null) return null
return (
<>
<Camera
style={StyleSheet.absoluteFill}
device={device}
isActive={isFocused}
outputs={[videoOutput]}
/>
<Pressable style={styles.shutter} onPress={recording ? stop : start} />
</>
)
}
```
What changed: `video`/`audio` props → `useVideoOutput({ enableAudio: true })`; recording moved off the Camera ref onto a `Recorder` you create per take; the finished callback now yields `(filePath, reason)` where `reason` is `'stopped' | 'max-duration-reached' | 'max-file-size-reached'`. <!-- source: Recorder.nitro.ts:21-24 -->
---
## Template C — Frame processor with ML + Reanimated overlay
### ❌ V4
```tsx
import { useFrameProcessor, Camera, useCameraDevice } from 'react-native-vision-camera'
import { useSharedValue } from 'react-native-reanimated'
import { runAtTargetFps } from 'react-native-vision-camera'
export function ScanScreen() {
const device = useCameraDevice('back')
const boxes = useSharedValue([])
const frameProcessor = useFrameProcessor((frame) => {
'worklet'
runAtTargetFps(5, () => {
'worklet'
boxes.value = detectObjects(frame) // a v4 plugin
})
}, [])
if (device == null) return null
return <Camera style={{ flex: 1 }} device={device} isActive frameProcessor={frameProcessor} pixelFormat="yuv" />
}
```
### ✅ V5
```tsx
import React from 'react'
import { StyleSheet } from 'react-native'
import { useIsFocused } from '@react-navigation/native'
import { useSharedValue } from 'react-native-reanimated'
import {
Camera,
useCameraDevice,
useFrameOutput,
useAsyncRunner,
CommonResolutions,
} from 'react-native-vision-camera'
export function ScanScreen() {
const device = useCameraDevice('back')
const isFocused = useIsFocused()
const boxes = useSharedValue<Box[]>([])
const asyncRunner = useAsyncRunner() // dedicated worklet runtime for heavy work. source: useAsyncRunner.ts:33
const frameOutput = useFrameOutput({
pixelFormat: 'yuv', // CPU-accessible; default is 'native'. source: useFrameOutput.ts:123
targetResolution: CommonResolutions.VGA_16_9, // stream small for ML
onFrame(frame) {
'worklet'
// runAtTargetFps is gone — offload heavy work and let backpressure throttle. source: grep "runAtTargetFps" → absent
const accepted = asyncRunner.runAsync(() => {
'worklet'
try {
boxes.value = detectObjects(frame) // worklets mutate SharedValues directly — no runOnJS
} finally {
frame.dispose() // dispose INSIDE the async task when accepted. source: AsyncRunner.ts:26-41
}
})
if (!accepted) frame.dispose() // runner busy → drop this frame, still dispose
},
})
if (device == null) return null
return (
<Camera
style={StyleSheet.absoluteFill}
device={device}
isActive={isFocused}
outputs={[frameOutput]}
/>
)
}
```
What changed: `frameProcessor={useFrameProcessor(...)}` → `useFrameOutput({ onFrame })` in `outputs`; `pixelFormat` moved from the Camera onto the output (default is now `'native'`); `runAtTargetFps` removed — offload via `useAsyncRunner` with the explicit `accepted ? dispose-inside : dispose-immediate` pattern; native plugins must be Nitro `HybridObject`s. Requires `react-native-vision-camera-worklets` + `react-native-worklets`. <!-- source: useFrameOutput.ts:67-70 -->
---
## Template D — QR / barcode scanner screen
### ❌ V4
```tsx
import { Camera, useCameraDevice, useCodeScanner } from 'react-native-vision-camera'
export function ScannerScreen() {
const device = useCameraDevice('back')
const codeScanner = useCodeScanner({
codeTypes: ['qr', 'ean-13'],
onCodeScanned: (codes) => console.log(codes[0]?.value),
})
if (device == null) return null
return <Camera style={{ flex: 1 }} device={device} isActive codeScanner={codeScanner} />
}
```
### ✅ V5 — drop-in view (simplest)
```tsx
import { CodeScanner } from 'react-native-vision-camera-barcode-scanner'
export function ScannerScreen() {
return (
<CodeScanner
style={{ flex: 1 }} // CodeScannerOptions requires `style`. source: CodeScanner.tsx:15-30
isActive
barcodeFormats={['qr-code', 'ean-13']} // TargetBarcodeFormat values. source: BarcodeFormat.ts:9-31
onBarcodeScanned={(barcodes) => console.log(barcodes[0]?.rawValue)} // source: Barcode.nitro.ts:64
onError={(e) => console.error(e)}
/>
)
}
```
### ✅ V5 — integrated output (when you also need photo/video on the same Camera)
```tsx
import React from 'react'
import { StyleSheet } from 'react-native'
import { useIsFocused } from '@react-navigation/native'
import { Camera, useCameraDevice } from 'react-native-vision-camera'
import { useBarcodeScannerOutput } from 'react-native-vision-camera-barcode-scanner'
export function ScannerScreen() {
const device = useCameraDevice('back')
const isFocused = useIsFocused()
const barcodeOutput = useBarcodeScannerOutput({
barcodeFormats: ['qr-code'],
onBarcodeScanned: (barcodes) => console.log(barcodes[0]?.rawValue),
onError: (e) => console.error(e),
}) // source: useBarcodeScannerOutput.ts:59-64
if (device == null) return null
return (
<Camera
style={StyleSheet.absoluteFill}
device={device}
isActive={isFocused}
outputs={[barcodeOutput]}
/>
)
}
```
What changed: `useCodeScanner` + `codeScanner` prop (in v4 core) → the separate `react-native-vision-camera-barcode-scanner` package (MLKit on both platforms). Code type names changed (`'qr'` → `'qr-code'`); the value field is `rawValue` (was `value`). For iOS-only QR/face/body detection without an ML dependency, use core `useObjectOutput({ types, onObjectsScanned })` + `isScannedCode`/`isScannedFace` instead. <!-- source: ScannedObject.nitro.ts:35-63; isScannedObject.ts:12,21 -->
---
## Template E — "Pro" camera (photo + zoom gesture + tap-to-focus + flash + HDR), V5 only
A kitchen-sink screen showing how v5 composes outputs, constraints, animated values, and the `CameraRef`. There is no single v4 equivalent — in v4 this required `useCameraFormat`, `Reanimated.createAnimatedComponent`, `addWhitelistedNativeProps`, and manual `Gesture` wiring.
```tsx
import React, { useEffect, useRef, useState } from 'react'
import { StyleSheet, Pressable } from 'react-native'
import { useIsFocused } from '@react-navigation/native'
import { useSharedValue } from 'react-native-reanimated'
import {
Camera,
useCameraDevice,
useCameraPermission,
usePhotoOutput,
type CameraRef,
} from 'react-native-vision-camera'
export function ProCameraScreen() {
const device = useCameraDevice('back')
const { hasPermission, requestPermission } = useCameraPermission()
const isFocused = useIsFocused()
const cameraRef = useRef<CameraRef>(null) // source: Camera.tsx:35 (CameraRef)
const zoom = useSharedValue(1) // 1 = natural default; SharedValue is accepted directly. source: Camera.tsx:120
const [flashOn, setFlashOn] = useState(false)
const photoOutput = usePhotoOutput({ previewImageTargetSize: { width: 120, height: 160 } })
useEffect(() => { if (!hasPermission) requestPermission() }, [hasPermission, requestPermission])
const onShutter = async () => {
const photo = await photoOutput.capturePhoto(
{ flashMode: flashOn ? 'on' : 'off' },
{ onPreviewImageAvailable: (thumb) => {/* show thumb instantly */} }, // source: CameraPhotoOutput.nitro.ts:121
)
// ...use photo.toImageAsync()...
photo.dispose()
}
const onTapFocus = async (x: number, y: number) => {
// CameraRef converts view-point → camera-point for you. source: Camera.tsx:199-208
await cameraRef.current?.focusTo({ x, y })
}
if (!hasPermission || device == null) return null
return (
<>
<Camera
ref={cameraRef}
style={StyleSheet.absoluteFill}
device={device}
isActive={isFocused}
outputs={[photoOutput]}
zoom={zoom} // animate zoom via Reanimated SharedValue
enableNativeTapToFocusGesture // or wire your own gesture → cameraRef.focusTo(...)
constraints={[{ photoHDR: true }]} // negotiated intent; never throws. source: Constraint.ts:119
/>
<Pressable style={styles.flash} onPress={() => setFlashOn((v) => !v)} />
<Pressable style={styles.shutter} onPress={onShutter} />
</>
)
}
```
Key v5 building blocks used: `outputs={[...]}` (not boolean props), `zoom={SharedValue}` (no `createAnimatedComponent`/`addWhitelistedNativeProps`), `enableNativeTapToFocusGesture` (or `cameraRef.focusTo({x,y})`), `constraints={[{ photoHDR: true }]}` (no `useCameraFormat`), and `previewImageTargetSize` + `onPreviewImageAvailable` for an instant thumbnail.
---
## Shared styles (for the templates above)
```ts
const styles = StyleSheet.create({
preview: { position: 'absolute', bottom: 24, right: 24, width: 96, height: 128, borderRadius: 8 },
shutter: { position: 'absolute', alignSelf: 'center', bottom: 48, width: 72, height: 72, borderRadius: 36, backgroundColor: 'white' },
flash: { position: 'absolute', top: 48, right: 24, width: 44, height: 44, borderRadius: 22, backgroundColor: '#0008' },
})
```
## Pointers
- Conceptual mapping + per-API notes: [migration-v4-to-v5.md](./migration-v4-to-v5.md)
- Outputs, constraints, devices, session lifecycle: [outputs-and-constraints.md](./outputs-and-constraints.md)
- Capture & controls (photo/video/zoom/focus/3A): [capture-and-controls.md](./capture-and-controls.md)
- Frame processors & async: [frame-processors.md](./frame-processors.md)
- Barcode / Depth / Skia / Resizer / Location: [advanced-features.md](./advanced-features.md)
references/migration-v4-to-v5.md
# Migration guide — VisionCamera v4 → v5
V5 is a ground-up Nitro rewrite. Do not try to incrementally upgrade — most surfaces are renamed or replaced. Treat this as a port. Work through the changes below in order; each step is independent of the next and can land in its own commit.
## The cheat sheet
| Concern | v4 | v5 |
|---|---|---|
| Enable photo | `<Camera photo={true} />` | `const photoOutput = usePhotoOutput()` then `outputs={[photoOutput]}` |
| Enable video | `<Camera video={true} audio={true} />` | `const videoOutput = useVideoOutput({ enableAudio: true })` then `outputs={[videoOutput]}` |
| Enable frame processor | `frameProcessor={useFrameProcessor(...)}` | `const frameOutput = useFrameOutput({ onFrame })` then `outputs={[frameOutput]}` |
| Enable code scanner | `codeScanner={useCodeScanner(...)}` | Separate package `react-native-vision-camera-barcode-scanner` → `const barcodeOutput = useBarcodeScannerOutput(...)` then `outputs={[barcodeOutput]}` OR iOS-only native `useObjectOutput` |
| Format / resolution / fps / HDR | `useCameraFormat(device, [...])` + `format`, `fps`, `videoHdr`, `photoHdr` props | `constraints={[...]}` prop — priority-ordered, auto-negotiated |
| Take photo | `await cameraRef.current.takePhoto({ flash: 'on' })` | `await photoOutput.capturePhoto({ flashMode: 'on' }, {})` — returns in-memory `Photo` |
| Save photo to file | takePhoto returned a file path already | `await photoOutput.capturePhotoToFile(...)` returns `{ filePath }` |
| Start recording | `cameraRef.current.startRecording({ onRecordingFinished, onRecordingError })` | `const recorder = await videoOutput.createRecorder({}); await recorder.startRecording(onFinished, onError)` |
| Stop recording | `cameraRef.current.stopRecording()` | `recorder.stopRecording()` |
| Focus | `cameraRef.current.focus({ x, y })` | `cameraRef.current.focusTo({ x, y })` or `controller.focusTo(meteringPoint, { modes, adaptiveness, autoResetAfter, responsiveness })` |
| Pinch-to-zoom | Manual `Gesture.Pinch()` + `animatedProps` + `addWhitelistedNativeProps` | `<Camera enableNativeZoomGesture />` (or still-manual with `zoom={sharedValue}`) |
| Tap-to-focus | Manual `Gesture.Tap()` + `camera.focus(point)` | `<Camera enableNativeTapToFocusGesture />` |
| Pixel format | `pixelFormat` on `<Camera />` | Per-output: `useFrameOutput({ pixelFormat: 'yuv' })` |
| Worklets engine | `react-native-worklets-core` + babel plugin | `react-native-worklets` (Software Mansion) + `react-native-vision-camera-worklets`; no separate babel plugin (Reanimated plugin covers it) |
| Native frame processor plugin | Swift class extends `FrameProcessorPlugin`, registered via `VISION_EXPORT_SWIFT_FRAME_PROCESSOR` macro + `[String: Any?]` options | Nitro `HybridObject` spec, platforms in Swift/Kotlin, full typed params |
| Expo config plugin flag | `"enableFrameProcessors": false` in app.json | Worklets are opt-in by installing the package; no flag required |
## 1. Packages
Remove:
```sh
npm uninstall react-native-worklets-core
# if you had these community plugins, check their v5 status — many are being rewritten
npm uninstall vision-camera-code-scanner vision-camera-resize-plugin
```
Install:
```sh
npm i react-native-vision-camera@5 react-native-nitro-modules react-native-nitro-image
# If you used frame processors:
npm i react-native-vision-camera-worklets react-native-worklets
# If you used code scanning:
npm i react-native-vision-camera-barcode-scanner
# If you used location tagging (EXIF/video metadata):
npm i react-native-vision-camera-location
# If you used vision-camera-resize-plugin:
npm i react-native-vision-camera-resizer
cd ios && pod install
```
`babel.config.js` — remove the `react-native-worklets-core/plugin` entry. The react-native-worklets plugin needs to be added https://docs.swmansion.com/react-native-worklets/docs/.
Expo — remove `"enableFrameProcessors": false` from the `react-native-vision-camera` plugin block. That flag no longer exists.
## 2. `<Camera />` — props
V4 boolean props → V5 explicit outputs:
```tsx
// ❌ V4
<Camera
device={device}
photo={true}
video={true}
audio={true}
frameProcessor={frameProcessor}
codeScanner={codeScanner}
format={format}
fps={60}
videoHdr={true}
photoHdr={true}
pixelFormat="yuv"
enableDepthData={true}
/>
// ✅ V5
const photoOutput = usePhotoOutput()
const videoOutput = useVideoOutput({ enableAudio: true })
const frameOutput = useFrameOutput({ pixelFormat: 'yuv', onFrame })
const depthOutput = useDepthOutput({ onDepth })
<Camera
device={device}
outputs={[photoOutput, videoOutput, frameOutput, depthOutput]}
constraints={[
{ fps: 60 },
{ photoHDR: true },
{ videoDynamicRange: { bitDepth: 'hdr-10-bit', colorSpace: 'hlg-bt2020', colorRange: 'full' } },
]}
/>
```
Notes:
- `device` now accepts the string shortcut `"back"` or `"front"` in addition to a `CameraDevice` object. `useCameraDevice('back')` still works and is still preferred when you need capability probing.
- `format={...}` is removed. There is no direct replacement — express intent via `constraints`.
- `pixelFormat`, `videoHdr`, `photoHdr`, `fps`, `videoStabilizationMode` are all **not** Camera props anymore. They are either constraints or per-output options.
## 3. Formats → Constraints
The biggest conceptual shift. In v4 you built a `CameraDeviceFormat` by filtering a list. In v5 you declare what you want; the Camera picks.
```tsx
// ❌ V4
const format = useCameraFormat(device, [
{ fps: 60 },
{ videoHdr: true },
{ videoResolution: { width: 3840, height: 2160 } },
])
const minFps = Math.max(format.minFps, 20)
const maxFps = Math.min(format.maxFps, 30)
<Camera device={device} format={format} fps={60} videoHdr />
// ✅ V5
<Camera
device={device}
constraints={[
{ fps: 60 },
{ videoDynamicRange: CommonDynamicRanges.ANY_HDR },
{ resolutionBias: videoOutput }, // prefer videoOutput's target resolution
]}
/>
```
Rules:
- Order matters. Earliest constraint = highest priority.
- Never throws for an unreachable combination — it picks the closest supported fallback.
- Use `resolutionBias: output` to say "optimize for this output's resolution" instead of picking pixel dimensions by hand.
- Set per-output `targetResolution` (e.g. `usePhotoOutput({ targetResolution: CommonResolutions.UHD_16_9 })`) rather than computing `{ width, height }` in a format filter.
- Probe support with `device.isSessionConfigSupported(...)` when you need to gate UI (e.g. "show HDR toggle only if supported").
- Resolve target constraints without opening a Camera via `VisionCamera.resolveConstraints(...)` upfront if desired.
- `onSessionConfigSelected={(config) => console.log(config.toString())}` tells you what the Camera actually picked (same as `VisionCamera.resolveConstraints(...)`).
Common migrations:
```tsx
// Slow motion 240fps
// ❌ V4: useCameraFormat(device, [{ fps: 240 }]) then fps={format.maxFps}
// ✅ V5:
<Camera constraints={[{ fps: 240 }]} />
// "Highest possible photo resolution"
// ❌ V4: useCameraFormat(device, [{ photoResolution: 'max' }])
// ✅ V5: drop the constraint — fewer outputs already mean higher resolutions
// are negotiated. Or explicitly:
const photoOutput = usePhotoOutput({ targetResolution: CommonResolutions.UHD_16_9 })
<Camera outputs={[photoOutput]} constraints={[{ resolutionBias: photoOutput }]} />
// Video HDR
// ❌ V4: useCameraFormat(device, [{ videoHdr: true }]) + videoHdr prop
// ✅ V5:
<Camera constraints={[{
videoDynamicRange: { bitDepth: 'hdr-10-bit', colorSpace: 'hlg-bt2020', colorRange: 'full' }
}]} />
```
## 4. Taking photos
```tsx
// ❌ V4
const file = await cameraRef.current.takePhoto({
flash: 'on',
enableAutoRedEyeReduction: true,
enableShutterSound: false,
})
const uri = `file://${file.path}`
// ✅ V5 — in-memory (preferred)
const photo = await photoOutput.capturePhoto(
{ flashMode: 'on' },
{
onWillBeginCapture: () => {},
onWillCapturePhoto: () => {},
onDidCapturePhoto: () => {},
onPreviewImageAvailable: (image) => { /* thumbnail for instant UI */ },
}
)
const image = await photo.toImageAsync() // render with react-native-nitro-image
// ✅ V5 — if you genuinely need a file
const { filePath } = await photoOutput.capturePhotoToFile({ flashMode: 'on' }, {})
```
Behavioral changes:
- `takePhoto` wrote to a temp file on every call. `capturePhoto` skips that entirely. Prefer it — it is faster and uses less I/O and disk.
- Callbacks that used to fire on the Camera are now passed as a second argument object on every capture call (`onWillBeginCapture`, `onWillCapturePhoto`, `onDidCapturePhoto`, `onPreviewImageAvailable`).
- Thumbnail preview: in v5, configure `previewImageTargetSize` on `usePhotoOutput(...)` and receive via `onPreviewImageAvailable`, rather than showing the saved file.
- `photoQualityBalance` prop is gone. Pass `qualityPrioritization: 'speed' | 'balanced' | 'quality'` on the photo **output** options (`usePhotoOutput({ qualityPrioritization })`) — it is NOT a per-capture `CapturePhotoSettings` field. <!-- source: PhotoOutputOptions.qualityPrioritization (CameraPhotoOutput.nitro.ts:68); CapturePhotoSettings (:137-225) has none -->
- Shutter sound / red-eye options live on `CapturePhotoSettings`. Note the renames: `flash` → `flashMode`, `enableAutoRedEyeReduction` → `enableRedEyeReduction`.
## 5. Recording video
```tsx
// ❌ V4
<Camera video={true} audio={true} />
camera.current.startRecording({
onRecordingFinished: (video) => console.log(video.path),
onRecordingError: (err) => console.error(err),
})
await camera.current.stopRecording()
// ✅ V5
const videoOutput = useVideoOutput({ enableAudio: true })
<Camera outputs={[videoOutput]} />
const recorder = await videoOutput.createRecorder({
// optional: location, audio settings, codec, etc.
})
await recorder.startRecording(
(filePath, reason) => console.log('finished:', filePath, reason),
(err) => console.error(err),
() => console.log('paused'),
() => console.log('resumed'),
)
await recorder.pauseRecording()
await recorder.resumeRecording()
await recorder.stopRecording()
await recorder.cancelRecording() // deletes the file
```
- `Recorder` is **single-use**. Do not reuse it. Always `createRecorder` again.
- Progress: `recorder.recordedFileSize`, `recorder.isRecording`, `recorder.isPaused`, `recorder.filePath`.
- For recordings that must survive a device switch (e.g. front→back mid-recording), set `enablePersistentRecorder: true` on `useVideoOutput(...)`.
## 6. Frame processors
### 6a. Basic processor
```tsx
// ❌ V4
const frameProcessor = useFrameProcessor((frame) => {
'worklet'
const objects = detectObjects(frame)
}, [])
<Camera frameProcessor={frameProcessor} pixelFormat="yuv" />
// ✅ V5
const frameOutput = useFrameOutput({
pixelFormat: 'yuv',
onFrame(frame) {
'worklet'
try {
const objects = detectObjects(frame)
} finally {
frame.dispose() // REQUIRED
}
},
})
<Camera outputs={[frameOutput]} />
```
- `frame.dispose()` is mandatory in v5. The buffer pool is bounded; without dispose, the pipeline stalls and new frames are dropped.
- Worklets now run on `react-native-worklets` — a worklet can mutate a Reanimated `SharedValue` directly (no `runOnJS` needed).
### 6b. runAsync / runAtTargetFps → AsyncRunner
```tsx
// ❌ V4 runAsync
const frameProcessor = useFrameProcessor((frame) => {
'worklet'
runAsync(frame, () => {
'worklet'
doHeavyWork(frame)
})
}, [])
// ✅ V5 AsyncRunner with explicit backpressure
const asyncRunner = useAsyncRunner()
const frameOutput = useFrameOutput({
onFrame(frame) {
'worklet'
const accepted = asyncRunner.runAsync(() => {
'worklet'
try { doHeavyWork(frame) } finally { frame.dispose() }
})
if (!accepted) frame.dispose() // dropped — still must dispose
},
})
```
`runAsync` returns a boolean. `true` = work accepted; dispose inside the async callback. `false` = runner full; dispose immediately. Always handle both branches.
`runAtTargetFps` does not exist in v5. Throttle by counting frames in a worklet-level shared value and early-returning; or offload via async and let backpressure do the throttling for you.
### 6c. Native plugin authoring
This is a hard break. The v4 `FrameProcessorPlugin` base class and `VISION_EXPORT_SWIFT_FRAME_PROCESSOR` macro are gone. V5 plugins are Nitro `HybridObject`s with typed specs.
V4 (Swift) — deprecated:
```swift
@objc(FaceDetectorFrameProcessorPlugin)
public class FaceDetectorFrameProcessorPlugin: FrameProcessorPlugin {
public override init(proxy: VisionCameraProxyHolder, options: [AnyHashable:Any]! = [:]) {
super.init(proxy: proxy, options: options)
}
public override func callback(_ frame: Frame, withArguments arguments: [AnyHashable:Any]?) -> Any {
let buffer = frame.buffer
// ...
return nil
}
}
// + Objective-C:
// VISION_EXPORT_SWIFT_FRAME_PROCESSOR(FaceDetectorFrameProcessorPlugin, detectFaces)
```
V5 (Nitro) — the only supported path:
```ts
// spec.ts
import type { HybridObject } from 'react-native-nitro-modules'
import type { Frame } from 'react-native-vision-camera'
export interface MyNativePlugin extends HybridObject<{ ios: 'swift', android: 'kotlin' }> {
call(frame: Frame): void
}
```
```swift
// iOS: HybridMyNativePlugin.swift
import VisionCamera
import AVFoundation
class HybridMyNativePlugin: HybridMyNativePluginSpec {
func call(frame: any HybridFrameSpec) {
guard let native = frame as? any NativeFrame else { return }
let buffer = native.sampleBuffer // CMSampleBuffer
// ...
}
}
```
```kotlin
// Android: HybridMyNativePlugin.kt
import com.margelo.nitro.camera.HybridFrameSpec
import com.margelo.nitro.camera.public.NativeFrame
class HybridMyNativePlugin : HybridMyNativePluginSpec() {
fun call(frame: HybridFrameSpec) {
val native = frame as? NativeFrame ?: return
val image = native.image // ImageProxy
// ...
}
}
```
```ts
// JS call site
import { NitroModules } from 'react-native-nitro-modules'
const plugin = NitroModules.createHybridObject<MyNativePlugin>('MyNativePlugin')
const frameOutput = useFrameOutput({
onFrame(frame) {
'worklet'
plugin.call(frame)
frame.dispose()
},
})
```
For full Nitro scaffolding (nitrogen codegen, podspec, CMake, linking VisionCamera), delegate to the `build-nitro-modules` skill.
## 7. Code scanning
```tsx
// ❌ V4 — in core
const codeScanner = useCodeScanner({
codeTypes: ['qr', 'ean-13'],
onCodeScanned: (codes) => {},
})
<Camera codeScanner={codeScanner} />
// ✅ V5 — separate package (MLKit, works on both platforms)
import { useBarcodeScannerOutput } from 'react-native-vision-camera-barcode-scanner'
const barcodeOutput = useBarcodeScannerOutput({
barcodeFormats: ['qr-code', 'ean-13'],
onBarcodeScanned: (barcodes) => {},
})
<Camera outputs={[barcodeOutput]} />
// Or use the simple drop-in view:
import { CodeScanner } from 'react-native-vision-camera-barcode-scanner'
<CodeScanner style={{ flex: 1 }} isActive barcodeFormats={['qr-code']} onBarcodeScanned={(barcodes) => {}} onError={(e) => {}} /> {/* style is required */}
// Or, iOS-only, no ML dependency, native AVCaptureMetadataOutput:
import { useObjectOutput, isScannedCode } from 'react-native-vision-camera'
const objectOutput = useObjectOutput({
types: ['qr'],
onObjectsScanned: (objects) => {
for (const obj of objects) if (isScannedCode(obj)) console.log(obj.value)
},
})
<Camera outputs={[objectOutput]} />
```
- V4 `'ean-13'` iOS-UPC-A ambiguity: fixed in v5 because MLKit is used on both platforms.
- Narrow `barcodeFormats` to only what you need — fewer formats = faster detection.
## 8. Focus
```tsx
// ❌ V4
await camera.current.focus({ x: tap.x, y: tap.y })
// ✅ V5 — simple path (CameraRef converts view → camera coords for you)
await camera.current.focusTo({ x: tap.x, y: tap.y })
// ✅ V5 — imperative controller, full options
const meteringPoint = previewView.createMeteringPoint(tap.x, tap.y)
await controller.focusTo(meteringPoint, {
modes: ['AE', 'AF'], // subset of 3A metering
adaptiveness: 'locked', // 'continuous' (default) or 'locked'
autoResetAfter: 10, // seconds; null to disable auto-reset
responsiveness: 'steady', // 'snappy' (default) or 'steady'
})
await controller.resetFocus()
// Or let the library handle it:
<Camera enableNativeTapToFocusGesture={true} />
```
`focusTo` now works with `<SkiaCamera />` too (v4 only supported the default preview).
## 9. Zoom
```tsx
// ❌ V4 — manual Reanimated integration
Reanimated.addWhitelistedNativeProps({ zoom: true })
const ReanimatedCamera = Reanimated.createAnimatedComponent(Camera)
const zoom = useSharedValue(device.neutralZoom)
const animatedProps = useAnimatedProps<CameraProps>(() => ({ zoom: zoom.value }), [zoom])
<ReanimatedCamera {...props} animatedProps={animatedProps} />
// ✅ V5 — zoom prop accepts a SharedValue natively
const zoom = useSharedValue(device.minZoom)
<Camera zoom={zoom} /* ... */ />
// Or native gesture:
<Camera enableNativeZoomGesture={true} />
// Imperative:
await controller.setZoom(2)
await controller.startZoomAnimation(5, 2) // animate to 5x; 2nd arg is `rate`, not a duration in seconds
await controller.cancelZoomAnimation()
```
`device.neutralZoom` was the v4 default. In v5, `1` is the recommended default and `device.zoomLensSwitchFactors` exposes the virtual-device switch points.
## 10. Manual 3A (new capability in v5)
No v4 equivalent — these didn't exist. Pro-camera controls:
```ts
await controller.setFocusLocked(0.3) // lens pos 0..1
await controller.setExposureLocked(minDuration, maxISO)
await controller.setWhiteBalanceLocked({ redGain: 1, greenGain: 0.1, blueGain: 0.1 })
// or lock current auto values:
await controller.lockCurrentExposure()
await controller.lockCurrentFocus()
await controller.lockCurrentWhiteBalance()
```
## 11. Pixel formats
v4 `pixelFormat` was on the Camera (`"yuv" | "rgb"`). In v5 it moves to the **Frame output** (`useFrameOutput`), where the default is `"native"`. (`useDepthOutput` has no `pixelFormat` — read the depth format from `depth.pixelFormat`.)
```tsx
const frameOutput = useFrameOutput({
// pixelFormat defaults to 'native' (zero-copy GPU path); verify the resolved format via frame.pixelFormat
pixelFormat: 'yuv', // CPU-accessible YUV (MLKit/OpenCV); ~2.6× cheaper than 'rgb'
// pixelFormat: 'rgb', // forces YUV→RGB conversion; prefer the Resizer for ML
onFrame,
})
```
<!-- source: useFrameOutput.ts:123 (default 'native'); useDepthOutput.ts:70-77 (no pixelFormat); VideoPixelFormat.ts:52-62 -->
## 12. Lifecycle
Nothing changed conceptually — `isActive` is still the right lever. Keep the Camera mounted; combine with `useIsFocused()`:
```tsx
const isFocused = useIsFocused()
<Camera isActive={isFocused} ... />
```
Interruption handling (incoming calls, thermal throttle) is now formalised on all three usage forms — `<Camera />`, `useCamera`, imperative `CameraSession` — via `onInterruptionStarted`/`onInterruptionEnded`.
## 13. Imperative session API (new in v5)
For multi-cam or fully programmatic control there's a new low-level API:
```ts
const session = await VisionCamera.createCameraSession(/* isMultiCam */ false)
const device = await getDefaultCameraDevice('back')
const photoOutput = VisionCamera.createPhotoOutput({})
const videoOutput = VisionCamera.createVideoOutput({})
await session.configure([{
input: device,
outputs: [
{ output: photoOutput, mirrorMode: 'auto' },
{ output: videoOutput, mirrorMode: 'auto' },
],
constraints: [{ fps: 30 }],
}], {})
await session.start()
// ...
await session.stop()
// No session.dispose() exists — Nitro releases the CameraSession once it is unreferenced. source: CameraSession.nitro.ts
```
Multi-cam: `createCameraSession(true)` + one connection per input device. Gate on `VisionCamera.supportsMultiCamSessions` (platform-level) and pick a supported input pair from `deviceFactory.supportedMultiCamDeviceCombinations`. <!-- source: CameraFactory.nitro.ts:71; CameraSession.nitro.ts:70-74,182-186 (no supportsMultiCamSessions on CameraDevice) -->
## 14. Testing / mocking
V4's `RN_SRC_EXT` / Metro mock pattern still works in v5 — the library exports the same top-level module shape, just with different identifiers. Your mock needs to expose `Camera`, `useCameraPermission`, `useCameraDevice`, `usePhotoOutput`, `useVideoOutput`, `useFrameOutput`, and whatever else your app imports.
## Checklist for a v4→v5 PR
- [ ] Uninstall `react-native-worklets-core` and its babel plugin; uninstall `vision-camera-code-scanner`, `vision-camera-resize-plugin`, and any v4-only frame-processor plugins.
- [ ] Install `react-native-nitro-modules`, `react-native-nitro-image`, and whichever v5 sub-packages you need. Run `pod install`.
- [ ] Replace `photo` / `video` / `audio` / `frameProcessor` / `codeScanner` props with `outputs={[...]}`.
- [ ] Replace `takePhoto(...)` call sites with `photoOutput.capturePhoto(...)`; drop `file://`-prefix logic unless you explicitly need `capturePhotoToFile`.
- [ ] Replace `startRecording/stopRecording` on the Camera ref with the Recorder lifecycle on the video output.
- [ ] Delete the `format`/`useCameraFormat` code path. Re-express FPS, HDR, stabilization, and resolution intent via `constraints={[...]}` and per-output `targetResolution`.
- [ ] Audit every `onFrame` worklet: add `try/finally` + `frame.dispose()`. Port any `runAsync(frame, ...)` to `asyncRunner.runAsync(() => ...)` with the `accepted ? dispose-inside : dispose-immediate` pattern.
- [ ] Rewrite any in-house native frame-processor plugin as a Nitro `HybridObject`. Remove the `FrameProcessorPlugin` subclass and the registration macro.
- [ ] Delete `Reanimated.createAnimatedComponent(Camera)` / `addWhitelistedNativeProps` — pass the `SharedValue` directly to `zoom` / `exposure`.
- [ ] Replace `camera.focus(point)` with `camera.focusTo(point)` or the native gesture prop.
- [ ] Remove Expo config plugin's `enableFrameProcessors` option.
- [ ] Smoke-test on a real device (emulators frequently misreport formats).
## Pointers
- V5 release blog: https://blog.margelo.com/whats-new-in-visioncamera-v5
- V5 docs: https://visioncamera.margelo.com
- V4 archived docs: https://visioncamera4.margelo.com
- V4 snapshot repo: https://github.com/margelo/react-native-vision-camera-v4-snapshot
- V5 repo (release notes via `gh api repos/mrousavy/react-native-vision-camera/releases/tags/v5.0.0`): https://github.com/mrousavy/react-native-vision-camera
- Worklets engine (SWM, replaces worklets-core): https://docs.swmansion.com/react-native-worklets/docs/
- Related: [quickstart-v5.md](./quickstart-v5.md), [outputs-and-constraints.md](./outputs-and-constraints.md), [frame-processors.md](./frame-processors.md), [capture-and-controls.md](./capture-and-controls.md), [advanced-features.md](./advanced-features.md)
references/outputs-and-constraints.md
# Outputs, Constraints, Devices, and Session lifecycle
The three ideas that define v5:
1. **Outputs are first-class objects.** A Camera is "idle" until outputs are attached. Each output (Photo, Video, Frame, Depth, Preview, Object) is a `HybridObject` created upfront, passed via `outputs={[...]}`, and owns its own capture methods.
2. **Constraints are negotiated declarations of intent.** You say what you want (fps, HDR, resolution bias, stabilization); the Camera picks the closest supported config. Array order = priority.
3. **There are three usage forms.** Declarative `<Camera />`, hook-driven `useCamera(...)`, and imperative `VisionCamera.createCameraSession(...)`. Pick the least powerful one that does the job.
## Outputs
| Output | Create with | Purpose |
|---|---|---|
| `CameraPhotoOutput` | `usePhotoOutput(options)` | Still capture. `capturePhoto` / `capturePhotoToFile`. |
| `CameraVideoOutput` | `useVideoOutput(options)` | Video recording. `createRecorder`. |
| `CameraFrameOutput` | `useFrameOutput({ onFrame, pixelFormat, targetResolution })` | Real-time frame streaming for ML / CV. |
| `CameraDepthFrameOutput` | `useDepthOutput({ onDepth })` | Depth streams from LiDAR / ToF / disparity virtual cameras. |
| `CameraPreviewOutput` | `usePreviewOutput()` | Manual preview via `<NativePreviewView />` (multi-cam). |
| `CameraObjectOutput` (iOS only) | `useObjectOutput({ types, onObjectsScanned })` | Native QR/face/body detection via `AVCaptureMetadataOutput`, no ML dep. |
| Barcode output (separate package) | `useBarcodeScannerOutput(...)` | MLKit barcode detection on both platforms. |
Key rules:
- Attach all outputs you want to use at once — swapping outputs at runtime causes a brief reconfiguration pause.
- **Fewer outputs = higher negotiated resolution.** 8K photo on iOS is only reachable with photo-only output sets.
- Each output has its own `targetResolution`. Set it to the smallest resolution the consumer actually needs (e.g. your ML model wants 128×128 → don't stream 4K frames to dispose of 99% of pixels).
- Use the `onConfigured` callback (on the Camera or session) to gate work that depends on the output being ready.
### Hook vs imperative creation
```tsx
// Hook (declarative + useCamera)
const photoOutput = usePhotoOutput({ targetResolution: CommonResolutions.UHD_16_9 })
const videoOutput = useVideoOutput({ enableAudio: true })
<Camera device="back" isActive={true} outputs={[photoOutput, videoOutput]} />
// Or:
const camera = useCamera({
device,
outputs: [photoOutput, videoOutput],
constraints: [{ fps: 30 }],
isActive: true,
})
```
```ts
// Imperative — full control, required for multi-cam
const session = await VisionCamera.createCameraSession(/* isMultiCam */ false)
const device = await getDefaultCameraDevice('back')
const photoOutput = VisionCamera.createPhotoOutput({})
const [controller] = await session.configure([{
input: device,
outputs: [{ output: photoOutput, mirrorMode: 'auto' }],
constraints: [{ fps: 30 }],
}], {})
await session.start()
// ... later:
await session.stop()
// No session.dispose() — CameraSession has no dispose(); Nitro releases it once unreferenced.
// source: CameraSession.nitro.ts only exposes isRunning/configure/start/stop/addOn*Listener
```
## Constraints
The Constraints API replaces the entire v4 formats system. You never pick a `CameraDeviceFormat` by hand again.
### Principles
- **Array order = priority, descending.** The first constraint is the one the Camera will bend least to satisfy.
- **Always succeeds.** `{ fps: 99999 }` resolves to the highest supported FPS. Constraints never throw for unreachable combos.
- **Probe with `device.isSessionConfigSupported(config)`** — a synchronous method on `CameraDevice` returning `boolean` — when you want to conditionally show UI (e.g. only render an HDR toggle on devices that can actually do HDR at the requested resolution). <!-- source: CameraDevice.nitro.ts:611 `isSessionConfigSupported(config: CameraSessionConfig): boolean` -->
- **Observe the chosen config** via `onSessionConfigSelected={(config) => ...}` or by reading `config` returned from `resolveConstraints`.
### Constraint types
```ts
// FPS
{ fps: 60 }
// Photo HDR (3-frame fusion at ISP)
{ photoHDR: true }
// Video HDR / Log
{ videoDynamicRange: { bitDepth: 'hdr-10-bit', colorSpace: 'hlg-bt2020', colorRange: 'full' } }
// Shortcut:
{ videoDynamicRange: CommonDynamicRanges.ANY_HDR }
// Apple Log: colorSpace: 'apple-log'
// Stabilization
{ videoStabilizationMode: 'cinematic-extended' }
{ previewStabilizationMode: 'standard' }
// Pixel format (frame output)
{ pixelFormat: 'yuv-420-8-bit-full' }
// Resolution — prefer an output-driven bias over pixel dimensions
{ resolutionBias: photoOutput }
// Binned sensor readout — bigger effective pixels, better low-light, less bandwidth
{ binned: true }
```
### Common recipes
Photo-first tuning (highest-res photos, still a usable preview/video path):
```ts
constraints={[
{ resolutionBias: photoOutput },
{ photoHDR: true },
{ resolutionBias: videoOutput },
]}
```
High-FPS video (60fps wins over resolution):
```ts
constraints={[
{ fps: 60 },
{ resolutionBias: videoOutput },
]}
```
Low-resolution frame stream for ML:
```ts
const frameOutput = useFrameOutput({ targetResolution: CommonResolutions.VGA_16_9, onFrame })
constraints={[{ resolutionBias: frameOutput }]}
```
10-bit HLG video:
```ts
constraints={[{ videoDynamicRange: { bitDepth: 'hdr-10-bit', colorSpace: 'hlg-bt2020', colorRange: 'full' } }]}
```
### Programmatic resolution
```ts
const config = await VisionCamera.resolveConstraints(
device,
[{ output: videoOutput, mirrorMode: 'auto' }],
[{ resolutionBias: videoOutput }, { fps: 60 }],
) // resolveConstraints is a method on VisionCamera — source: CameraFactory.nitro.ts:149
const ok = device.isSessionConfigSupported(config) // sync, single arg, on the device — source: CameraDevice.nitro.ts:611
```
## Devices
- `useCameraDevice('back')` / `useCameraDevice('front')` / `useCameraDevice('external')` — most common.
- `useCameraDevices()` for reactive listings (e.g. UVC plug/unplug).
- Filter by physical types when you need a simpler pipeline (faster startup):
```tsx
const device = useCameraDevice('back', { physicalDevices: ['wide-angle'] })
```
vs. a `'triple'` virtual device that switches across ultra-wide / wide / telephoto.
- Probe capabilities upfront: `device.getSupportedResolutions('photo')`, `device.supportedFPSRanges`, `device.supportedPixelFormats`, `device.supportsPhotoHDR`, `device.supportsVideoStabilizationMode('cinematic')`, `device.supportsExposureBias`, `device.supportsFocusMetering`, `device.supportsFocusLocking`, `device.zoomLensSwitchFactors`. Multi-cam is **platform-level**, not a device flag: use `VisionCamera.supportsMultiCamSessions` and `deviceFactory.supportedMultiCamDeviceCombinations`. <!-- source: device props in CameraDevice.nitro.ts; multi-cam in CameraFactory.nitro.ts:71 + CameraSession.nitro.ts:70,182 (no supportsMultiCamSessions on CameraDevice) -->
- External cameras (iPad/Mac/UVC on Android) use `'external'` and emit change notifications: `addOnCameraDevicesChangedListener`.
## Session lifecycle
Three states:
1. **Idle** — no connections, not running.
2. **Ready** — configured with outputs but not streaming. Memory + permissions still held.
3. **Active** — streaming.
Toggle between Ready and Active with `isActive`. Do **not** toggle between Idle and Ready by mounting/unmounting — that tears down and rebuilds the whole session.
```tsx
import { useIsFocused } from '@react-navigation/native'
function Screen() {
const isFocused = useIsFocused()
return <Camera isActive={isFocused} /* ... */ />
}
```
Interruptions (phone calls, thermal throttle, another app grabbing the camera):
```tsx
<Camera
onInterruptionStarted={(reason) => {}}
onInterruptionEnded={() => {}}
onError={(error) => {}}
/>
```
Reconfiguration cost: changing `outputs`, `device`, or `constraints` pauses briefly while the session rebuilds. Batch changes and avoid changing them in render loops. The docs guide explicitly says: *"ensure that most configuration is handled before you start the session, as any configuration while the session is running can be expensive and cause stutters."*
## Performance cheatsheet
- Prefer single-physical-device cameras (`'wide-angle'` only) over virtual multi-device cameras when you don't need seamless zoom switching — faster startup.
- Disable features you don't need: video HDR, stabilization, unneeded outputs.
- For frame output, the default `pixelFormat` is `'native'` (zero-copy, format negotiated — check `frame.pixelFormat`). Among CPU-accessible formats, `'yuv'` beats `'rgb'` (~2.6× less bandwidth); `'rgb'` forces a conversion per frame. <!-- source: useFrameOutput.ts:123 (default 'native'); CameraFrameOutput.nitro.ts:71-95 -->
- Match FPS and resolution to the consumer. 30fps is sufficient for 99% of recording; 60/120/240 only when the UX demands it.
- Binned formats (`{ binned: true }`) for better low-light and lower bandwidth when fine detail is not needed.
- For rapid photo bursts: `qualityPrioritization: 'speed'` on the photo output options or per capture. For instant capture with zero shutter lag, use `takeSnapshot()` via the Camera ref.
- Don't rotate buffers in the pipeline — handle orientation downstream with flags.
## Pointers
- Docs — Camera Outputs: https://visioncamera.margelo.com/docs/camera-outputs
- Docs — Constraints: https://visioncamera.margelo.com/docs/constraints
- Docs — Photo Output: https://visioncamera.margelo.com/docs/photo-output
- Docs — Depth Output: https://visioncamera.margelo.com/docs/depth-output
- Docs — Object Output: https://visioncamera.margelo.com/docs/object-output
- Docs — Async Frame Processing: https://visioncamera.margelo.com/docs/async-frame-processing
- API — `Constraint` type: https://visioncamera.margelo.com/api/react-native-vision-camera/type-aliases/Constraint
- API — `CameraPhotoOutput`: https://visioncamera.margelo.com/api/react-native-vision-camera/hybrid-objects/CameraPhotoOutput
- Related: [capture-and-controls.md](./capture-and-controls.md), [frame-processors.md](./frame-processors.md), [advanced-features.md](./advanced-features.md), [quickstart-v5.md](./quickstart-v5.md)
references/quickstart-v5.md
# Quickstart — VisionCamera v5
Minimum working Camera on iOS and Android with v5.
## 1. Install
Core + required Nitro peers:
```sh
npm i react-native-vision-camera@5 react-native-nitro-modules react-native-nitro-image
cd ios && pod install
```
Optional, install only what the app needs:
```sh
# Frame Processors (worklets). The new default worklets engine is Software Mansion's
# react-native-worklets, NOT react-native-worklets-core.
npm i react-native-vision-camera-worklets react-native-worklets
# Barcode/QR (MLKit on iOS + Android, consistent formats)
npm i react-native-vision-camera-barcode-scanner
# GPU-accelerated frame resize for ML pipelines (Metal on iOS, Vulkan on Android)
npm i react-native-vision-camera-resizer
# GPS/EXIF metadata
npm i react-native-vision-camera-location
# Skia-based preview + shader effects.
npm i react-native-vision-camera-skia @shopify/react-native-skia react-native-vision-camera-worklets react-native-worklets
```
babel plugin is needed for worklet. `react-native-worklets/plugin` must be added in `babel.config.js` . See https://docs.swmansion.com/react-native-worklets/docs/
## 2. Permissions
**iOS — `ios/<App>/Info.plist`:**
```xml
<key>NSCameraUsageDescription</key>
<string>$(PRODUCT_NAME) needs access to your Camera to capture photos and videos.</string>
<key>NSMicrophoneUsageDescription</key>
<string>$(PRODUCT_NAME) needs access to your Microphone to record audio.</string>
```
Add `NSLocationWhenInUseUsageDescription` only if using `react-native-vision-camera-location`.
**Android — `android/app/src/main/AndroidManifest.xml`:**
```xml
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
```
Request permissions in JS before rendering:
```tsx
import { useCameraPermission, useMicrophonePermission } from 'react-native-vision-camera'
const { hasPermission, requestPermission } = useCameraPermission()
useEffect(() => { if (!hasPermission) requestPermission() }, [hasPermission])
```
## 3. First Camera — photo capture
The idiomatic v5 screen: hook-based permission → hook-based device → output(s) → `<Camera />`.
```tsx
import { useEffect } from 'react'
import { StyleSheet } from 'react-native'
import { useIsFocused } from '@react-navigation/native'
import {
Camera,
useCameraDevice,
useCameraPermission,
usePhotoOutput,
} from 'react-native-vision-camera'
export function CameraScreen() {
const { hasPermission, requestPermission } = useCameraPermission()
useEffect(() => { if (!hasPermission) requestPermission() }, [hasPermission, requestPermission])
const device = useCameraDevice('back')
const isFocused = useIsFocused()
const photoOutput = usePhotoOutput()
if (!hasPermission || device == null) return null
const onShutter = async () => {
const photo = await photoOutput.capturePhoto({ flashMode: 'auto' }, {})
// Display in-memory without hitting disk:
const image = await photo.toImageAsync() // from react-native-nitro-image
// ... render <Image source={image} />
}
return (
<Camera
style={StyleSheet.absoluteFill}
device={device}
isActive={isFocused}
outputs={[photoOutput]}
/>
)
}
```
Key differences from v4 in this snippet:
- `outputs={[photoOutput]}` replaces the `photo={true}` boolean prop.
- `capturePhoto` lives on the output, not the Camera ref.
- The return value is a `Photo` — in memory, with EXIF and camera calibration already attached. Use `capturePhotoToFile` only if you genuinely need a file path.
- `isActive={isFocused}` keeps the session configured but stopped when the screen is off-stage. Do not unmount the Camera to hide it.
## 4. Video recording — minimum
```tsx
import { Camera, useCameraDevice, useVideoOutput, usePhotoOutput } from 'react-native-vision-camera'
const device = useCameraDevice('back')
const videoOutput = useVideoOutput({ enableAudio: true })
const photoOutput = usePhotoOutput()
const record = async () => {
const recorder = await videoOutput.createRecorder({})
await recorder.startRecording(
(filePath, reason) => console.log('finished:', filePath, reason),
(err) => console.error(err),
)
// later...
await recorder.stopRecording()
}
return <Camera style={StyleSheet.absoluteFill} device={device} isActive={true} outputs={[photoOutput, videoOutput]} />
```
Notes:
- A `Recorder` is **single-use**. Call `createRecorder` again for each recording.
- Audio is **off by default** — pass `enableAudio: true` and ensure microphone permission.
- `stopRecording()` resolves immediately; the `onFinished` callback fires once the file has been flushed.
## 5. Minimum Frame Processor
```tsx
import { Camera, useFrameOutput, useCameraDevice } from 'react-native-vision-camera'
const device = useCameraDevice('back')
const frameOutput = useFrameOutput({
pixelFormat: 'yuv', // optional; default is 'native' (zero-copy). 'yuv' = CPU-accessible YUV (good for MLKit/OpenCV) — source: useFrameOutput.ts:123
onFrame(frame) {
'worklet'
try {
// ... work with frame.width, frame.height, frame.pixelFormat, native buffer
} finally {
frame.dispose() // REQUIRED — buffer pool is bounded
}
},
})
return <Camera style={StyleSheet.absoluteFill} device={device} isActive={true} outputs={[frameOutput]} />
```
Requires `react-native-vision-camera-worklets` + `react-native-worklets`. See [frame-processors.md](frame-processors.md) for async offloading, native plugin authoring, and pixel-format decisions.
## 6. Common gotchas on first install
- "Nitro module not found" / immediate native crash → peers missing. Install `react-native-nitro-modules` and `react-native-nitro-image`; for frame processors add `react-native-worklets` + `react-native-vision-camera-worklets`; run `pod install`.
- Frame processor silently does nothing → check both worklets packages are installed. v5 does not fall back to `worklets-core`.
- Camera is black on Android emulator → emulators rarely emulate all formats; test on a real device before debugging constraints.
- Immediate dealloc / crash when navigating away → you're unmounting the Camera. Mount it once and toggle `isActive`.
- Trying to set `format={...}` → that prop was removed. Use `constraints={[...]}`.
## Pointers
- Docs — Getting Started: https://visioncamera.margelo.com/docs
- Docs — Camera Outputs: https://visioncamera.margelo.com/docs/camera-outputs
- Worklets (Software Mansion): https://docs.swmansion.com/react-native-worklets/docs/
- Repo: https://github.com/mrousavy/react-native-vision-camera
- llms index: https://visioncamera.margelo.com/llms.txt / https://visioncamera.margelo.com/llms-full.txt
- Related: [migration-v4-to-v5.md](./migration-v4-to-v5.md), [outputs-and-constraints.md](./outputs-and-constraints.md), [frame-processors.md](./frame-processors.md), [capture-and-controls.md](./capture-and-controls.md)
SKILL.md
---
name: react-native-vision-camera
description: Best-practices guide for React Native VisionCamera v5 setup, migration, capture, controls, outputs, and basic frame processing. Use the separate react-native-vision-camera-realtime skill for production low-latency GPU, ML, CV, Skia or WebGPU pipelines and frame-coupled overlays.
---
# react-native-vision-camera (v5)
VisionCamera v5 is the maintained and latest version of `react-native-vision-camera`. It is a full Nitro Modules rewrite with a new **Constraints API**, **Output-based architecture**, **in-memory `Photo`**, and a hard break from the v4 format/prop model. Almost every v4 surface is gone or renamed — treat v5 as a new API, not an incremental upgrade.
This skill is a router. Read this file first, then load the reference that matches the task. Every reference is self-contained — do not load more than you need.
## When to load which reference
- **Production low-latency GPU, ML, CV, or frame-coupled overlay work:** use the separate `react-native-vision-camera-realtime` skill. Load the basic frame-processing reference too only when setup or API fundamentals are also needed.
- **New install, getting a Camera on screen, permissions, minimum boilerplate** → [references/quickstart-v5.md](references/quickstart-v5.md)
- **Porting a v4 codebase, understanding what changed** → [references/migration-v4-to-v5.md](references/migration-v4-to-v5.md) (load this FIRST when the user mentions v4, takePhoto, useCameraFormat, format prop, photo/video boolean props, or CodeScanner in core)
- **Porting a whole v4 *screen* — want a complete before/after file to transplant** → [references/migration-templates.md](references/migration-templates.md) (full copy-paste templates: photo screen, video screen, frame-processor+ML, barcode scanner, pro camera)
- **Choosing/attaching outputs, fps/HDR/resolution via constraints, session lifecycle** → [references/outputs-and-constraints.md](references/outputs-and-constraints.md)
- **Frame Processors, worklets, async frame work, pixel formats, writing a native plugin** → [references/frame-processors.md](references/frame-processors.md) (load this when user says "frame processor", "worklet", "ML on frames", "Nitro plugin", "vision-camera-plugin-*")
- **Capturing photos (incl. callbacks, RAW, HDR, preview image), recording video, Recorder lifecycle, manual AE/AF/AWB, exposure bias, zoom, focus** → [references/capture-and-controls.md](references/capture-and-controls.md)
- **Depth streaming, multi-cam, Skia preview, GPU resizer for ML, barcode scanner package, GPS location metadata, custom native outputs** → [references/advanced-features.md](references/advanced-features.md)
When in doubt, load [references/migration-v4-to-v5.md](references/migration-v4-to-v5.md) — it covers the shape of the new API by contrasting it with v4 and is the fastest orientation.
## Non-negotiable rules for v5 code
These are the rules that catch people who "know" v4. Apply them without asking:
1. **Install the Nitro peers.** `react-native-nitro-modules` and `react-native-nitro-image` are required peer deps. Frame processors additionally require `react-native-vision-camera-worklets` AND `react-native-worklets` (Software Mansion's — not `-core`). Worklets - https://docs.swmansion.com/react-native-worklets/docs/
2. **`outputs={[...]}` replaces `photo` / `video` / `frameProcessor` / `codeScanner` props.** Create outputs with `usePhotoOutput`, `useVideoOutput`, `useFrameOutput`, `useDepthOutput`, `useObjectOutput` (or `useBarcodeScannerOutput` from the barcode package) and pass them in an array. Capture methods (`capturePhoto`, `createRecorder`) live on the Output, not the Camera ref.
3. **There is no `format` prop and no `useCameraFormat`.** Use `constraints={[...]}` — array order = priority, descending. The Camera negotiates the closest supported config automatically, so an impossible constraint like `{ fps: 99999 }` never throws.
4. **`takePhoto()` does not exist.** Use `photoOutput.capturePhoto(settings, callbacks)` for in-memory `Photo`, or `photoOutput.capturePhotoToFile(...)` for a file path. The default path is in-memory — do not write temp files unless explicitly asked.
5. **Frame Processor plugins must be Nitro Modules.** The v4 `FrameProcessorPlugin` base class, `VISION_EXPORT_SWIFT_FRAME_PROCESSOR` macro, and `VisionCameraProxy.addFrameProcessorPlugin` are gone. A v5 plugin is a `HybridObject` with a typed Nitro spec. See [references/frame-processors.md](references/frame-processors.md).
6. **Every `Frame` (and `Depth`) MUST be `.dispose()`d.** The buffer pool is bounded; leaking a frame stalls the pipeline. Wrap work in `try { ... } finally { frame.dispose() }`. When offloading via `asyncRunner.runAsync(...)`, dispose inside the async callback if it returned `true`, and dispose immediately in the `else` branch when it returned `false`.
7. **CodeScanner is not in core.** `react-native-vision-camera-barcode-scanner` is a separate package, MLKit-based on both platforms. For iOS-only object detection (QR, faces, bodies via native AVFoundation metadata, no ML dep), use `useObjectOutput` from core.
8. **Keep the Camera mounted; toggle `isActive`.** Remounting tears down the session. Integrate with `useIsFocused()` from react-navigation so the session goes Idle → Ready while not on screen, and keeps preferences warm for fast resume.
9. **Frame output `pixelFormat` defaults to `'native'` (zero-copy), NOT `'yuv'`.** `'native'` streams in the session's negotiated `nativePixelFormat` with zero conversions (it may resolve to a YUV, RGB, RAW, or `'private'` format; verify the actual one via `frame.pixelFormat`). `'yuv'` picks the YUV format closest to native and is the best general-purpose CPU-accessible choice (MLKit/OpenCV/Skia); `'rgb'` forces a YUV-to-RGB conversion with about 2.6 times more bandwidth, so use it only when a consumer hard-requires RGB. `useDepthOutput` has **no** `pixelFormat` option. For ML consumers that require CPU-visible RGB or tensor input, prefer `react-native-vision-camera-resizer` over paying a per-frame RGB conversion in the Camera pipeline.
<!-- source: useFrameOutput.ts:123 (`pixelFormat = 'native'` default); VideoPixelFormat.ts:52-62; CameraFrameOutput.nitro.ts:71,84-86 ("recommended to use 'native' ... zero-copy GPU-only path"); useDepthOutput.ts:70-77 (options have no pixelFormat) -->
10. **Do not hand-clamp FPS/resolution with `Math.min/Math.max`.** That was a v4 workaround. In v5 the Constraints API negotiates internally — express intent and let the Camera pick.
11. **Worklets mutate Reanimated SharedValues directly in v5.** This is suitable for ordinary asynchronous UI or animation state. For frame-locked overlays, use the separate real-time skill and draw from the same frame with Skia or WebGPU.
## Operating rules for this skill
- Never invent v4→v5 API shapes. If a v4 API has no documented v5 equivalent in the references, say so and link to [the v5 docs](https://visioncamera.margelo.com) — do not guess.
- Do not add documentation files (README, CHANGELOG) unless the user asks.
- Assume the user is on v5 unless they show v4 code. If they show v4 code, load [references/migration-v4-to-v5.md](references/migration-v4-to-v5.md) before writing anything.
- When writing a new Camera example, default to the hook-based declarative form (`useCameraPermission` + `useCameraDevice` + `usePhotoOutput` + `<Camera />`). Use the imperative `VisionCamera.createCameraSession(...)` API only when the user asks for multi-cam or full programmatic control.
- For a basic ML path whose consumer needs CPU-visible input, recommend `react-native-vision-camera-resizer` over the v4-era `vision-camera-resize-plugin`. Route latency-critical ML, GPU inference, and live overlays to `react-native-vision-camera-realtime` instead of prescribing the Resizer universally.
<!-- source: react-native-vision-camera-resizer/src/specs/GPUFrame.nitro.ts + Resizer.nitro.ts (GPU resize→GPUFrame). The often-quoted "~5×" figure is a blog claim, not in source, so it is omitted here. -->
- Verify peer dependency installs. A user reporting a native crash after install 95% of the time has missed `react-native-nitro-modules`, `react-native-nitro-image`, or (for frame processors) `react-native-worklets` + `react-native-vision-camera-worklets`.
## Authoritative links
- Docs: https://visioncamera.margelo.com
- `llms.txt` index: https://visioncamera.margelo.com/llms.txt
- V5 release notes (includes migration snippets): `gh api repos/mrousavy/react-native-vision-camera/releases/tags/v5.0.0`
- Blog announcement: https://blog.margelo.com/whats-new-in-visioncamera-v5
- Main repo: https://github.com/mrousavy/react-native-vision-camera
- V4 snapshot (archived docs): https://visioncamera4.margelo.com