From c70560025d006a666e7975dc1b1e6fa887c0544b Mon Sep 17 00:00:00 2001 From: cosmonaut Date: Wed, 7 Jun 2023 12:32:27 -0700 Subject: [PATCH] double buffered video for fast loops --- src/Video/VideoAV1.cs | 45 +++++------------- src/Video/VideoAV1Stream.cs | 91 +++++++++++++++++++++++++++++++++++++ src/Video/VideoPlayer.cs | 71 +++++++++-------------------- 3 files changed, 125 insertions(+), 82 deletions(-) create mode 100644 src/Video/VideoAV1Stream.cs diff --git a/src/Video/VideoAV1.cs b/src/Video/VideoAV1.cs index 6cb238b..2f485a5 100644 --- a/src/Video/VideoAV1.cs +++ b/src/Video/VideoAV1.cs @@ -3,10 +3,13 @@ using System.IO; namespace MoonWorks.Video { - public unsafe class VideoAV1 : IDisposable + public unsafe class VideoAV1 { - public IntPtr Handle => handle; - IntPtr handle; + public string Filename { get; } + + // "double buffering" so we can loop without a stutter + internal VideoAV1Stream StreamA { get; } + internal VideoAV1Stream StreamB { get; } public int Width => width; public int Height => height; @@ -19,8 +22,6 @@ namespace MoonWorks.Video private int height; private Dav1dfile.PixelLayout pixelLayout; - bool IsDisposed; - public VideoAV1(string filename, double framesPerSecond) { if (!File.Exists(filename)) @@ -28,12 +29,13 @@ namespace MoonWorks.Video throw new ArgumentException("Video file not found!"); } - if (Dav1dfile.df_fopen(filename, out handle) == 0) + if (Dav1dfile.df_fopen(filename, out var handle) == 0) { throw new Exception("Failed to open video file!"); } - Dav1dfile.df_videoinfo(Handle, out width, out height, out pixelLayout); + Dav1dfile.df_videoinfo(handle, out width, out height, out pixelLayout); + Dav1dfile.df_close(handle); if (pixelLayout == Dav1dfile.PixelLayout.I420) { @@ -56,34 +58,11 @@ namespace MoonWorks.Video } FramesPerSecond = framesPerSecond; - } - protected virtual void Dispose(bool disposing) - { - if (!IsDisposed) - { - if (disposing) - { - // dispose managed state (managed objects) - } + Filename = filename; - // free unmanaged resources (unmanaged objects) - Dav1dfile.df_close(Handle); - - IsDisposed = true; - } - } - - ~VideoAV1() - { - Dispose(disposing: false); - } - - public void Dispose() - { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: true); - GC.SuppressFinalize(this); + StreamA = new VideoAV1Stream(this); + StreamB = new VideoAV1Stream(this); } } } diff --git a/src/Video/VideoAV1Stream.cs b/src/Video/VideoAV1Stream.cs new file mode 100644 index 0000000..5ac443c --- /dev/null +++ b/src/Video/VideoAV1Stream.cs @@ -0,0 +1,91 @@ +using System; + +namespace MoonWorks.Video +{ + internal class VideoAV1Stream + { + public IntPtr Handle => handle; + IntPtr handle; + + public bool Ended => Dav1dfile.df_eos(Handle) == 1; + + public IntPtr yDataHandle; + public IntPtr uDataHandle; + public IntPtr vDataHandle; + public uint yDataLength; + public uint uvDataLength; + public uint yStride; + public uint uvStride; + + public bool FrameDataUpdated { get; private set; } + + bool IsDisposed; + + public VideoAV1Stream(VideoAV1 video) + { + if (Dav1dfile.df_fopen(video.Filename, out handle) == 0) + { + throw new Exception("Failed to open video file!"); + } + + Reset(); + } + + public void Reset() + { + lock (this) + { + Dav1dfile.df_reset(Handle); + ReadNextFrame(); + } + } + + public void ReadNextFrame() + { + lock (this) + { + if (Dav1dfile.df_readvideo( + Handle, + 1, + out yDataHandle, + out uDataHandle, + out vDataHandle, + out yDataLength, + out uvDataLength, + out yStride, + out uvStride) == 1 + ) { + FrameDataUpdated = true; + } + } + } + + protected virtual void Dispose(bool disposing) + { + if (!IsDisposed) + { + if (disposing) + { + // dispose managed state (managed objects) + } + + // free unmanaged resources (unmanaged objects) + Dav1dfile.df_close(Handle); + + IsDisposed = true; + } + } + + ~VideoAV1Stream() + { + Dispose(disposing: false); + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/src/Video/VideoPlayer.cs b/src/Video/VideoPlayer.cs index ce95001..7c33532 100644 --- a/src/Video/VideoPlayer.cs +++ b/src/Video/VideoPlayer.cs @@ -1,5 +1,6 @@ using System; using System.Diagnostics; +using System.Threading.Tasks; using System.Runtime.InteropServices; using MoonWorks.Audio; using MoonWorks.Graphics; @@ -14,6 +15,7 @@ namespace MoonWorks.Video public float PlaybackSpeed { get; set; } = 1; private VideoAV1 Video = null; + private VideoAV1Stream CurrentStream = null; private GraphicsDevice GraphicsDevice; private Texture yTexture = null; @@ -21,17 +23,6 @@ namespace MoonWorks.Video private Texture vTexture = null; private Sampler LinearSampler; - private object readLock = new object(); - private bool frameDataUpdated; - - private IntPtr yDataHandle; - private IntPtr uDataHandle; - private IntPtr vDataHandle; - private uint yDataLength; - private uint uvDataLength; - private uint yStride; - private uint uvStride; - private int currentFrame; private Stopwatch timer; @@ -152,7 +143,7 @@ namespace MoonWorks.Video lastTimestamp = 0; timeElapsed = 0; - Dav1dfile.df_reset(Video.Handle); + InitializeDav1dStream(); State = VideoState.Stopped; } @@ -176,28 +167,27 @@ namespace MoonWorks.Video int thisFrame = ((int) (timeElapsed / (1000.0 / Video.FramesPerSecond))); if (thisFrame > currentFrame) { - if (frameDataUpdated) + if (CurrentStream.FrameDataUpdated) { UpdateRenderTexture(); } currentFrame = thisFrame; - System.Threading.Tasks.Task.Run(ReadNextFrame); + Task.Run(CurrentStream.ReadNextFrame); } - bool ended = Dav1dfile.df_eos(Video.Handle) == 1; - if (ended) + if (CurrentStream.Ended) { timer.Stop(); timer.Reset(); - Dav1dfile.df_reset(Video.Handle); + Task.Run(CurrentStream.Reset); if (Loop) { - // Start over! - InitializeDav1dStream(); - + // Start over on the next stream! + CurrentStream = (CurrentStream == Video.StreamA) ? Video.StreamB : Video.StreamA; + currentFrame = -1; timer.Start(); } else @@ -209,7 +199,7 @@ namespace MoonWorks.Video private void UpdateRenderTexture() { - lock (readLock) + lock (CurrentStream) { var commandBuffer = GraphicsDevice.AcquireCommandBuffer(); @@ -217,13 +207,13 @@ namespace MoonWorks.Video yTexture, uTexture, vTexture, - yDataHandle, - uDataHandle, - vDataHandle, - yDataLength, - uvDataLength, - yStride, - uvStride + CurrentStream.yDataHandle, + CurrentStream.uDataHandle, + CurrentStream.vDataHandle, + CurrentStream.yDataLength, + CurrentStream.uvDataLength, + CurrentStream.yStride, + CurrentStream.uvStride ); commandBuffer.BeginRenderPass( @@ -269,28 +259,11 @@ namespace MoonWorks.Video private void InitializeDav1dStream() { - System.Threading.Tasks.Task.Run(ReadNextFrame); - currentFrame = -1; - } + Task.Run(Video.StreamA.Reset); + Task.Run(Video.StreamB.Reset); - private void ReadNextFrame() - { - lock (readLock) - { - if (Dav1dfile.df_readvideo( - Video.Handle, - 1, - out yDataHandle, - out uDataHandle, - out vDataHandle, - out yDataLength, - out uvDataLength, - out yStride, - out uvStride) == 1 - ) { - frameDataUpdated = true; - } - } + CurrentStream = Video.StreamA; + currentFrame = -1; } protected virtual void Dispose(bool disposing)