/* * 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 } }