/*
* VideoKit
* Copyright © 2026 Yusuf Olokoba. All Rights Reserved.
*/
#nullable enable
namespace VideoKit {
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using UnityEngine;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using Newtonsoft.Json;
using Internal;
using Status = Internal.VideoKit.Status;
///
/// Pixel buffer.
/// The format of the pixel buffer varies by platform and must be taken into consideration when using the pixel data.
/// The pixel buffer may contain a metadata dictionary.
/// NOTE: The pixel buffer NEVER owns its own memory.
///
public unsafe readonly struct PixelBuffer : IDisposable {
#region --Enumerations--
///
/// Pixel buffer format.
///
public enum Format : int { // CHECK // VideoKit.h
///
/// Unknown image format.
///
Unknown = 0,
///
/// Generic YUV 420 planar format.
///
YCbCr420 = 1,
///
/// RGBA8888.
///
RGBA8888 = 2,
///
/// BGRA8888.
///
BGRA8888 = 3,
}
public enum Rotation : int { // CHECK // VideoKit.h
///
/// No rotation.
///
_0 = 0,
///
/// Rotate 90 degrees clockwise.
///
_90 = 3,
///
/// Rotate 180 degrees.
///
_180 = 2,
///
/// Rotate 270 degrees clockwise.
///
_270 = 1
}
#endregion
#region --Types--
///
/// Pixel buffer plane for planar formats.
///
public readonly struct Plane {
#region --Properties--
///
/// Pixel data.
///
public unsafe readonly NativeArray data {
get {
pixelBuffer.GetPixelBufferPlaneData(index, out var data).Throw();
pixelBuffer.GetPixelBufferPlaneDataSize(index, out var size).Throw();
var nativeArray = NativeArrayUnsafeUtility.ConvertExistingDataToNativeArray(
data,
size,
Allocator.None
);
#if ENABLE_UNITY_COLLECTIONS_CHECKS
NativeArrayUnsafeUtility.SetAtomicSafetyHandle(ref nativeArray, AtomicSafetyHandle.Create());
#endif
return nativeArray;
}
}
///
/// Plane width.
///
public readonly int width => pixelBuffer.GetPixelBufferPlaneWidth(index, out var width).Throw() == Status.Ok ? width : default;
///
/// Plane height.
///
public readonly int height => pixelBuffer.GetPixelBufferPlaneHeight(index, out var height).Throw() == Status.Ok ? height : default;
///
/// Row stride in bytes.
///
public readonly int rowStride => pixelBuffer.GetPixelBufferPlaneRowStride(index, out var stride).Throw() == Status.Ok ? stride : default;
///
/// Pixel stride in bytes.
///
public readonly int pixelStride => pixelBuffer.GetPixelBufferPlanePixelStride(index, out var stride).Throw() == Status.Ok ? stride : default;
#endregion
#region --Operations--
private readonly IntPtr pixelBuffer;
private readonly int index;
internal Plane (IntPtr pixelBuffer, int index) {
this.pixelBuffer = pixelBuffer;
this.index = index;
}
#endregion
}
#endregion
#region --Properties--
///
/// Pixel data.
/// Some planar pixel buffers might not have contiguous data.
/// In this case, the data is uninitialized.
///
public unsafe readonly NativeArray data {
get {
if (dataBuffer.IsCreated)
return dataBuffer;
handle.GetPixelBufferData(out var data);
handle.GetPixelBufferDataSize(out var size);
if (data == default)
return default;
var nativeArray = NativeArrayUnsafeUtility.ConvertExistingDataToNativeArray(
data,
size,
Allocator.None
);
#if ENABLE_UNITY_COLLECTIONS_CHECKS
NativeArrayUnsafeUtility.SetAtomicSafetyHandle(ref nativeArray, AtomicSafetyHandle.Create());
#endif
return nativeArray;
}
}
///
/// Pixel buffer format.
///
public readonly Format format => handle.GetPixelBufferFormat(out var format) == Status.Ok ? format : default;
///
/// Pixel buffer width.
///
public readonly int width => handle.GetPixelBufferWidth(out var width) == Status.Ok ? width : default;
///
/// Pixel buffer height.
///
public readonly int height => handle.GetPixelBufferHeight(out var height) == Status.Ok ? height : default;
///
/// Pixel buffer row stride in bytes.
/// This is zero if the pixel data is planar.
///
public readonly int rowStride => handle.GetPixelBufferRowStride(out var stride) == Status.Ok ? stride : default;
///
/// Pixel buffer timestamp in nanoseconds.
/// The timestamp is based on the system media clock.
///
public readonly long timestamp => handle.GetSampleBufferTimestamp(out var timestamp) == Status.Ok ? timestamp : default;
///
/// Whether the pixel buffer is vertically mirrored.
///
public readonly bool verticallyMirrored => handle.GetPixelBufferIsVerticallyMirrored(out var mirrored) == Status.Ok ? mirrored : default;
///
/// Pixel buffer planes for planar formats.
/// This is `null` for interleaved formats.
///
public readonly IReadOnlyList? planes {
get {
var planes = new NativePlanes(this) as IReadOnlyList;
return planes.Count > 0 ? planes : null;
}
}
///
/// Pixel buffer metadata.
/// A new dictionary is returned every time this property is accessed.
/// This is `null` if the pixel buffer has no metadata.
///
public readonly Dictionary? metadata {
get {
var sb = new StringBuilder(8192);
if (handle.CopyPixelBufferMetadata(sb, sb.Capacity) != Status.Ok)
return null;
var metadata = JsonConvert.DeserializeObject>(sb.ToString());
return metadata;
}
}
#endregion
#region --Lifecycle--
///
/// Create a pixel buffer from a texture.
///
/// Input texture.
/// Pixel buffer timestamp.
/// Whether the pixel buffer is vertically mirrored.
/// Created pixel buffer.
public PixelBuffer(
Texture2D texture,
long timestamp = 0L,
bool mirrored = false
) : this(
width: texture.width,
height: texture.height,
format: ToImageFormat(texture.format),
data: texture.GetRawTextureData(),
timestamp: timestamp,
mirrored: mirrored
) { }
///
/// Create an interleaved pixel buffer from pixel data in native memory.
/// NOTE: This overload makes a copy of the input buffer, so prefer using the other overloads instead.
///
/// Pixel buffer width.
/// Pixel buffer height.
/// Pixel buffer format. This MUST be `RGBA8888`.
/// Pixel data.
/// Pixel buffer row stride.
/// Pixel buffer timestamp.
/// Whether the pixel buffer is vertically mirrored.
/// Created pixel buffer.
public PixelBuffer(
int width,
int height,
Format format,
byte[] data,
int rowStride = 0,
long timestamp = 0L,
bool mirrored = false
) : this(
width: width,
height: height,
format: format,
rowStride: rowStride,
timestamp: timestamp,
mirrored: mirrored
) => dataBuffer.CopyFrom(data);
///
/// Create an interleaved pixel buffer from pixel data.
///
/// Pixel buffer width.
/// Pixel buffer height.
/// Pixel buffer format. This MUST be `RGBA8888`.
/// Pixel data.
/// Pixel buffer row stride.
/// Pixel buffer timestamp.
/// Whether the pixel buffer is vertically mirrored.
/// Created pixel buffer.
public unsafe PixelBuffer(
int width,
int height,
Format format,
NativeArray data,
int rowStride = 0,
long timestamp = 0L,
bool mirrored = false
) : this(
width: width,
height: height,
format: format,
data: (byte*)data.GetUnsafePtr(),
rowStride: rowStride,
timestamp: timestamp,
mirrored: mirrored
) { }
///
/// Create an interleaved pixel buffer from pixel data in native memory.
/// NOTE: Do not use this overload unless you know what you are doing!
///
/// Pixel buffer width.
/// Pixel buffer height.
/// Pixel buffer format. This MUST be `RGBA8888`.
/// Pixel data.
/// Pixel buffer row stride.
/// Pixel bufffer timestamp.
/// Whether the pixel buffer is vertically mirrored.
/// Created pixel buffer.
public unsafe PixelBuffer(
int width,
int height,
Format format,
byte* data,
int rowStride = 0,
long timestamp = 0L,
bool mirrored = false
) {
VideoKit.CreatePixelBuffer(
width,
height,
format,
data,
rowStride > 0 ? rowStride : GetDefaultStride(format, width),
timestamp,
mirrored,
out handle
).Throw();
dataBuffer = default;
}
///
/// Dispose the pixel buffer and release resources.
///
public void Dispose() {
handle.ReleaseSampleBuffer();
dataBuffer.Dispose();
}
#endregion
#region --Processing--
///
/// Copy pixel data into a destination pixel buffer.
///
/// Destination pixel buffer.
/// Rotation to apply when copying pixel data.
public void CopyTo(
PixelBuffer destination,
Rotation rotation = Rotation._0
) => handle.CopyToPixelBuffer(destination, rotation).Throw();
#endregion
#region --Operations--
private readonly IntPtr handle;
private readonly NativeArray dataBuffer;
internal PixelBuffer(IntPtr handle) {
this.handle = handle;
this.dataBuffer = default;
}
internal PixelBuffer( // might wanna make this public
int width,
int height,
Format format,
int rowStride = 0,
long timestamp = 0L,
bool mirrored = false
) {
rowStride = rowStride > 0 ? rowStride : GetDefaultStride(format, width);
dataBuffer = new NativeArray(rowStride * height, Allocator.Persistent);
VideoKit.CreatePixelBuffer(
width,
height,
format,
(byte*)dataBuffer.GetUnsafePtr(),
rowStride,
timestamp,
mirrored,
out handle
).Throw();
}
public static implicit operator IntPtr(PixelBuffer pixelBuffer) => pixelBuffer.handle;
#endregion
#region --Utilities--
private static Format ToImageFormat(TextureFormat format) => format switch {
TextureFormat.RGBA32 => Format.RGBA8888,
TextureFormat.BGRA32 => Format.BGRA8888,
_ => throw new ArgumentException($"Cannot create pixel buffer from texture with format: {format}"),
};
private static int GetDefaultStride(Format format, int width) => format switch {
Format.RGBA8888 => width * 4,
Format.BGRA8888 => width * 4,
_ => throw new ArgumentException($"Cannot infer default stride for format: {format}"),
};
private readonly struct NativePlanes : IReadOnlyList {
private readonly IntPtr pixelBuffer;
public NativePlanes(IntPtr pixelBuffer) => this.pixelBuffer = pixelBuffer;
int IReadOnlyCollection.Count => pixelBuffer.GetPixelBufferPlaneCount(out var count) == Status.Ok ? count : default;
Plane IReadOnlyList.this [int index] => new(pixelBuffer, index);
IEnumerator IEnumerable.GetEnumerator() {
pixelBuffer.GetPixelBufferPlaneCount(out var count);
for (var idx = 0; idx < count; ++idx)
yield return new(pixelBuffer, idx);
}
IEnumerator IEnumerable.GetEnumerator() => (this as IEnumerable).GetEnumerator();
}
#endregion
}
}