packYuv420 function

PackedYuv? packYuv420({
  1. required int width,
  2. required int height,
  3. required YuvPlane y,
  4. required YuvPlane u,
  5. YuvPlane? v,
  6. Uint8List? into,
})

Packs a YUV420 camera frame into a single contiguous buffer suitable for native colour conversion (e.g. opencv's cvtColor with a COLOR_YUV2BGR_NV21 / COLOR_YUV2BGR_NV12 / COLOR_YUV2BGR_I420 code).

Auto-detects the source layout based on the plane count and the U plane's YuvPlane.pixelStride:

  • 2 planes: NV12. iOS AVFoundation default.
  • 3 planes, U pixelStride 2: NV21. Most Android devices (semi-planar); the V plane's buffer is used as the VU-interleaved region start.
  • 3 planes, U pixelStride 1: I420. Planar Android.

Row-stride padding is stripped so the returned buffer is tightly packed.

Typical usage with opencv_dart:

final packed = packYuv420(
  width: image.width,
  height: image.height,
  y: (bytes: image.planes[0].bytes,
      rowStride: image.planes[0].bytesPerRow,
      pixelStride: image.planes[0].bytesPerPixel ?? 1),
  u: (bytes: image.planes[1].bytes,
      rowStride: image.planes[1].bytesPerRow,
      pixelStride: image.planes[1].bytesPerPixel ?? 1),
  v: image.planes.length > 2
    ? (bytes: image.planes[2].bytes,
       rowStride: image.planes[2].bytesPerRow,
       pixelStride: image.planes[2].bytesPerPixel ?? 1)
    : null,
);

final code = switch (packed.layout) {
  YuvLayout.nv12 => cv.COLOR_YUV2BGR_NV12,
  YuvLayout.nv21 => cv.COLOR_YUV2BGR_NV21,
  YuvLayout.i420 => cv.COLOR_YUV2BGR_I420,
};
final layout = packed.sourceLayout;
final yuvMat = cv.Mat.create(
  rows: layout.rows,
  cols: layout.cols,
  type: cv.MatType.CV_8UC1,
);
layout.copyTo(yuvMat.data, packed.bytes);
final bgr = cv.cvtColor(yuvMat, code);
yuvMat.dispose();

Returns null for unsupported shapes (non-positive or odd width or height).

into optionally receives the packed bytes instead of allocating a fresh buffer, which matters in per-frame camera loops: a 720p pack otherwise allocates and zero-fills ~1.4 MB per call. Its length must be exactly width * height * 3 ~/ 2 (throws ArgumentError otherwise). Every byte is written (truncated source planes zero-fill their tail), so a reused buffer needs no clearing and the output is byte-identical to the allocating form.

Implementation

PackedYuv? packYuv420({
  required int width,
  required int height,
  required YuvPlane y,
  required YuvPlane u,
  YuvPlane? v,
  Uint8List? into,
}) {
  if (width <= 0 || height <= 0 || (width & 1) != 0 || (height & 1) != 0) {
    return null;
  }

  final int ySize = width * height;
  final int uvSize = width * (height ~/ 2);
  if (into != null && into.length != ySize + uvSize) {
    throw ArgumentError.value(
      into.length,
      'into.length',
      'Expected ${ySize + uvSize} bytes for ${width}x$height YUV420.',
    );
  }
  final Uint8List out = into ?? Uint8List(ySize + uvSize);

  _copyPlaneRows(
    src: y.bytes,
    srcStride: y.rowStride,
    rowBytes: width,
    rows: height,
    dst: out,
    dstOffset: 0,
  );

  if (v == null) {
    // 2-plane NV12: planes[1] is the UV-interleaved chroma region.
    _copyPlaneRows(
      src: u.bytes,
      srcStride: u.rowStride,
      rowBytes: width,
      rows: height ~/ 2,
      dst: out,
      dstOffset: ySize,
    );
    return PackedYuv(
      bytes: out,
      layout: YuvLayout.nv12,
      width: width,
      height: height,
    );
  }

  if (u.pixelStride == 2) {
    // Android semi-planar (NV21). plane[2] points to the V byte, which is
    // the first byte of the VU-interleaved region when the device uses NV21.
    _copyPlaneRows(
      src: v.bytes,
      srcStride: v.rowStride,
      rowBytes: width,
      rows: height ~/ 2,
      dst: out,
      dstOffset: ySize,
    );
    return PackedYuv(
      bytes: out,
      layout: YuvLayout.nv21,
      width: width,
      height: height,
    );
  }

  // Planar I420.
  final int uvWidth = width ~/ 2;
  final int uvHeight = height ~/ 2;
  _copyPlaneRows(
    src: u.bytes,
    srcStride: u.rowStride,
    rowBytes: uvWidth,
    rows: uvHeight,
    dst: out,
    dstOffset: ySize,
  );
  _copyPlaneRows(
    src: v.bytes,
    srcStride: v.rowStride,
    rowBytes: uvWidth,
    rows: uvHeight,
    dst: out,
    dstOffset: ySize + uvWidth * uvHeight,
  );
  return PackedYuv(
    bytes: out,
    layout: YuvLayout.i420,
    width: width,
    height: height,
  );
}