/* * VideoKit * Copyright © 2025 Yusuf Olokoba. All Rights Reserved. */ #nullable enable namespace VideoKit.Sources { using System; using UnityEngine; using UnityEngine.Rendering; using Clocks; using Internal; /// /// Media source for generating pixel buffers from textures. /// public sealed class TextureSource : IDisposable { #region --Client API-- /// /// Texture to automatically capture pixel buffers from. /// When this is set, the source will generate pixel buffers in the application update loop. /// To manually capture pixel buffers, set this to `null` and use the `Append` method. /// public Texture? texture; /// /// Watermark image. /// If `null`, no watermark will be rendered. /// public Texture? watermark; /// /// Watermark display rect in pixel coordinates. /// public RectInt watermarkRect; /// /// Whether to aspect fit the watermark into the watermark display rect. /// Defaults to `true`. /// public bool watermarkAspectFit = true; /// /// Region of interest to capture in pixel coordinates. /// public RectInt regionOfInterest; /// /// Control number of successive frames to skip while generating pixel buffers. /// This is very useful for GIF recording, which typically has a lower framerate appearance. /// public int frameSkip; /// /// Create a texture source. /// /// Pixel buffer width. /// Pixel buffer height. /// Handler to receive pixel buffers. /// Clock for generating pixel buffer timestamps. public TextureSource ( int width, int height, Action handler, IClock? clock = null ) { this.handler = handler; this.clock = clock; this.descriptor = new(width, height, RenderTextureFormat.ARGB32, 0) { sRGB = true }; this.regionOfInterest = new(0, 0, width, height); VideoKitEvents.Instance.onFrame += OnFrame; } /// /// Append a pixel buffer from a texture. /// /// Texture to readback from. /// Pixel buffer timestamp in nanoseconds. public void Append (Texture texture, long timestamp = 0L) { // Check handler if (handler == null) return; // Blit var renderTexture = RenderTexture.GetTemporary(descriptor); Preprocess(texture, renderTexture); // Readback if (SystemInfo.supportsAsyncGPUReadback) AsyncGPUReadback.Request(renderTexture, 0, TextureFormat.RGBA32, request => { // Check handler if (handler == null) return; // Check error // Thanks Dr. Arth! if (request.hasError) { Debug.LogWarning("VideoKit TextureSource failed to readback texture data"); return; } // Invoke handler using var pixelBuffer = new PixelBuffer( request.width, request.height, PixelBuffer.Format.RGBA8888, request.GetData(), timestamp: timestamp ); handler(pixelBuffer); }); else { // Readback readbackBuffer = readbackBuffer != null ? readbackBuffer : new Texture2D(descriptor.width, descriptor.height, TextureFormat.RGBA32, false); var prevActive = RenderTexture.active; RenderTexture.active = renderTexture; readbackBuffer.ReadPixels(new Rect(0, 0, descriptor.width, descriptor.height), 0, 0, false); // Invoke handler using var pixelBuffer = new PixelBuffer( descriptor.width, descriptor.height, PixelBuffer.Format.RGBA8888, readbackBuffer.GetRawTextureData(), timestamp: timestamp ); handler(pixelBuffer); // Reassign RenderTexture.active = prevActive; } // Release RenderTexture.ReleaseTemporary(renderTexture); } /// /// Stop the media source and release resources. /// public void Dispose () { handler = null; VideoKitEvents.Instance.onFrame -= OnFrame; Texture2D.Destroy(readbackBuffer); } #endregion #region --Operations-- private Action? handler; private readonly IClock? clock; private readonly RenderTextureDescriptor descriptor; private int frameIdx; private Texture2D? readbackBuffer; private void OnFrame () { if (texture != null && frameIdx++ % (frameSkip + 1) == 0) Append(texture, clock?.timestamp ?? 0L); } private void Preprocess (Texture source, RenderTexture destination) { // Crop var cropDest = RenderTexture.GetTemporary(descriptor); ExtractRoI(source, cropDest); // Watermark var watermarkDest = RenderTexture.GetTemporary(descriptor); ApplyWatermark(cropDest, watermarkDest); // Blit Graphics.Blit(watermarkDest, destination); RenderTexture.ReleaseTemporary(cropDest); RenderTexture.ReleaseTemporary(watermarkDest); } private void ExtractRoI ( Texture source, RenderTexture destination ) { // Compute crop scale var frameSize = new Vector2(destination.width, destination.height); var ratio = new Vector2( frameSize.x / regionOfInterest.width, frameSize.y / regionOfInterest.height ); var scale = Mathf.Max(ratio.x, ratio.y); // Compute draw rect var pixelSize = scale * frameSize; var minPoint = 0.5f * frameSize - scale * regionOfInterest.center; var maxPoint = minPoint + pixelSize; var drawRect = new Rect(minPoint.x, destination.height - maxPoint.y, pixelSize.x, pixelSize.y); // Render var prevActive = RenderTexture.active; RenderTexture.active = destination; GL.Clear(true, true, Color.clear); GL.PushMatrix(); GL.LoadPixelMatrix(0, destination.width, destination.height, 0); Graphics.DrawTexture(drawRect, source); GL.PopMatrix(); RenderTexture.active = prevActive; } private void ApplyWatermark ( Texture source, RenderTexture destination ) { // Render source var prevActive = RenderTexture.active; RenderTexture.active = destination; GL.Clear(true, true, Color.clear); GL.PushMatrix(); GL.LoadPixelMatrix(0, destination.width, destination.height, 0); Graphics.Blit(source, destination); // Render watermark if (watermark != null) { var rect = watermarkAspectFit ? AspectFitRect(watermark, watermarkRect) : ToRect(watermarkRect); rect.y = destination.height - rect.max.y; // required by `Graphics::DrawTexture` Graphics.DrawTexture(rect, watermark); } // Restore GL.PopMatrix(); RenderTexture.active = prevActive; } #endregion #region --Utility-- private static Rect AspectFitRect (Texture watermark, RectInt frame) { var frameAspect = (float)frame.width / frame.height; var textureAspect = (float)watermark.width / watermark.height; var fitToWidth = textureAspect > frameAspect; var width = fitToWidth ? frame.width : frame.height * textureAspect; var height = fitToWidth ? frame.width / textureAspect : frame.height; var x = 0.5f * (frame.width - width); var y = 0.5f * (frame.height - height); return new(frame.x + x, frame.y + y, width, height); } private static Rect ToRect (RectInt rect) => new(rect.x, rect.y, rect.width, rect.height); #endregion } }