Home

Awesome

yuv-buffer

Utility package for manipulating video image frames in planar YUV encoding (also known as YCbCr).

Planar YUV image frames represent a color image in the YUV color space commonly used for video processing and both video and image compression. The Y or "luma" plane holds brightness values, while the U and V "chroma" planes store color 'offsets' for blue and red components. Chroma planes are often at a different resolution than the luma plane as a method of psychovisual compression, taking advantage of the human eye's greater sensitivity to brightness and contrast over color.

YUV images must usually be converted to and from RGB for actual capture and display. See the yuv-canvas module for in-browser display of YUV frames, or ogv.js for a full in-browser video decoder/player.

Change log

1.0.1

Installation

$ npm install yuv-buffer

Usage

var YUVBuffer = require("yuv-buffer.js");

// HDTV 1080p:
var format = {
  // Many video formats require an 8- or 16-pixel block size.
  width: 1920,
  height: 1088,

  // Using common 4:2:0 layout, chroma planes are halved in each dimension.
  chromaWidth: 1920 / 2,
  chromaHeight: 1088 / 2,

  // Crop out a 1920x1080 visible region:
  cropLeft: 0,
  cropTop: 4,
  cropWidth: 1920,
  cropHeight: 1080,

  // Square pixels, so same as the crop size.
  displayWidth: 1920,
  displayHeight: 1080
};

var format = YUVBuffer.format(format);
import YUVBuffer, { YUVFormat, YUVFrame, YUVPlane } from "yuv-buffer";

// HDTV 1080p:
let format: YUVFormat = {
  // Many video formats require an 8- or 16-pixel block size.
  width: 1920,
  height: 1088,

  // Using common 4:2:0 layout, chroma planes are halved in each dimension.
  chromaWidth: 1920 / 2,
  chromaHeight: 1088 / 2,

  // Crop out a 1920x1080 visible region:
  cropLeft: 0,
  cropTop: 4,
  cropWidth: 1920,
  cropHeight: 1080,

  // Square pixels, so same as the crop size.
  displayWidth: 1920,
  displayHeight: 1080,
};

format = YUVBuffer.format(format);

Data format

Actual frames are stored in plain JS objects to facilitate transfer between worker threads via structured clone; behavior is provided through static methods on the YUVBuffer utility namespace.

A suitably-formatted frame buffer object looks like this:

{
  format: {
    width,
    height,
    chromaWidth,
    chromaHeight,
    cropLeft,
    cropTop,
    cropWidth,
    cropHeight,
    displayWidth,
    displayHeight
  },
  y: { bytes, stride },
  u: { bytes, stride },
  v: { bytes, stride }
}

The format object provides information necessary for interpreting or displaying frame data, and can be shared between many frame buffers:

The y, u, and v properties contain the pixel data for luma (Y) and chroma (U and V) components of the image:

Format objects

To create or process a YUV frame, you'll need a YUVFormat object describing the memory layout of the pixel data. These are plain JavaScript objects to facilitate data transfer, but should be validated with the YUVBuffer.format() function:

// HDTV 1080p:
var format = YUVBuffer.format({
  // Many video formats require an 8- or 16-pixel block size.
  width: 1920,
  height: 1088,

  // Using common 4:2:0 layout, chroma planes are halved in each dimension.
  chromaWidth: 1920 / 2,
  chromaHeight: 1088 / 2,

  // Crop out a 1920x1080 visible region:
  cropLeft: 0,
  cropTop: 4,
  cropWidth: 1920,
  cropHeight: 1080,

  // Square pixels, so same as the crop size.
  displayWidth: 1920,
  displayHeight: 1080
});
// 480p anamorphic DVD:
var format = YUVBuffer.format({
  // Encoded size is 720x480, for classic NTSC standard def video
  width: 720,
  height: 480

  // DVD is also 4:2:0, so halve the chroma dimensions.
  chromaWidth: 720 / 2,
  chromaHeight: 480 / 2,

  // Full frame is visible.
  cropLeft: 0,
  cropTop: 0,
  cropWidth: 720,
  cropHeight: 480

  // Final display size stretches back out to 16:9 widescreen:
  displayWidth: 853,
  displayHeight: 480
});

A common format object can be passed in to multiple frames, so be sure not to change them unexpectedly in a consumer!

All fields are required in the format object; as a shorthand for simpler frame layouts you can let YUVBuffer.format() derive missing fields at creation time. You must specify width and height, and usually will need to specify chromaWidth and chromaHeight unless using 4:4:4 subsampling consistently.

// HDTV 1080p
var format = YUVBuffer.format({
  // Absolutely required:
  width: 1920,
  height: 1088,

  // To use 4:2:0 layout, we set the chroma plane dimensions:
  chromaWidth: 1920 / 2,
  chromaHeight: 1088 / 2,

  // Explicit cropHeight, with other crop dimensions & display size implied:
  cropHeight: 1080
});

expands into:

{
  width: 1920,
  height: 1088,
  chromaWidth: 960,
  chromaHeight: 544,
  cropLeft: 0, // default
  cropTop: 0, // default
  cropWidth: 1920, // derived from width
  cropHeight: 1080,
  displayWidth: 1920, // derived from width via cropWidth
  displayHeight: 1080 // derived from cropHeight
}

Frame objects

Objects in YUVFrame layout are also plain JavaScript objects to facilitate transfer between Worker threads and potentially storage in IndexedDB.

You can allocate a blank frame with enough memory to work with using the YUVBuffer.frame() helper function, passing in the format object:

var frame = YUVBuffer.frame(format);
console.log(frame.y.bytes.length); // bunch o' bytes

Or if you have planes of data ready to go, you can pass them in as well:

var frame = YUVBuffer.frame(format, y, u, v);

Plane objects

A YUVPlane-formatted object contains the actual byte array and the stride (row-to-row byte increment) of a plane. Note that the stride may be larger than the plane's width, but can never be smaller. The number of rows is not stored in the plane object, and is either format.height for the Y (luma) plane, or format.chromaHeight for the U and V (chroma) planes. The bytes array must have room for at least the height times the stride number of bytes.

If you need to create an individual plane object with empty data, you can use the YUVBuffer.lumaPlane() or YUVBuffer.chromaPlane() functions with a format object. Appropriate dimensions and stride will be selected automatically:

var y = YUVBuffer.lumaPlane(format),
  u = YUVBuffer.chromaPlane(format),
  v = YUVBuffer.chromaPlane(format);
// ...
var frame = YUVBuffer.frame(format, y, u, v);

When you have pixels in another data structure, such as an emscripten-compiled C library's heap memory, you can extract individual plane objects from that larger structure and pass them in to the new frame by passing the source array and stride/offset:

// Module.HEAPU8 is a large Uint8Array
var frame = YUVBuffer.frame(format,
  YUVBuffer.lumaPlane(format, Module.HEAPU8, ystride, yoffset),
  YUVBuffer.chromaPlane(format, Module.HEAPU8, ustride, uoffset),
  YUVBuffer.chromaPlane(format, Module.HEAPU8, vstride, voffset)
);

Note this will make a copy of the bytes in the source array(s). It's possible to manually create "live" views into a heap but this is error-prone; see the "Heap extraction"" section below.

Performance concerns

Threading

Since video processing is CPU-intensive, it is expected that frame data may need to be shuffled between multiple Web Worker threads -- for instance a video decoder in a background thread sending frames to be displayed back to the main thread. Frame buffer objects are plain JS objects to facilitate being sent via postMessage's "structured clone" algorithm. To transfer the raw pixel data buffers instead of copying them in this case, list an array containing the bytes subproperties as transferables:

// video-worker.js
while (true) {
  var frame = processNextFrame();
  postMessage({nextFrame: frame}, YUVBuffer.transferables(frame));
}

Heap extraction

Producers can avoid a data copy by using Uint8Array byte arrays that are views of a larger buffer, such as an emscripten-compiled C library's heap array. However this introduces several potential sources of bugs:

You can use the YUVBuffer.copyFrame static method to duplicate a frame object from an unknown source and "normalize" its heap representation to a fresh copy; or when creating one manually be sure to create a copy with slice instead of a view with subarray.

If deliberately using subarray views, be careful to avoid data corruption or bloated copies.

Recycling

Creating and deleting many frame buffer objects may cause some garbage collection churn or memory fragmentation; it may be advantageous to recycle spare buffers in a producer-consumer loop.

It can be difficult to avoid GC churn when sending data between threads as objects will be created and destroyed on each end, but the pixel buffers can be transferred back and forth without deallocation.

Now what?

So you have a YUV image frame buffer format. What do you do with it?

License

MIT-style license:

Copyright (c) 2014-2024 Brooke Vibber bvibber@pobox.com

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.