MoonWorks/src/Video/VideoPlayer.cs

324 lines
6.8 KiB
C#

using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using MoonWorks.Audio;
using MoonWorks.Graphics;
namespace MoonWorks.Video
{
public unsafe class VideoPlayer : IDisposable
{
public Texture RenderTexture { get; private set; } = null;
public VideoState State { get; private set; } = VideoState.Stopped;
public bool Loop { get; set; }
public float PlaybackSpeed { get; set; } = 1;
private VideoAV1 Video = null;
private GraphicsDevice GraphicsDevice;
private Texture yTexture = null;
private Texture uTexture = null;
private Texture vTexture = null;
private Sampler LinearSampler;
private int currentFrame;
private Stopwatch timer;
private double lastTimestamp;
private double timeElapsed;
private Stopwatch profilingTimer = new Stopwatch();
private bool disposed;
public VideoPlayer(GraphicsDevice graphicsDevice, AudioDevice audioDevice)
{
GraphicsDevice = graphicsDevice;
if (GraphicsDevice.VideoPipeline == null)
{
throw new InvalidOperationException("Missing video shaders!");
}
LinearSampler = new Sampler(graphicsDevice, SamplerCreateInfo.LinearClamp);
timer = new Stopwatch();
}
public void Load(VideoAV1 video)
{
if (Video != video)
{
Stop();
if (RenderTexture == null)
{
RenderTexture = CreateRenderTexture(GraphicsDevice, video.Width, video.Height);
}
if (yTexture == null)
{
yTexture = CreateSubTexture(GraphicsDevice, video.Width, video.Height);
}
if (uTexture == null)
{
uTexture = CreateSubTexture(GraphicsDevice, video.UVWidth, video.UVHeight);
}
if (vTexture == null)
{
vTexture = CreateSubTexture(GraphicsDevice, video.UVWidth, video.UVHeight);
}
if (video.Width != RenderTexture.Width || video.Height != RenderTexture.Height)
{
RenderTexture.Dispose();
RenderTexture = CreateRenderTexture(GraphicsDevice, video.Width, video.Height);
}
if (video.Width != yTexture.Width || video.Height != yTexture.Height)
{
yTexture.Dispose();
yTexture = CreateSubTexture(GraphicsDevice, video.Width, video.Height);
}
if (video.UVWidth != uTexture.Width || video.UVHeight != uTexture.Height)
{
uTexture.Dispose();
uTexture = CreateSubTexture(GraphicsDevice, video.UVWidth, video.UVHeight);
}
if (video.UVWidth != vTexture.Width || video.UVHeight != vTexture.Height)
{
vTexture.Dispose();
vTexture = CreateSubTexture(GraphicsDevice, video.UVWidth, video.UVHeight);
}
Video = video;
InitializeDav1dStream();
}
}
public void Play()
{
if (Video == null) { return; }
if (State == VideoState.Playing)
{
return;
}
timer.Start();
State = VideoState.Playing;
}
public void Pause()
{
if (Video == null) { return; }
if (State != VideoState.Playing)
{
return;
}
timer.Stop();
State = VideoState.Paused;
}
public void Stop()
{
if (Video == null) { return; }
if (State == VideoState.Stopped)
{
return;
}
timer.Stop();
timer.Reset();
lastTimestamp = 0;
timeElapsed = 0;
Dav1dfile.df_reset(Video.Handle);
State = VideoState.Stopped;
}
public void Unload()
{
Stop();
Video = null;
}
public void Update()
{
if (Video == null) { return; }
}
public void Render()
{
if (Video == null || State == VideoState.Stopped)
{
return;
}
timeElapsed += (timer.Elapsed.TotalMilliseconds - lastTimestamp) * PlaybackSpeed;
lastTimestamp = timer.Elapsed.TotalMilliseconds;
int thisFrame = ((int) (timeElapsed / (1000.0 / Video.FramesPerSecond)));
if (thisFrame > currentFrame)
{
profilingTimer.Restart();
int readResult = Dav1dfile.df_readvideo(
Video.Handle,
thisFrame - currentFrame,
out var yData,
out var uData,
out var vData,
out var yDataLength,
out var uvDataLength,
out var yStride,
out var uvStride
);
profilingTimer.Stop();
System.Console.WriteLine($"Frame decoded in: {profilingTimer.Elapsed.TotalMilliseconds}");
if (readResult == 1 || currentFrame == -1)
{
UpdateRenderTexture(yData, uData, vData, yDataLength, uvDataLength, yStride, uvStride);
}
currentFrame = thisFrame;
}
bool ended = Dav1dfile.df_eos(Video.Handle) == 1;
if (ended)
{
timer.Stop();
timer.Reset();
Dav1dfile.df_reset(Video.Handle);
if (Loop)
{
// Start over!
InitializeDav1dStream();
timer.Start();
}
else
{
State = VideoState.Stopped;
}
}
}
private void UpdateRenderTexture(IntPtr yData, IntPtr uData, IntPtr vData, uint yDataLength, uint uvDataLength, uint yStride, uint uvStride)
{
var commandBuffer = GraphicsDevice.AcquireCommandBuffer();
commandBuffer.SetTextureDataYUV(
yTexture,
uTexture,
vTexture,
yData,
uData,
vData,
yDataLength,
uvDataLength,
yStride,
uvStride
);
commandBuffer.BeginRenderPass(
new ColorAttachmentInfo(RenderTexture, Color.Black)
);
commandBuffer.BindGraphicsPipeline(GraphicsDevice.VideoPipeline);
commandBuffer.BindFragmentSamplers(
new TextureSamplerBinding(yTexture, LinearSampler),
new TextureSamplerBinding(uTexture, LinearSampler),
new TextureSamplerBinding(vTexture, LinearSampler)
);
commandBuffer.DrawPrimitives(0, 1, 0, 0);
commandBuffer.EndRenderPass();
GraphicsDevice.Submit(commandBuffer);
}
private static Texture CreateRenderTexture(GraphicsDevice graphicsDevice, int width, int height)
{
return Texture.CreateTexture2D(
graphicsDevice,
(uint) width,
(uint) height,
TextureFormat.R8G8B8A8,
TextureUsageFlags.ColorTarget | TextureUsageFlags.Sampler
);
}
private static Texture CreateSubTexture(GraphicsDevice graphicsDevice, int width, int height)
{
return Texture.CreateTexture2D(
graphicsDevice,
(uint) width,
(uint) height,
TextureFormat.R8,
TextureUsageFlags.Sampler
);
}
private void InitializeDav1dStream()
{
// Grab the first video frame ASAP.
Dav1dfile.df_readvideo(
Video.Handle,
1,
out var _,
out var _,
out var _,
out var _,
out var _,
out var _,
out var _);
currentFrame = -1;
}
protected virtual void Dispose(bool disposing)
{
if (!disposed)
{
if (disposing)
{
// dispose managed state (managed objects)
RenderTexture.Dispose();
yTexture.Dispose();
uTexture.Dispose();
vTexture.Dispose();
}
disposed = true;
}
}
~VideoPlayer()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
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);
}
}
}