miniav 0.5.2
miniav: ^0.5.2 copied to clipboard
A cross-platform capture library for audio and video providing a standard buffer with GPU path
miniav #
A Flutter library for cross-platform audio, video, and input capture with high-performance buffer management and GPU integration support.
Try it out at miniav.practicalxr.com!
Three Things to Know #
-
Native assets compilation can take time, especially on first build. Run with -v to see build progress and errors.
-
This package uses dart native assets. For flutter, you must be on the master channel and run
flutter config --enable-native-assetsFor dart, each run must contain the--enable-experiment=native-assetsflag. -
Platform-specific permissions are required for camera, microphone, and screen capture. See the Permissions section below for detailed setup instructions.
Platform Support #
| Module | Windows | Linux | macOS | Web | Android | iOS |
|---|---|---|---|---|---|---|
| Camera | ✅ | ✅ | ✅ | ✅ | 🚧 | 🚧 |
| Screen Capture | ✅ | ✅ | ✅ | ✅ | 🚧 | 🚧 |
| Audio Input | ✅ | ✅ | ✅ | ✅ | 🚧 | 🚧 |
| Audio Loopback | ✅ | ✅ | ✅ 15+ | ❌ | ❌ | ❌ |
| Input Capture | ✅ | 🚧 | 🚧 | 🚧 | ❌ | ❌ |
Legend: ✅ Supported • ❌ Not Available • 🚧 Planned
(Maybe) Planned Features #
- Android/iOS: Full support on mobile platforms
- GPU Interop: Helpers to easily manage handles and shared fences for GPU processing
- Permission Management: Simplified APIs for handling platform-specific permissions
- macOS/Linux context-lost wiring: Per-context device-loss callbacks for non-Windows platforms (currently handled via the polling watcher)
Installation #
Add the following to your pubspec.yaml:
dependencies:
miniav: ^0.5.0
Then run:
dart pub get
Getting Started #
git clone https://github.com/practicalxr/miniav.git
cd miniav_ffi
dart --enable-experiment=native-assets test
dart:
cd miniav
dart --enable-experiment=native-assets example/miniav_example.dart
flutter:
cd miniav/example/flutter_example
flutter config --enable-native-assets
flutter run -d chrome/windows/linux
Example #
import 'package:miniav/miniav.dart';
Future<void> captureCamera() async {
// Initialize MiniAV
MiniAV.setLogLevel(MiniAVLogLevel.info);
// Enumerate camera devices
final cameras = await MiniCamera.enumerateDevices();
if (cameras.isEmpty) {
print('No cameras found');
return;
}
// Use first camera
final selectedCamera = cameras.first;
final format = await MiniCamera.getDefaultFormat(selectedCamera.deviceId);
print('Using camera: ${selectedCamera.name}');
print('Format: ${format.width}x${format.height} @ ${format.frameRateNumerator}/${format.frameRateDenominator} FPS');
// Create and configure context
final context = await MiniCamera.createContext();
await context.configure(selectedCamera.deviceId, format);
// Start capture with callback
int frameCount = 0;
await context.startCapture((buffer, userData) {
frameCount++;
print('Camera frame #$frameCount - ${buffer.dataSizeBytes} bytes');
// Process video data here
if (buffer.type == MiniAVBufferType.video) {
final videoBuffer = buffer.data as MiniAVVideoBuffer;
final rawData = videoBuffer.planes[0];
// Use raw pixel data for computer vision, GPU upload, etc.
}
// IMPORTANT: Release buffer when done
MiniAV.releaseBuffer(buffer);
});
// Capture for 10 seconds
await Future.delayed(Duration(seconds: 10));
// Stop and cleanup
await context.stopCapture();
await context.destroy();
MiniAV.dispose();
}
Features #
Multi-Stream Capture #
MiniAV supports simultaneous capture from multiple sources:
- Camera: Access webcams and external cameras
- Screen: Capture displays and windows with optional audio
- Audio Input: Record from microphones and audio devices
- Loopback Audio: Capture system audio output (platform dependent)
- Input Capture: Keyboard, mouse, and gamepad events with configurable throttling
Device Change Subscriptions #
Subscribe to device add/remove events without polling. Each subscription returns a disposer function — call it to unsubscribe.
// Camera devices
final cancel = MiniCamera.addDeviceChangeListener((notification) {
print('Camera ${notification.event.name}: ${notification.device.name}');
});
// Microphone devices
final cancel = MiniAudioInput.addDeviceChangeListener((notification) {
print('Mic ${notification.event.name}: ${notification.device.name}');
});
// Loopback (audio output) targets
final cancel = MiniLoopback.addDeviceChangeListener((notification) {
print('Loopback target ${notification.event.name}: ${notification.device.name}');
});
// Display monitors
final cancel = MiniScreen.addDisplayChangeListener((notification) {
print('Display ${notification.event.name}: ${notification.device.name}');
});
// Windows (visible windows list)
final cancel = MiniScreen.addWindowChangeListener((notification) {
print('Window ${notification.event.name}: ${notification.device.name}');
});
// Gamepads
final cancel = MiniInput.addGamepadChangeListener((notification) {
print('Gamepad ${notification.event.name}: ${notification.device.name}');
});
// notification.event is a MiniAVDeviceChangeEvent:
// .added, .removed, .defaultChanged
Multiple listeners on the same module are independent; disposing one does not affect others.
Context-Lost Notifications #
For contexts that are actively capturing, you can subscribe to a notification when the underlying device becomes unavailable (e.g. a webcam is unplugged, an audio endpoint is removed, or a captured window is closed).
final context = await MiniCamera.createContext();
await context.configure(deviceId, format);
final cancelLost = context.addLostListener((reason) {
// Fired from the capture thread — schedule UI work on the main isolate
print('Device lost (code $reason), stopping capture...');
});
await context.startCapture((buffer, _) {
MiniAV.releaseBuffer(buffer);
});
// ...later
cancelLost(); // unsubscribe
await context.stopCapture();
await context.destroy();
addLostListener is available on MiniCameraContext, MiniAudioInputContext, MiniLoopbackContext, and MiniScreenContext.
Important: The lost callback is fired from a native capture thread. Do not call
context.destroy()synchronously inside the callback — schedule it on the main isolate instead.
High-Performance Buffers #
- Zero-Copy Design: Direct access to native buffers where possible
- GPU Integration: Ready for use with compute shaders and WebGPU
- Explicit Release: Manual buffer management prevents resource leaks
- Multiple Formats: Support for RGB, YUV, and compressed formats
Cross-Platform APIs #
// Camera capture
final cameras = await MiniCamera.enumerateDevices();
final context = await MiniCamera.createContext();
final cancelCamera = MiniCamera.addDeviceChangeListener((n) { /* ... */ });
// Screen capture with audio
final displays = await MiniScreen.enumerateDisplays();
final screenContext = await MiniScreen.createContext();
final cancelDisplay = MiniScreen.addDisplayChangeListener((n) { /* ... */ });
final cancelWindow = MiniScreen.addWindowChangeListener((n) { /* ... */ });
// Audio input
final audioDevices = await MiniAudioInput.enumerateDevices();
final audioContext = await MiniAudioInput.createContext();
final cancelAudio = MiniAudioInput.addDeviceChangeListener((n) { /* ... */ });
// System audio loopback (where supported)
final loopbackDevices = await MiniLoopback.enumerateDevices();
final loopbackContext = await MiniLoopback.createContext();
final cancelLoopback = MiniLoopback.addDeviceChangeListener((n) { /* ... */ });
// Input capture (keyboard, mouse, gamepad)
final gamepads = await MiniInput.enumerateGamepads();
final inputContext = await MiniInput.createContext();
final cancelGamepad = MiniInput.addGamepadChangeListener((n) { /* ... */ });
Input Capture #
MiniAV includes a unified input capture module for keyboard, mouse, and gamepad events.
Basic Input Capture #
import 'package:miniav/miniav.dart';
Future<void> captureInput() async {
// Create and configure an input context
final context = await MiniInput.createContext();
await context.configure(MiniAVInputConfig(
inputTypes: MiniAVInputType.keyboard.value |
MiniAVInputType.mouse.value |
MiniAVInputType.gamepad.value,
mouseThrottleHz: 120, // Limit mouse events to 120 Hz
gamepadPollHz: 60, // Poll gamepads at 60 Hz
));
// Start capture with per-type callbacks
await context.startCapture(
onKeyboard: (event, userData) {
final action = event.action == MiniAVKeyAction.down ? 'DOWN' : 'UP';
print('Key $action: keyCode=${event.keyCode} scanCode=${event.scanCode}');
},
onMouse: (event, userData) {
print('Mouse ${event.action.name}: (${event.x}, ${event.y})');
},
onGamepad: (event, userData) {
print('Gamepad ${event.gamepadIndex}: '
'buttons=0x${event.buttons.toRadixString(16)} '
'LStick=(${event.leftStickX}, ${event.leftStickY}) '
'triggers=(${event.leftTrigger}, ${event.rightTrigger})');
},
);
// Capture for 10 seconds
await Future.delayed(Duration(seconds: 10));
// Cleanup
await context.stopCapture();
await context.destroy();
}
Gamepad Enumeration #
final gamepads = await MiniInput.enumerateGamepads();
for (final pad in gamepads) {
print('Gamepad: ${pad.name} (${pad.deviceId})');
}
Input Types #
Input types are configured as a bitmask, so you can capture any combination:
// Keyboard only
MiniAVInputConfig(inputTypes: MiniAVInputType.keyboard.value);
// Mouse + gamepad
MiniAVInputConfig(
inputTypes: MiniAVInputType.mouse.value | MiniAVInputType.gamepad.value,
);
Permissions #
macOS #
Add to macos/Runner/Info.plist:
<key>NSCameraUsageDescription</key>
<string>This app uses the camera for video capture</string>
<key>NSMicrophoneUsageDescription</key>
<string>This app uses the microphone for audio recording</string>
For screen recording, manually enable in System Preferences > Security & Privacy > Privacy > Screen Recording.
Windows #
Camera and microphone access controlled via Windows 10+ Privacy Settings. Screen capture generally requires no special permissions for desktop applications. Input capture (keyboard/mouse hooks, XInput gamepads) works without additional permissions.
Linux #
User must be in video and audio groups:
sudo usermod -a -G audio,video $USER
Install required development packages:
# Ubuntu/Debian
sudo apt install libasound2-dev libpulse-dev libpipewire-0.3-dev libv4l-dev
# Fedora
sudo dnf install alsa-lib-devel pulseaudio-libs-devel pipewire-devel libv4l-devel
Web #
Requires HTTPS for camera, microphone, and screen capture APIs. All capture requires user gesture and permission.
Architecture #
MiniAV follows a modular architecture:
- miniav_platform_interface: Abstract interface definitions
- miniav_ffi: Native implementation using FFI and C library
- miniav_web: Web implementation using browser APIs
- miniav_c: Core C library with platform-specific backends
Buffer Management #
MiniAV uses explicit buffer release for optimal performance:
await context.startCapture((buffer, userData) {
// Process buffer data
final videoData = buffer.data as MiniAVVideoBuffer;
// Access raw pixel planes
final plane0 = videoData.planes[0]; // Y plane for YUV, or RGB data
final plane1 = videoData.planes[1]; // U plane for YUV
final plane2 = videoData.planes[2]; // V plane for YUV
// CRITICAL: Always release when done
MiniAV.releaseBuffer(buffer);
});
GPU Integration #
Buffers can contain GPU handles for zero-copy workflows:
if (buffer.contentType == MiniAVBufferContentType.gpuD3D11Handle) {
// Direct GPU texture handle (Windows)
final gpuHandle = videoData.planes[0];
// Pass to minigpu or other GPU library
}
GPU Interop with minigpu #
MiniAV is designed to feed directly into minigpu for zero-copy GPU processing — camera frames and screen captures can be imported as GPU textures and processed with custom WGSL compute shaders.
Buffer contract #
Every MiniAVVideoBuffer delivered in a capture callback carries the fields that minigpu's importVideoFrame needs:
| Field | Role |
|---|---|
contentType |
cpu = copy into GPU texture; gpuD3D11Handle = zero-copy shared texture (Windows) |
pixelFormat |
rgba32 or nv12 are supported by minigpu on all platforms |
width / height |
Frame dimensions in pixels |
planes[n] |
Raw Uint8List pixel data (CPU path) — one plane for RGBA, two for NV12 |
strideBytes[n] |
Row stride in bytes per plane |
nativeHandles[n] |
Platform GPU handle (D3D11 texture pointer, DMA-BUF fd, etc.) for GPU path |
nativeFence |
Sync fence for GPU-path handoff (D3D11 fence, sync_fd, or Metal shared event) |
Camera → GPU texture → RGBA readback (CPU path) #
import 'dart:ffi';
import 'dart:typed_data';
import 'package:ffi/ffi.dart';
import 'package:miniav/miniav.dart';
import 'package:minigpu/minigpu.dart';
Future<void> cameraToGpu() async {
final gpu = Minigpu();
await gpu.init();
MiniAV.setLogLevel(MiniAVLogLevel.warn);
final devices = await MiniCamera.enumerateDevices();
if (devices.isEmpty) return;
final fmt = await MiniCamera.getDefaultFormat(devices.first.deviceId);
final ctx = await MiniCamera.createContext();
await ctx.configure(devices.first.deviceId, fmt);
await ctx.startCapture((buffer, _) async {
if (buffer.type != MiniAVBufferType.video) {
MiniAV.releaseBuffer(buffer);
return;
}
final vb = buffer.data as MiniAVVideoBuffer;
// Copy the CPU plane onto the native heap for the duration of the GPU import.
final plane = vb.planes[0]!;
final ptr = malloc<Uint8>(plane.length);
ptr.asTypedList(plane.length).setAll(0, plane);
final tex = gpu.importVideoFrame(ExternalVideoBuffer(
contentType: ExternalContentType.cpu,
pixelFormat: ExternalPixelFormat.rgba32, // match fmt.pixelFormat
width: vb.width,
height: vb.height,
planes: [
ExternalPlane(
dataPtr: ptr.address,
width: vb.width,
height: vb.height,
strideBytes: vb.strideBytes[0],
),
],
));
if (tex != null) {
final out = tex.toRGBA();
// ... dispatch a compute shader or read back ...
out.destroy();
tex.destroy();
}
malloc.free(ptr);
MiniAV.releaseBuffer(buffer);
});
await Future.delayed(const Duration(seconds: 5));
await ctx.stopCapture();
await ctx.destroy();
}
Camera → custom compute shader (colour inversion example) #
const kInvertShader = '''
@group(0) @binding(0) var in_tex : texture_2d<f32>;
@group(0) @binding(1) var<storage, read_write> out_buf : array<u32>;
struct Params { width: u32, height: u32 }
@group(0) @binding(2) var<storage, read_write> params : Params;
@compute @workgroup_size(8, 8, 1)
fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
if (gid.x >= params.width || gid.y >= params.height) { return; }
let px = textureLoad(in_tex, vec2<u32>(gid.x, gid.y), 0);
let r = u32((1.0 - px.r) * 255.0);
let g = u32((1.0 - px.g) * 255.0);
let b = u32((1.0 - px.b) * 255.0);
out_buf[gid.y * params.width + gid.x] =
r | (g << 8u) | (b << 16u) | (255u << 24u);
}
''';
// In capture callback (after building tex as above):
final W = vb.width, H = vb.height;
final outBuf = gpu.createBuffer(W * H * 4, BufferDataType.uint8);
final paramsBuf = gpu.createBuffer(8, BufferDataType.uint32);
await paramsBuf.write(Uint32List.fromList([W, H]), 2, dataType: BufferDataType.uint32);
final cs = gpu.createComputeShader();
cs.loadKernelString(kInvertShader);
tex.setOnShader(cs, 0); // imported texture → binding 0
cs.setBufferAtSlot(1, outBuf); // output → binding 1
cs.setBufferAtSlot(2, paramsBuf); // params → binding 2
await cs.dispatch((W + 7) ~/ 8, (H + 7) ~/ 8, 1);
final result = Uint8List(W * H * 4);
await outBuf.read(result, W * H * 4, dataType: BufferDataType.uint8);
cs.destroy(); outBuf.destroy(); paramsBuf.destroy();
tex.destroy(); malloc.free(ptr);
NV12 camera frame → GPU BT.709 conversion #
Request NV12 output from the camera for more efficient GPU upload, then let minigpu's built-in toRGBA() apply BT.709 full-range conversion:
// Configure for NV12 output
final fmt = MiniAVVideoInfo(
width: 1280, height: 720,
pixelFormat: MiniAVPixelFormat.nv12,
frameRateNumerator: 30, frameRateDenominator: 1,
outputPreference: MiniAVOutputPreference.cpu,
);
await ctx.configure(deviceId, fmt);
// In the capture callback:
final yPlane = vb.planes[0]!; // width × height bytes
final uvPlane = vb.planes[1]!; // width × (height/2) bytes
final yPtr = malloc<Uint8>(yPlane.length)..asTypedList(yPlane.length).setAll(0, yPlane);
final uvPtr = malloc<Uint8>(uvPlane.length)..asTypedList(uvPlane.length).setAll(0, uvPlane);
final tex = gpu.importVideoFrame(ExternalVideoBuffer(
contentType: ExternalContentType.cpu,
pixelFormat: ExternalPixelFormat.nv12,
width: vb.width, height: vb.height,
planes: [
ExternalPlane(dataPtr: yPtr.address, width: vb.width, height: vb.height, strideBytes: vb.strideBytes[0]),
ExternalPlane(dataPtr: uvPtr.address, width: vb.width ~/ 2, height: vb.height ~/ 2, strideBytes: vb.strideBytes[1]),
],
));
final rgba = tex!.toRGBA(); // BT.709 NV12 → RGBA on GPU
// ... use rgba buffer ...
rgba.destroy(); tex.destroy();
malloc.free(yPtr); malloc.free(uvPtr);
Advanced Usage #
Multiple Stream Synchronization #
// Start multiple streams
await cameraContext.startCapture(onCameraFrame);
await audioContext.startCapture(onAudioFrame);
void synchronizeStreams(MiniAVBuffer cameraBuffer, MiniAVBuffer audioBuffer) {
final timeDiff = cameraBuffer.timestampUs - audioBuffer.timestampUs;
if (timeDiff.abs() < 16667) { // Within ~16ms for 60fps
// Process synchronized frame
}
}
Custom Format Selection #
final formats = await MiniCamera.getSupportedFormats(deviceId);
final preferredFormat = formats.firstWhere(
(f) => f.width >= 1920 && f.pixelFormat == MiniAVPixelFormat.nv12,
orElse: () => formats.first,
);
await context.configure(deviceId, preferredFormat);
Error Handling #
try {
await context.startCapture(callback);
} on MiniAVException catch (e) {
switch (e.code) {
case MiniAVResultCode.errorNotSupported:
// Handle unsupported operation
break;
case MiniAVResultCode.errorInvalidArg:
// Handle invalid parameters
break;
default:
print('Capture error: ${e.message}');
}
}
Performance Tips #
- Release Buffers Promptly: Delayed release can cause frame drops
- Use Appropriate Formats: Choose formats matching your processing needs
- Minimize Copies: Prefer direct buffer access over copying data
- GPU Preference: Set
outputPreference: gpufor zero-copy workflows - Background Processing: Move heavy processing off the capture callback thread
Dependencies #
Native Dependencies #
- Windows: Media Foundation, DirectX 11, WASAPI, XInput
- macOS: AVFoundation, Core Graphics, Core Audio
- Linux: PipeWire, PulseAudio, ALSA, V4L2
Build Dependencies #
- CMake 3.15+
- Platform-appropriate C++ compiler
- pkg-config (Linux)
Troubleshooting #
Common Issues #
No devices found: Check permissions and platform-specific requirements
Frame drops or freezes: Ensure timely buffer release and avoid blocking operations in callbacks
Build failures: Verify CMake version and platform dependencies are installed
Permission denied: Add user to required groups (Linux) or enable privacy settings
Debug Logging #
MiniAV.setLogLevel(MiniAVLogLevel.debug);
MiniAV.setLogCallback((level, message) {
print('[$level] $message');
});