What's New in VisionCamera V5?
React Native's best Camera library just got an update - and it's a big one!
V5 is a new foundation for VisionCamera: rewritten to Nitro Modules, redesigned around a new Constraints API, and flexible enough to unlock workflows that were awkward or impossible in V4.
That new foundation shows up everywhere: in-memory Photo objects, depth streaming, RAW capture, Multi-Cam sessions, modular plugins, and a much cleaner path for custom native integrations.
Why V5 matters
VisionCamera V5 replaces most of the hand-written JSI/C++ layer with Nitro, making the core runtime faster, safer, and much easier to evolve.
The new Constraints API lets the Camera negotiate supported configurations instead of making developers guess which feature combinations will actually work.
Photos, outputs, sessions, and plugins are now modeled in a way that unlocks in-memory workflows, depth data, RAW capture, and advanced multi-camera setups.
V5 also ships with a new documentation site and a more modular package structure, so advanced features no longer have to live in one monolithic package.
Full rewrite to Nitro
VisionCamera V5 has been fully rewritten to Nitro Modules after years of relying on hand-written JSI/C++ code for Frame Processors, dealing with limitations of the Native Module Callback system, and working around unsafe typing.
With Nitro, the implementation is now fully type-safe, faster, and flexible enough to enable entirely new workflows for developers.
Nitro's five-year path
Nitro Modules is a module system I (@mrousavy) built as an alternative to Turbo- and Expo Modules. What started with VisionCamera back in 2021 turned into a five-year journey before we finally migrated our React Native library ecosystem over to Nitro. Today, we have written more than 30 libraries using Nitro, including react-native-mmkv, react-native-video, and react-native-unistyles. VisionCamera V5 now finally marks the major success of this experiment, and we're excited to have pushed through with this for such a long time.
Better performance, everywhere
Nitro's purpose is simple: write native code as if it were a JS class. This implies that the JS layer automatically communicates more with the native layer, allowing for a much more elegant API design.
Previously (in V4 and below) only a few methods like getAllCameraDevices() and getCameraPermissionStatus() were exposed to JS, while the rest was declarative through props on <Camera />. In VisionCamera V5, there is much more back-and-forth communication between JS and native. For example, a CameraDevice is now a HybridObject and doesn't contain any values by itself. Only when a property is accessed (such as CameraDevice.hasFlash) a native call will be made using Nitro, which is ~15x faster than with Turbo-Modules, or ~60x faster than with Expo-Modules. This means we no longer need to serialize a large CameraDevice or CameraFormat object upfront. Each property is lazy, which improves startup performance and reduces overall camera latency.
No more hand-written JSI
In V5, the entire Frame Processor implementation is no longer built with hand-written JSI/C++ code. Almost all of it is now written in plain Swift and Kotlin, which reduced the codebase by ~3,000 LOC. While the custom JSI/C++ implementation has matured a lot over the past five years, there were still occasions where a race condition, a memory fault, or a threading violation caused crashes like SIGSEGV or SIGABRT. This has been entirely fixed thanks to Nitro.
The new Constraints API
Building a consistent Camera API across iOS and Android has always been difficult because the two platforms work fundamentally differently:
Apple knows their hardware, so they provide developers a full list of supported feature combinations upfront - this is called an
AVCaptureDevice.Format. AnAVCaptureDevicelists all of itsformats, which you - as a developer - can filter and sort based on your intent - for example, you might want to select a format wheredimensionsis 4K,isVideoHDRSupportedis true, andvideoSupportedFrameRateRangescontains 60 FPS - then simply set that as youractiveFormat: now you're running 4K + Video HDR + 60 FPS - easy peasy lemon squeezy.Android, on the other hand, is a more general-purpose operating system, and their Media APIs (here: Camera2) suffer from weak API design decisions. In contrast to iOS, there is no "list of formats" that developers can iterate - instead, the
CameraDeviceexposes individually supported features such asSCALER_STREAM_CONFIGURATION_MAP(resolutions/dimensions),REQUEST_AVAILABLE_DYNAMIC_RANGE_PROFILES(Video HDR) orCONTROL_AE_AVAILABLE_TARGET_FPS_RANGES(Frame Rate Ranges). This means, even if theCameraDevicesupports 4K, HDR, and 60 FPS, there is no clear indicator whether the device supports all of those features combined - your camera session might simply fail to start or crash.
Previously (in V4 and below), VisionCamera tried to expose a fake Formats list to JS (see V4: Camera Formats or V4: useCameraFormat(...)) on Android by stitching together known capabilities and limits mentioned in this insanely long matrix from the Android Developer documentation. This often meant that code like this:
TSX
function App() {
const device = ...
const format = useCameraFormat(device, [
{ fps: 60 },
{ videoHdr: true },
{ videoResolution: { width: 3840, height: 2160 } }
])
return (
<Camera
device="back"
format={format}
fps={Math.min(Math.max(format.minFps, 60), format.maxFps)}
videoHdr={format.supportsVideoHdr}
/>
)
}
...might've been giving you a format that had 60 FPS, Video HDR and 4K, but when the Camera was started, a different resolution, lower FPS, or SDR might've been chosen - and there was no way to find this out. Additionally, certain combinations, or certain phones with weird edge-cases, caused the Camera session to not start at all, which was even more problematic.
Also, individual features like fps, videoHdr, and more depended on the given format, which was weird DX as you'd have to respect the format when setting each prop, as you can see above we use Math.min(Math.max(...)) to ensure we don't exceed our format's FPS limit - otherwise the Camera would crash.
The new Constraints API solves this with an entirely different approach: the developer expresses intent via Constraints, which are then internally negotiated by the Camera to find a best-matching supported Camera Configuration. On iOS, the Constraint Resolver filters through the individual AVCaptureDevice.Format based on given Constraints, and on Android this probes given Constraints via CameraInfo.isSessionConfigSupported(...).
TSX
function App() {
return (
<Camera
device="back"
constraints={[
{ fps: 60 },
{ videoDynamicRange: CommonDynamicRanges.ANY_HDR }
]}
/>
)
}
constraints={...} not only finds a Camera Configuration where 60 FPS and HDR is supported, but also enables it - so it becomes your single source of truth for enabling Camera features that need to be negotiated with other features or outputs.
🧠
Constraints are prioritized by the order in the given constraints={...} array - here, the { fps: 60 } constraint is more important than the { videoDynamicRange: ... } constraint.
To find out which Camera Configuration has been resolved, use the onSessionConfigSelected={...} callback. Additionally, the resolveConstraints(...) method allows developers to manually resolve Constraints to a valid Camera Configuration without starting a Camera.
Since the Formats API has been completely removed in V5, individual Camera Device features are now exposed upfront on the CameraDevice (such as CameraDevice.supportedFPSRanges or CameraDevice.getSupportedResolutions(...)).
More resolutions
The Camera's available resolutions depend heavily on the attached Camera Outputs - which was previously (in V4 or below) not respected by the Formats system. Thanks to the new Constraints API, you can now select higher resolutions if fewer Camera Outputs are attached, for example you can now capture 8K Photos on iOS.
