This package is based on libuvc.
It provides Android UVC camera access, including USB device handling,
preview streaming to a Flutter Texture, frame access from Dart, preview
transforms, stream diagnostics, and UVC camera controls.
- Android only
minSdk 24- Supported ABIs:
arm64-v8a,armeabi-v7a,x86_64
flutter pub add flutter_ffi_uvcThis package combines three layers:
- Android USB Host API — device discovery, USB permission, and acquiring the file descriptor for the connected device.
- libusb — wraps that file descriptor and handles the actual USB communication.
- libuvc — sits on top of libusb and handles the UVC protocol: mode negotiation, frame streaming, and camera controls.
- Call
uvcCamera.ensureCameraPermission()if your app requires theCAMERApermission. - Call
uvcCamera.listUsbDevices()to discover attached UVC cameras. - Call
uvcCamera.openUsbDevice(deviceId)to request USB permission and open the device. - Read
uvcCamera.supportedModes(). - Pick a mode and call
await uvcCamera.startPreview(mode)— starts the stream and verifies frame delivery. - On success, attach a Flutter
TextureviaattachPreviewTexturefor live preview on Android. - Use
copyLatestFrame()when you need frame bytes in Dart, such as for capture or inspection. - Call
uvcCamera.stopPreview()when preview is no longer needed. - When finished, call
uvcCamera.closeUsbDevice().
This plugin is designed around a single, shared global uvcCamera instance. It supports one connected camera at a time:
import 'package:flutter_ffi_uvc/flutter_ffi_uvc.dart';
class UvcPreviewPage extends StatefulWidget {
UvcPreviewPage({
super.key,
UvcCamera? camera,
}) : camera = camera ?? uvcCamera;
final UvcCamera camera;
}// List attached UVC cameras
final List<UvcUsbDevice> devices = await uvcCamera.listUsbDevices();
// Open a device — requests USB permission if not already granted
final int result = await uvcCamera.openUsbDevice(devices.first.deviceId);
if (result != 0) {
print('Open failed: ${uvcCamera.lastError}');
}openUsbDevice goes through the Android USB layer to acquire permission and a file
descriptor, then passes it to libusb to open the session. It throws a
PlatformException if the Android layer fails, and returns a non-zero code if
libusb/libuvc fails to initialize.
To close and release the USB connection:
await uvcCamera.closeUsbDevice();If your app manages USB access independently, pass the file descriptor directly to skip the Android layer:
// fd: int from UsbDeviceConnection.fileDescriptor
uvcCamera.openFd(fd);Create a texture, start preview, then attach the texture once the stream is confirmed running:
final int textureId = await uvcCamera.createPreviewTexture();
// stableFrames (default): verifies both frame delivery and frame validity.
// sequenceOnly: verifies frame delivery only — frame validity is not checked.
final UvcPreviewStartResult result = await uvcCamera.startPreview(
mode,
policy: UvcPreviewPolicy.stableFrames,
);
if (result.success) {
await uvcCamera.attachPreviewTexture(
textureId,
width: mode.width,
height: mode.height,
);
}Display it with Flutter's Texture widget:
AspectRatio(
aspectRatio: mode.width / mode.height,
child: Texture(textureId: textureId),
)On teardown:
uvcCamera.stopPreview();
await uvcCamera.disposePreviewTexture(textureId);Rotation and flip are applied to the Flutter Texture output only.
// Absolute: set rotation and flip in one call
uvcCamera.setPreviewTransform(
const UvcPreviewTransform(rotation: 90, flipHorizontal: true),
);
// Incremental helpers
uvcCamera.rotatePreviewClockwise(); // +90° each call
uvcCamera.rotatePreviewCounterClockwise(); // -90° each call
uvcCamera.togglePreviewFlipHorizontal(); // mirror left-right
uvcCamera.togglePreviewFlipVertical(); // mirror top-bottom
// Read current state
final UvcPreviewTransform t = uvcCamera.previewTransform;rotation accepts 0, 90, 180, or 270 (clockwise degrees). Values
outside this set are normalised to 0 by the native layer.
For 90° and 270° rotations the output dimensions are swapped. Use
applyToSize() to get the correct dimensions for the AspectRatio widget:
final (int w, int h) = uvcCamera.previewTransform.applyToSize(mode.width, mode.height);
AspectRatio(
aspectRatio: w / h,
child: Texture(textureId: textureId),
)To get frame bytes in Dart — call copyLatestFrame() while preview is running:
final UvcPreviewFrame? frame = uvcCamera.copyLatestFrame();
if (frame != null) {
// frame.rgbaBytes: RGBA pixel data (width * height * 4 bytes)
// frame.width, frame.height: frame dimensions
}To capture with the current preview transform applied:
final UvcPreviewFrame? frame = uvcCamera.copyLatestFrameTransformed(
uvcCamera.previewTransform,
);frame.width and frame.height reflect the post-transform dimensions.
uvcCamera.isPreviewing returns true while the native stream callback is
active — that is, after a successful startPreview() and before stopPreview()
or device close. Use it to guard UI state or skip work when preview is not
running.
When the native callback is already processing a frame, incoming callbacks are dropped rather than queued.
Dropped callbacks are visible at UvcLogLevel.trace:
dropping frame callback because previous callback is still processing
Use getStreamStats() to read cumulative native stats for the current preview
session, including input/delivered FPS, decode failures, dropped frames,
inter-frame gap timing, and first-frame latency.
Stats reset when a new startPreview() session begins.
Frame pipeline errors — decode failures, undersized frames, buffer allocation
failures — are delivered proactively via streamErrors rather than being
silently stored in lastError.
Subscribe once when the widget is initialised and cancel on dispose:
StreamSubscription<UvcStreamError>? _streamErrorSub;
@override
void initState() {
super.initState();
_streamErrorSub = uvcCamera.streamErrors.listen((UvcStreamError error) {
// handle error, e.g. show a SnackBar
print(error.message);
});
}
@override
void dispose() {
_streamErrorSub?.cancel();
super.dispose();
}streamErrors is a broadcast stream, so multiple subscribers are allowed.
Errors are only emitted while a native error listener is active.
supportedControls() returns the controls exposed by the currently opened
device, including min/max/default/current values. getControl(...) and
setControl(...) use typed UvcControlId values instead of raw integer IDs.
For device debugging, debugBmControls() returns the controls advertised by
descriptor bmControls without GET_CUR probing. This is useful when a device
reports a control bit but rejects or mishandles GET_CUR.
Control labels are for display only. Use UvcControlId to identify controls in code:
final int? autoFocus = uvcCamera.getControl(UvcControlId.focusAuto);
await Future<void>.delayed(const Duration(milliseconds: 100));
uvcCamera.setControl(UvcControlId.focusAuto, autoFocus == 0 ? 1 : 0);Compound UVC controls are exposed as typed APIs instead of a single integer:
final UvcPanTiltAbsoluteControl? panTilt =
uvcCamera.getPanTiltAbsoluteControl();
if (panTilt != null) {
uvcCamera.setPanTiltAbsoluteControl(
UvcPanTiltAbsoluteControl(
pan: panTilt.pan + 10,
tilt: panTilt.tilt,
),
);
}You can change the log level for the underlying libuvc layer at runtime:
uvcCamera.setLogLevel(UvcLogLevel.warn);Available levels are:
UvcLogLevel.errorUvcLogLevel.warnUvcLogLevel.infoUvcLogLevel.debugUvcLogLevel.trace
If you do not call uvcCamera.setLogLevel(...), the package defaults to UvcLogLevel.info.
The bundled example app demonstrates:
- USB device discovery and permission handling
- Preview rendering via Flutter
Texture - Basic camera control interactions
Most users will interact with these classes:
UvcCameraUvcStreamErrorUvcUsbDeviceUvcCameraModeUvcPreviewFrameUvcPreviewStartResultUvcPreviewPolicyUvcPreviewTransformUvcCameraControlUvcControlIdUvcControlKind
Useful debugging classes:
UvcStreamStatsUvcLogLevelUvcBmControlInfo
For upcoming work areas and current planning direction, see ROADMAP.md.
This package is licensed under the BSD 3-Clause License. Bundled third-party components keep their own licenses.
See THIRD_PARTY_NOTICES.md for bundled dependency
license notices, including libuvc, libusb, and libjpeg-turbo.
