Skip to main content

yuv_ffi 0.5.2 copy "yuv_ffi: ^0.5.2" to clipboard
yuv_ffi: ^0.5.2 copied to clipboard

High-performance YUV/BGRA image processing for Flutter via native C/FFI.

yuv_ffi #

yuv_ffi processes and displays raw YUV and BGRA images in Flutter. Use it when your app works with decoded pixels rather than encoded files: convert between I420, NV12, and BGRA8888, crop, rotate, flip, blur, or adjust colors, and show the result as a widget or as a live stream of frames. Native platforms run C code through FFI; Web runs a WASM backend on Flutter's JavaScript and --wasm builds.

Which package do you need #

You want to Use
Create, convert, transform, and display YUV or BGRA images yuv_ffi (this package)
Show a live camera preview, process camera frames, or capture a frame yuv_camera, which builds on yuv_ffi

Camera capture and camera frame import are not part of yuv_ffi. Add yuv_camera only when your app uses a camera; it brings yuv_ffi with it, and yuv_ffi never depends on it.

Installation #

dependencies:
  yuv_ffi: ^0.5.2

Version 0.5.2 moves the camera code out of the example into yuv_camera. This removes or renames no existing yuv_ffi calls, and adds YuvFramePresenterLazyFrames.presentLazy. There are visible behavior changes: the implicit YuvFrameView fit now scales the whole frame to contain, and input validation rejects malformed layouts earlier. See MIGRATION.md and CHANGELOG.md for the before/after fit and validation details. Version 0.4.0 was published and later retracted; the migration guidance below also applies to applications whose lockfile still resolves 0.4.0.

Requirements #

Platform Requirement
Dart 3.12 or later
Flutter 3.44.0 or later; officially tested and supported: 3.44.9
Android API 26 or later; armeabi-v7a, arm64-v8a, or x86_64
iOS 13 or later
macOS 10.15 or later
Windows Native FFI backend
Linux Native FFI backend
Web Flutter JavaScript and --wasm builds; tested in Chrome

On iOS and macOS the plugin builds with either Swift Package Manager (the default in Flutter 3.44 and later) or CocoaPods with the template's dynamic use_frameworks! setup. CocoaPods also supplies linker roots for the 11 ABI v1 entry points so static archives retain the symbols used by Dart process lookups; no manual Runner linker flags are needed. Static use_frameworks! :linkage => :static was verified on an iOS 18.6 simulator in Debug with Firebase Core 3.15.2: Firebase initialization, native symbol loading and BGRA copy/rotate passed. A Podfile without use_frameworks!, macOS static runtime, Release static linkage and other Firebase versions remain unverified. The Apple sources live in darwin/; the C sources stay in src/.

Web operations match the native backend in the checked cases on both the JavaScript and --wasm Flutter builds. chromaSwap supports NV12 only, as on native. Both builds were verified in Chrome. Use flutter build web or flutter build web --wasm. Safari and Firefox have not been verified. Safari 16.4 or later is a technical minimum for release WASM SIMD, not a tested compatibility claim for this plugin. The module is requested from Flutter's default asset location, relative to the page, as in a plain flutter build web output. A custom assetBase, a CDN or a different origin, and a strict Content-Security-Policy have not been verified.

Quick start #

This complete lib/main.dart creates an I420 image, fills it from RGBA bytes, rotates it, and shows it.

import 'dart:typed_data';

import 'package:flutter/widgets.dart';
import 'package:yuv_ffi/yuv_ffi.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await YuvFfi.initialize();

  const width = 320;
  const height = 180;
  final rgba = Uint8List(width * height * 4);
  for (var y = 0; y < height; y++) {
    for (var x = 0; x < width; x++) {
      final i = (y * width + x) * 4;
      rgba[i] = x * 255 ~/ (width - 1); // red
      rgba[i + 1] = y * 255 ~/ (height - 1); // green
      rgba[i + 2] = 128; // blue
      rgba[i + 3] = 255; // alpha
    }
  }

  final image = YuvImage.i420(width, height)
    ..applyRgbaBytes(rgba)
    ..applyRotation(YuvImageRotation.rotation90);

  runApp(
    Directionality(
      textDirection: TextDirection.ltr,
      child: YuvImageWidget(image: image),
    ),
  );
}

What the example does:

  • YuvFfi.initialize() loads the native library or the Web module. Call it once in every isolate before processing. A successful initialization is cached; a failed one can be retried. It returns the capabilities of the loaded backend.
  • YuvImage.i420(width, height) allocates an empty image in the I420 format (separate Y, U, and V planes; see Formats and plane layout).
  • applyRgbaBytes takes input as RGBA bytes and converts it to the image's own format. BGRA images store the same pixels in B, G, R, A order, so RGBA input is never written to a BGRA plane unchanged.
  • apply* methods change the image in place and return it; methods such as cropped, rotated, copy, and toBgra return a new image. See Image mutation and revisions.
  • YuvImageWidget draws the image.

Initialization #

On Web, initialization must complete before every YuvImage operation. On native platforms, it must complete before these capability-gated methods: applyRgbaBytes, applyGrayscale, applyBlackWhite, applyNegate, applyGaussianBlur, applyMeanBlur, applyBoxBlur, applyCrop, applyFlipHorizontal, applyFlipVertical, applyRotation, applyFormat, applyChromaSwap, cropped, rotated, toI420, toNv12, and toBgra. Constructors, plane access, copy, applyPlanes, byte conversion, encoding, and decoding retain native lazy behavior.

To choose a path at run time, query the returned capabilities:

final capabilities = await YuvFfi.initialize();
if (capabilities.supports(YuvOperation.rotate, sourceFormat: image.format)) {
  image.applyRotation(YuvImageRotation.rotation90);
}

Image mutation and revisions #

apply* methods mutate their receiver in place and return that same image. Successful mutations advance image.revision once; rejected calls and genuine no-ops leave it unchanged. toI420, toNv12, toBgra, cropped, rotated, copy, and YuvImage.decode return independent images and do not mutate the source.

planes, yPlane, uPlane, and vPlane expose mutable backing storage. After directly changing plane bytes, call markDirty() so a widget creates a new cache key. apply* and applyPlanes already update the revision.

image.yPlane.bytes[0] = 0xFF;
image.markDirty();

applyPlanes(...) validates and atomically replaces all planes. Any plane reference obtained before it succeeds is stale and must be read again.

Displaying images #

Use YuvImageWidget for a standalone image. It creates a YuvImageProvider, whose cache key combines the image identity with the captured revision.

YuvImageWidget(
  image: image,
  boxFit: BoxFit.contain,
)

YuvImageProvider(image) is available when an ImageProvider is required. YuvImageWidget defaults to BoxFit.none; pass boxFit: BoxFit.contain to show the full decoded image. YuvFrameView defaults to YuvFrameFit.contain, so decoded and live views can use the same fit. For a live stream, YuvFramePresenter keeps one current image for display. present(frame) returns false while decoding or until the decoded frame has been rendered, so intermediate frames are dropped. Render it with YuvFrameView and call dispose() when the stream ends.

final presenter = YuvFramePresenter();
presenter.present(image);

final view = YuvFrameView(presenter: presenter);

Pass useShader: true to YuvFramePresenter for shader-backed live frames, and pass a YuvFrameOrientation to present to rotate or mirror at draw time. YuvFrameView.fit and alignment use YuvFrameGeometry; fit defaults to YuvFrameFit.contain for drawing. When fit is omitted, the view also keeps the upright frame's aspect ratio with AspectRatio; an explicit fit uses the available viewport constraints. Its onGeometryChanged callback reports computed geometry after layout and paint scheduling; it is not called before a frame or when the viewport has zero or non-finite dimensions. Use the callback's value for the overlay mapping. YuvImageWidget forwards asynchronous decode failures to errorBuilder; if you call image.toImage() directly, await its future and handle a decode error there.

Frame geometry and ML Kit #

YuvFrameGeometry keeps source pixels, ML Kit's upright pixels, and widget pixels in separate coordinate spaces. yuv_ffi does not depend on ML Kit; the ML Kit integration is application code, shown in the yuv_camera example. Give ML Kit the raw frame with orientation.rotation, then map its bounding boxes through MatrixUtils.transformRect(geometry.uprightToView, box). Mirroring is applied only after rotation, so the preview and overlay stay aligned.

final geometry = YuvFrameGeometry(
  sourceSize: frame.size,
  viewSize: widgetSize,
  orientation: const YuvFrameOrientation(rotation: YuvImageRotation.rotation90, mirrored: true),
  fit: YuvFrameFit.cover,
);
final displayedFrame = geometry.apply(frame);

YuvFrameRenderer uploads I420 and NV12 planes to the package shader when it is available, including Web (CanvasKit). Unsupported layouts and shader-load failures use the pixel-equivalent BGRA fallback instead.

Formats and plane layout #

YuvPixelFormat.i420 stores separate Y, U, and V planes. YuvPixelFormat.nv12 stores Y plus interleaved (U, V) chroma bytes. bgra8888 is a single packed BGRA plane.

Converting between I420 or NV12 and BGRA or RGBA bytes uses the BT.601 matrix in limited (video) range, and the shader path uses the same formula. The package does not detect full-range data or another color matrix such as BT.709 from the source, and it has no option to choose one: planes that use a different range or matrix are converted as if they were BT.601 limited. BGRA and RGBA carry no YUV range.

Factories that receive planes: default to YuvPlaneLayout.packed: they copy visible samples into tight planes and discard row padding and per-sample gaps. Pass layout: YuvPlaneLayout.preserve to retain the supplied rowStride, pixelStride, and padding bytes. copy, decode, and applyPlanes preserve their plane layouts.

final padded = YuvImage.i420(
  2,
  2,
  planes: <YuvPlane>[
    YuvPlane(2, 4),
    YuvPlane(1, 2),
    YuvPlane(1, 2),
  ],
  layout: YuvPlaneLayout.preserve,
);

padded.pack();

pack() removes row padding and pixel gaps in place while preserving visible samples, format, UV order, size, and orientation. Padding discarded by pack() cannot be recovered; use copy().pack() to retain an independent original image.

Inserting a patch #

applyPatch copies an opaque, unscaled image into another image of the same format without touching destination padding or pixel-gap bytes:

final fragment = source.cropped(region).rotated(YuvImageRotation.rotation90);
destination.applyPatch(fragment, x: 100, y: 40);

The fragment must fit completely. For I420 and NV12, x and y must be even; an odd fragment width or height is accepted only at the corresponding right or bottom edge. This keeps every chroma 2×2 sample wholly inside the patch.

Capabilities and errors #

Call capabilities.supports(operation, sourceFormat: ..., destinationFormat: ...) before dispatch when the application needs to choose a path. A destination format is required only for YuvOperation.convert; malformed or unsupported capability queries return false.

Public calls can throw:

  • ArgumentError for invalid dimensions, planes, byte lengths, geometry, or native argument validation.
  • StateError from YuvFrameGeometry.apply when its geometry has no visible source area; an empty image cannot be represented.
  • UnsupportedError when a backend or format pair cannot dispatch an operation, including a capability-gated call before initialization.
  • YuvNativeException for native overflow, allocation, internal failures, or an unrecognized native status.
  • StateError during initialization when Web configuration or runtime setup fails, or when a native library lacks a required ABI export.

Platform status #

The automated checks below run directly on the maintainer's Windows, Linux and macOS machines through the scripts in tool/ci/; each release records their results against its exact commit. The repository has no hosted CI.

Platform Support Checked by package scripts Checked manually
Android Native FFI: armeabi-v7a, arm64-v8a, x86_64 APK builds for armeabi-v7a and arm64-v8a; app-runtime smoke and the 1188-case correctness matrix on a physical Pixel 3 (arm64-v8a); x86_64 is built but not checked Release builds on a physical Pixel 3 for arm64-v8a and armeabi-v7a
iOS Native FFI Build, app-runtime smoke, and the 1188-case correctness matrix Release build on a physical iPhone (iOS 18.7): shader display
macOS Native FFI Example build, app-runtime smoke, and the 1188-case correctness matrix —
Windows Native FFI Build, app-runtime smoke, and the 1188-case correctness matrix —
Linux Native FFI Example build, packaging, app-runtime smoke, and the 1188-case correctness matrix —
Web WASM backend (JavaScript and --wasm builds); operation parity with native in checked cases Package checks, browser tests, and the 1188-case correctness matrix on both builds Chrome

Web backend #

Web operations match the native backend in the checked cases on the JavaScript and --wasm Flutter builds (verified in Chrome). chromaSwap is supported for NV12 only, as on native. Safari and Firefox have not been verified. YuvFfi.initialize() loads the module; use YuvCapabilities to query the operations exported by that module.

Migrating to 0.5.x #

The compatibility declarations from 0.2.4 and retracted 0.4.0 were removed. The detailed upgrade steps, API mapping, behavior changes, and verification commands are in MIGRATION.md. Key points: initialize every isolate, review mutating versus copy-returning calls, preserve plane layout explicitly when needed, and migrate serialized frames through an application-owned representation. The same guide applies to applications whose lockfile still resolves 0.4.0.

Building from a repository checkout #

Native sources live in src/ and are built through the platform plugin build configuration. From a repository checkout, build the Web module from a macOS/Linux shell, Git Bash, or WSL:

sh ./tool/wasm/build_wasm.sh

Do not edit lib/src/functions/bindings/yuv_ffi_bingings.dart manually. Regenerate bindings after a header or configuration change:

dart run ffigen --config ffigen.yaml

Learn more #

License #

MIT License

Verification boundaries #

The package allows Flutter 3.44.0 or later. Flutter 3.44.9 is the officially tested and supported version. Earlier Flutter 3.44.x patch releases may work, but have not been verified.

Safari, Firefox and mobile Web are unverified. Custom asset placement, CDN hosting and strict CSP for the WASM module remain unverified. Static CocoaPods verification is limited to the iOS simulator Debug scenario above. Image-operation --wasm evidence in Chrome does not establish camera --wasm support in yuv_camera.

2
likes
160
points
668
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

High-performance YUV/BGRA image processing for Flutter via native C/FFI.

Repository (GitHub)
View/report issues

License

(pending) (license)

Dependencies

ffi, flutter, flutter_web_plugins, plugin_platform_interface, web

More

Packages that depend on yuv_ffi

Packages that implement yuv_ffi