flutter_native_vision_camera 0.0.6
flutter_native_vision_camera: ^0.0.6 copied to clipboard
High-performance Flutter FFI camera plugin with zero-copy preview textures, integrated MLKit/Vision barcode scanning, and low-latency native frame access for real-time on-device vision.
Flutter Native Vision Camera #
A high-performance, FFI-powered camera plugin for Flutter, built for real-time on-device vision: direct, low-overhead access to camera frames for your own ML/CV code, integrated barcode/QR scanning, and full hardware control.
Status: Active development (
0.0.x) — the API may change before1.0.0. Device-verified on Android (Pixel 8) and iOS 18: preview, photo, video recording, barcode/QR scanning, zoom/torch/focus, orientation, and mirroring.
Demo #
| Preview + orientation | Barcode / QR scanning | FFI frame processor |
|---|---|---|
![]() |
![]() |
![]() |
Why use this instead of camera? #
The official camera package is excellent for
general capture. This package targets the niche it doesn't cover: a real-time
frame pipeline where you read raw camera buffers for your own vision code,
with integrated scanning, all sharing one GPU-texture preview.
| Feature | camera (standard) |
flutter_native_vision_camera |
|---|---|---|
| Preview | Platform texture | GPU texture (Android zero-copy SurfaceProducer; iOS CVPixelBuffer) |
| Frame access | startImageStream over the platform channel (serialized) |
Direct FFI pointer access to plane buffers — no channel serialization |
| Native frame hook | — | Synchronous C/C++ plugin on the camera thread (zero-latency) |
| Barcodes / QR | Separate plugin | Integrated (Android MLKit, iOS Vision) |
Features #
- GPU-texture preview with automatic orientation.
- FFI frame access — read raw Y/U/V (Android) or BGRA (iOS) planes directly.
- Synchronous native C/C++ plugins invoked on the camera thread.
- Integrated barcode/QR scanning (Android MLKit, iOS Vision).
- Hardware controls — zoom, torch, exposure, tap-to-focus, video recording.
Requirements #
- Flutter ≥ 3.22, Dart ≥ 3.8
- iOS 13.0+, Android
minSdk21+ - A physical device — simulators/emulators have no real camera.
Getting Started #
Install #
flutter pub add flutter_native_vision_camera
Permissions #
iOS — add to ios/Runner/Info.plist:
<key>NSCameraUsageDescription</key>
<string>This app needs camera access to capture photos and video.</string>
<key>NSMicrophoneUsageDescription</key>
<string>This app needs microphone access to record video with audio.</string>
Android — CAMERA and RECORD_AUDIO are declared by the plugin; request
them at runtime (shown below).
Basic usage — preview #
import 'package:flutter/material.dart';
import 'package:flutter_native_vision_camera/flutter_native_vision_camera.dart';
// Once, at startup (e.g. in main()):
initializeVisionCamera();
class CameraScreen extends StatefulWidget {
const CameraScreen({super.key});
@override
State<CameraScreen> createState() => _CameraScreenState();
}
class _CameraScreenState extends State<CameraScreen> {
final controller = CameraController();
@override
void initState() {
super.initState();
_start();
}
Future<void> _start() async {
// 0. Permission (otherwise you get a black preview).
if (await CameraPermissions.requestCameraPermission() !=
PermissionStatus.granted) {
return;
}
// 1. Pick a device.
final devices = await CameraDevices.getAvailableCameraDevices();
final back =
CameraDevices.getCameraDevice(devices, CameraPosition.back) ??
devices.first;
// 2. Initialize + start.
await controller.initialize(back, enablePhoto: true);
await controller.setActive(true);
if (mounted) setState(() {});
}
@override
void dispose() {
controller.dispose(); // Release the hardware.
super.dispose();
}
@override
Widget build(BuildContext context) =>
CameraPreview(controller: controller, resizeMode: ResizeMode.cover);
}
Take a photo #
takePhoto() requires enablePhoto: true at init.
final photo = await controller.takePhoto();
Image.file(File(photo.path)); // photo.width / photo.height / photo.path
Record video #
Initialize with enableVideo: true, then:
final dir = await getTemporaryDirectory();
await controller.startRecording('${dir.path}/clip.mp4');
// ... seamlessly zoom / toggle torch while recording ...
await controller.stopRecording();
The
mirrorflag oninitializecontrols the front-camera selfie mirror for both the preview and the saved photo/video (defaulttrue; setfalseto save what the camera actually sees).
Scan barcodes / QR codes #
await controller.initialize(
device,
codeScanner: CodeScannerConfiguration(
codeTypes: [CodeType.qr, CodeType.ean13, CodeType.code128],
),
);
controller.onCodeScanned.listen((codes) {
for (final code in codes) debugPrint('${code.type}: ${code.value}');
});
Lifecycle management #
Release the hardware on dispose(), and pause/resume with the app lifecycle:
class _S extends State<MyCam> with WidgetsBindingObserver {
@override
void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); }
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
controller.dispose();
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (!controller.isInitialized) return;
if (state == AppLifecycleState.inactive ||
state == AppLifecycleState.paused) {
controller.setActive(false);
} else if (state == AppLifecycleState.resumed) {
controller.setActive(true);
}
}
}
Frame Processors #
Run code for every camera frame — the package's headline feature.
Threading model — read this #
- Dart frame callback: delivered asynchronously on the main isolate's
event loop (via
dart:ffiNativeCallable.listener). It does not run on a background isolate and does not block the camera thread. Keep the work light, or copy data out and hand it to your own isolate. (A true off-isolate worklet model is on the roadmap.) - Native C/C++ hook: runs synchronously on the camera thread with zero added latency — use this for the heaviest work.
await controller.setFrameProcessor((frame) {
// FFI hot path: read pixels here for ML/CV.
final yPlane = frame.getPlaneData(0); // Uint8List view
final stride = frame.planeBytesPerRow(0); // bytes per row (>= width)
final avgLuma = frame.computeLuminance(0, 0, frame.width, frame.height);
});
getPlaneData(i)returns a direct view of native memory. UseplaneBytesPerRow(i)/planePixelStride(i)to walk it correctly (rows are padded).computeLuminanceis YUV/Android-only — on iOS (BGRA) it returns0.0; readgetPlaneData(0)instead.
Keeping a frame past the callback #
The Frame and its buffers are valid only during the callback. To use the
data later (e.g. on another isolate), either copy it out synchronously:
final bytes = Uint8List.fromList(frame.getPlaneData(0)); // owns a copy
…or extend the native lifetime by balancing the ref-count exactly once:
frame.incrementRefCount(); // keep the buffer alive
// ... use frame asynchronously ...
frame.decrementRefCount(); // release it (mandatory)
Drawing ML detection boxes on the preview #
Detection boxes are in the frame's coordinate space; the preview is rotated (and
maybe mirrored) for display, and the two are tracked independently — so a box
drawn naively drifts as the device turns. Map it with previewRectFromFrame:
final previewRect = controller.previewRectFromFrame(
detection.boundingBox, // normalized 0..1 from your model
sourceRotationDegrees: frame.orientation.degrees, // rotation you fed the model
);
// Scale previewRect onto the preview widget's rect (use displayPreviewSize for BoxFit).
It accounts for the live previewRotation and the front-camera mirror, so boxes
stay aligned at any orientation. See the example's Object Detector page for a
full EfficientDet-Lite0 overlay.
High-performance native C/C++ plugin #
For the heaviest work, hook the synchronous frame loop in C++ (src/VisionCamera.hpp):
#include "VisionCamera.hpp"
class BrightnessPlugin : public vision::Plugin {
public:
const char* name() const override { return "brightness"; }
void onFrame(const vision::Frame& frame) override {
const uint8_t* y = frame.data(); // Y / first plane
int stride = frame.bytesPerRow();
// ...heavy SIMD/AI math on the camera thread...
}
};
// Register once (e.g. from an init function you call via FFI at startup):
extern "C" __attribute__((visibility("default"))) void registerMyPlugins() {
vision::Registry::instance().addPlugin(std::make_shared<BrightnessPlugin>());
}
Put your .cpp alongside the plugin sources; it links via CMake on Android and
the podspec on iOS. See src/VisionCamera_NativePluginExample.cpp for a full
working example.
Platform Support #
| Capability | Android | iOS |
|---|---|---|
| Preview (GPU texture) | ✅ | ✅ |
| Photo capture | ✅ | ✅ |
| Video recording (+ audio) | ✅ CameraX | ✅ AVAssetWriter |
| Barcode / QR scanning | ✅ MLKit | ✅ Vision |
| Frame processor (Dart, main isolate) | ✅ YUV planes | ✅ BGRA |
| Native C/C++ plugin (camera thread) | ✅ | ✅ |
| Zoom / torch / exposure / tap-focus | ✅ | ✅ |
| Manual focus distance | ⬜ | ✅ |
takeSnapshot |
⬜ | ✅ |
regionOfInterest (scanning) |
⬜ | ✅ |
| Minimum OS | minSdk 21 |
iOS 13.0 |
Legend: ✅ supported · ⬜ not implemented yet. Symbology coverage differs slightly between MLKit (Android) and Vision (iOS).
Limitations & Roadmap #
- No web / desktop — Android + iOS only.
- Frame processor runs on the main isolate today; a true off-isolate worklet model is planned.
- Recording options (
RecordVideoOptions, codec/HDR) and pause/resume are not yet wired on all platforms;startRecordingtakes a path string. - Mirror is set at
initializetime (no runtime toggle yet). - Controls (
setZoom/setTorch/etc.) are no-ops untilsetActive(true). - No RAW capture / multi-camera;
takeSnapshotis iOS-only. - Planned API polish: typed
CameraException,TorchModeenum,switchCamera, richer error reporting.
Credits & Attribution #
Inspired by react-native-vision-camera
by Marc Rousavy — bringing the same high-performance,
low-level camera philosophy to Flutter via synchronous FFI and Texture rendering.
License #
MIT


