Show More
Commit Description:
Various UI improvements.
Commit Description:
Various UI improvements.
References:
File last commit:
Show/Diff file:
Action:
FNA/src/Media/Xiph/VideoPlayer.cs
776 lines | 15.9 KiB | text/x-csharp | CSharpLexer
776 lines | 15.9 KiB | text/x-csharp | CSharpLexer
r0 | #region License | |||
/* FNA - XNA4 Reimplementation for Desktop Platforms | ||||
* Copyright 2009-2020 Ethan Lee and the MonoGame Team | ||||
* | ||||
* Released under the Microsoft Public License. | ||||
* See LICENSE for details. | ||||
*/ | ||||
#endregion | ||||
#region Using Statements | ||||
using System; | ||||
using System.Diagnostics; | ||||
using System.Runtime.InteropServices; | ||||
using Microsoft.Xna.Framework.Audio; | ||||
using Microsoft.Xna.Framework.Graphics; | ||||
#endregion | ||||
namespace Microsoft.Xna.Framework.Media | ||||
{ | ||||
public sealed class VideoPlayer : IDisposable | ||||
{ | ||||
#region Hardware-accelerated YUV -> RGBA | ||||
private Effect shaderProgram; | ||||
private IntPtr stateChangesPtr; | ||||
private Texture2D[] yuvTextures = new Texture2D[3]; | ||||
private Viewport viewport; | ||||
private static VertexPositionTexture[] vertices = new VertexPositionTexture[] | ||||
{ | ||||
new VertexPositionTexture( | ||||
new Vector3(-1.0f, -1.0f, 0.0f), | ||||
new Vector2(0.0f, 1.0f) | ||||
), | ||||
new VertexPositionTexture( | ||||
new Vector3(1.0f, -1.0f, 0.0f), | ||||
new Vector2(1.0f, 1.0f) | ||||
), | ||||
new VertexPositionTexture( | ||||
new Vector3(-1.0f, 1.0f, 0.0f), | ||||
new Vector2(0.0f, 0.0f) | ||||
), | ||||
new VertexPositionTexture( | ||||
new Vector3(1.0f, 1.0f, 0.0f), | ||||
new Vector2(1.0f, 0.0f) | ||||
) | ||||
}; | ||||
private VertexBufferBinding vertBuffer; | ||||
// Used to restore our previous GL state. | ||||
private Texture[] oldTextures= new Texture[3]; | ||||
private SamplerState[] oldSamplers = new SamplerState[3]; | ||||
private RenderTargetBinding[] oldTargets; | ||||
private VertexBufferBinding[] oldBuffers; | ||||
private BlendState prevBlend; | ||||
private DepthStencilState prevDepthStencil; | ||||
private RasterizerState prevRasterizer; | ||||
private Viewport prevViewport; | ||||
private void GL_initialize() | ||||
{ | ||||
// Load the YUV->RGBA Effect | ||||
shaderProgram = new Effect( | ||||
currentDevice, | ||||
Resources.YUVToRGBAEffect | ||||
); | ||||
unsafe | ||||
{ | ||||
stateChangesPtr = Marshal.AllocHGlobal( | ||||
sizeof(MojoShader.MOJOSHADER_effectStateChanges) | ||||
); | ||||
} | ||||
// Allocate the vertex buffer | ||||
vertBuffer = new VertexBufferBinding( | ||||
new VertexBuffer( | ||||
currentDevice, | ||||
VertexPositionTexture.VertexDeclaration, | ||||
4, | ||||
BufferUsage.WriteOnly | ||||
) | ||||
); | ||||
vertBuffer.VertexBuffer.SetData(vertices); | ||||
} | ||||
private void GL_dispose() | ||||
{ | ||||
if (currentDevice == null) | ||||
{ | ||||
// We never initialized to begin with... | ||||
return; | ||||
} | ||||
currentDevice = null; | ||||
// Delete the Effect | ||||
if (shaderProgram != null) | ||||
{ | ||||
shaderProgram.Dispose(); | ||||
} | ||||
if (stateChangesPtr != IntPtr.Zero) | ||||
{ | ||||
Marshal.FreeHGlobal(stateChangesPtr); | ||||
} | ||||
// Delete the vertex buffer | ||||
if (vertBuffer.VertexBuffer != null) | ||||
{ | ||||
vertBuffer.VertexBuffer.Dispose(); | ||||
} | ||||
// Delete the textures if they exist | ||||
for (int i = 0; i < 3; i += 1) | ||||
{ | ||||
if (yuvTextures[i] != null) | ||||
{ | ||||
yuvTextures[i].Dispose(); | ||||
} | ||||
} | ||||
} | ||||
private void GL_setupTextures( | ||||
int yWidth, | ||||
int yHeight, | ||||
int uvWidth, | ||||
int uvHeight | ||||
) { | ||||
// Allocate YUV GL textures | ||||
for (int i = 0; i < 3; i += 1) | ||||
{ | ||||
if (yuvTextures[i] != null) | ||||
{ | ||||
yuvTextures[i].Dispose(); | ||||
} | ||||
} | ||||
yuvTextures[0] = new Texture2D( | ||||
currentDevice, | ||||
yWidth, | ||||
yHeight, | ||||
false, | ||||
SurfaceFormat.Alpha8 | ||||
); | ||||
yuvTextures[1] = new Texture2D( | ||||
currentDevice, | ||||
uvWidth, | ||||
uvHeight, | ||||
false, | ||||
SurfaceFormat.Alpha8 | ||||
); | ||||
yuvTextures[2] = new Texture2D( | ||||
currentDevice, | ||||
uvWidth, | ||||
uvHeight, | ||||
false, | ||||
SurfaceFormat.Alpha8 | ||||
); | ||||
// Precalculate the viewport | ||||
viewport = new Viewport(0, 0, yWidth, yHeight); | ||||
} | ||||
private void GL_pushState() | ||||
{ | ||||
// Begin the effect, flagging to restore previous state on end | ||||
currentDevice.GLDevice.BeginPassRestore( | ||||
shaderProgram.glEffect, | ||||
stateChangesPtr | ||||
); | ||||
// Prep our samplers | ||||
for (int i = 0; i < 3; i += 1) | ||||
{ | ||||
oldTextures[i] = currentDevice.Textures[i]; | ||||
oldSamplers[i] = currentDevice.SamplerStates[i]; | ||||
currentDevice.Textures[i] = yuvTextures[i]; | ||||
currentDevice.SamplerStates[i] = SamplerState.LinearClamp; | ||||
} | ||||
// Prep buffers | ||||
oldBuffers = currentDevice.GetVertexBuffers(); | ||||
currentDevice.SetVertexBuffers(vertBuffer); | ||||
// Prep target bindings | ||||
oldTargets = currentDevice.GetRenderTargets(); | ||||
currentDevice.GLDevice.SetRenderTargets( | ||||
videoTexture, | ||||
null, | ||||
DepthFormat.None | ||||
); | ||||
// Prep render state | ||||
prevBlend = currentDevice.BlendState; | ||||
prevDepthStencil = currentDevice.DepthStencilState; | ||||
prevRasterizer = currentDevice.RasterizerState; | ||||
currentDevice.BlendState = BlendState.Opaque; | ||||
currentDevice.DepthStencilState = DepthStencilState.None; | ||||
currentDevice.RasterizerState = RasterizerState.CullNone; | ||||
// Prep viewport | ||||
prevViewport = currentDevice.Viewport; | ||||
currentDevice.GLDevice.SetViewport(viewport); | ||||
} | ||||
private void GL_popState() | ||||
{ | ||||
// End the effect, restoring the previous shader state | ||||
currentDevice.GLDevice.EndPassRestore(shaderProgram.glEffect); | ||||
// Restore GL state | ||||
currentDevice.BlendState = prevBlend; | ||||
currentDevice.DepthStencilState = prevDepthStencil; | ||||
currentDevice.RasterizerState = prevRasterizer; | ||||
prevBlend = null; | ||||
prevDepthStencil = null; | ||||
prevRasterizer = null; | ||||
/* Restore targets using GLDevice directly. | ||||
* This prevents accidental clearing of previously bound targets. | ||||
*/ | ||||
if (oldTargets == null || oldTargets.Length == 0) | ||||
{ | ||||
currentDevice.GLDevice.SetRenderTargets( | ||||
null, | ||||
null, | ||||
DepthFormat.None | ||||
); | ||||
} | ||||
else | ||||
{ | ||||
IRenderTarget oldTarget = oldTargets[0].RenderTarget as IRenderTarget; | ||||
currentDevice.GLDevice.SetRenderTargets( | ||||
oldTargets, | ||||
oldTarget.DepthStencilBuffer, | ||||
oldTarget.DepthStencilFormat | ||||
); | ||||
} | ||||
oldTargets = null; | ||||
// Set viewport AFTER setting targets! | ||||
currentDevice.GLDevice.SetViewport(prevViewport); | ||||
// Restore buffers | ||||
currentDevice.SetVertexBuffers(oldBuffers); | ||||
oldBuffers = null; | ||||
// Restore samplers | ||||
for (int i = 0; i < 3; i += 1) | ||||
{ | ||||
/* The application may have set a texture ages | ||||
* ago, only to not unset after disposing. We | ||||
* have to avoid an ObjectDisposedException! | ||||
*/ | ||||
if (oldTextures[i] == null || !oldTextures[i].IsDisposed) | ||||
{ | ||||
currentDevice.Textures[i] = oldTextures[i]; | ||||
} | ||||
currentDevice.SamplerStates[i] = oldSamplers[i]; | ||||
oldTextures[i] = null; | ||||
oldSamplers[i] = null; | ||||
} | ||||
} | ||||
#endregion | ||||
#region Public Member Data: XNA VideoPlayer Implementation | ||||
public bool IsDisposed | ||||
{ | ||||
get; | ||||
private set; | ||||
} | ||||
public bool IsLooped | ||||
{ | ||||
get; | ||||
set; | ||||
} | ||||
private bool backing_ismuted; | ||||
public bool IsMuted | ||||
{ | ||||
get | ||||
{ | ||||
return backing_ismuted; | ||||
} | ||||
set | ||||
{ | ||||
backing_ismuted = value; | ||||
UpdateVolume(); | ||||
} | ||||
} | ||||
public TimeSpan PlayPosition | ||||
{ | ||||
get | ||||
{ | ||||
return timer.Elapsed; | ||||
} | ||||
} | ||||
public MediaState State | ||||
{ | ||||
get; | ||||
private set; | ||||
} | ||||
public Video Video | ||||
{ | ||||
get; | ||||
private set; | ||||
} | ||||
private float backing_volume; | ||||
public float Volume | ||||
{ | ||||
get | ||||
{ | ||||
return backing_volume; | ||||
} | ||||
set | ||||
{ | ||||
if (value > 1.0f) | ||||
{ | ||||
backing_volume = 1.0f; | ||||
} | ||||
else if (value < 0.0f) | ||||
{ | ||||
backing_volume = 0.0f; | ||||
} | ||||
else | ||||
{ | ||||
backing_volume = value; | ||||
} | ||||
UpdateVolume(); | ||||
} | ||||
} | ||||
#endregion | ||||
#region Private Member Data: XNA VideoPlayer Implementation | ||||
// We use this to update our PlayPosition. | ||||
private Stopwatch timer; | ||||
// Store this to optimize things on our end. | ||||
private RenderTargetBinding[] videoTexture; | ||||
// We need to access the GraphicsDevice frequently. | ||||
private GraphicsDevice currentDevice; | ||||
#endregion | ||||
#region Private Member Data: Theorafile | ||||
private IntPtr yuvData; | ||||
private int currentFrame; | ||||
private const int AUDIO_BUFFER_SIZE = 4096 * 2; | ||||
private static readonly float[] audioData = new float[AUDIO_BUFFER_SIZE]; | ||||
private static GCHandle audioHandle = GCHandle.Alloc(audioData, GCHandleType.Pinned); | ||||
private IntPtr audioDataPtr = audioHandle.AddrOfPinnedObject(); | ||||
#endregion | ||||
#region Private Member Data: Audio Stream | ||||
private DynamicSoundEffectInstance audioStream; | ||||
#endregion | ||||
#region Private Methods: XNA VideoPlayer Implementation | ||||
private void checkDisposed() | ||||
{ | ||||
if (IsDisposed) | ||||
{ | ||||
throw new ObjectDisposedException("VideoPlayer"); | ||||
} | ||||
} | ||||
#endregion | ||||
#region Private Methods: Audio Stream | ||||
private void UpdateVolume() | ||||
{ | ||||
if (audioStream == null) | ||||
{ | ||||
return; | ||||
} | ||||
if (IsMuted) | ||||
{ | ||||
audioStream.Volume = 0.0f; | ||||
} | ||||
else | ||||
{ | ||||
/* FIXME: Works around MasterVolume only temporarily! | ||||
* We need to detach this source from the AL listener properties. | ||||
* -flibit | ||||
*/ | ||||
audioStream.Volume = Volume * (1.0f / SoundEffect.MasterVolume); | ||||
} | ||||
} | ||||
#endregion | ||||
#region Public Methods: XNA VideoPlayer Implementation | ||||
public VideoPlayer() | ||||
{ | ||||
// Initialize public members. | ||||
IsDisposed = false; | ||||
IsLooped = false; | ||||
IsMuted = false; | ||||
State = MediaState.Stopped; | ||||
Volume = 1.0f; | ||||
// Initialize private members. | ||||
timer = new Stopwatch(); | ||||
videoTexture = new RenderTargetBinding[1]; | ||||
} | ||||
public void Dispose() | ||||
{ | ||||
if (IsDisposed) | ||||
{ | ||||
return; | ||||
} | ||||
// Stop the VideoPlayer. This gets almost everything... | ||||
Stop(); | ||||
// Destroy the other GL bits. | ||||
GL_dispose(); | ||||
// Dispose the DynamicSoundEffectInstance | ||||
if (audioStream != null) | ||||
{ | ||||
audioStream.Dispose(); | ||||
audioStream = null; | ||||
} | ||||
// Dispose the Texture. | ||||
if (videoTexture[0].RenderTarget != null) | ||||
{ | ||||
videoTexture[0].RenderTarget.Dispose(); | ||||
} | ||||
// Free the YUV buffer | ||||
if (yuvData != IntPtr.Zero) | ||||
{ | ||||
Marshal.FreeHGlobal(yuvData); | ||||
yuvData = IntPtr.Zero; | ||||
} | ||||
// Okay, we out. | ||||
IsDisposed = true; | ||||
} | ||||
public Texture2D GetTexture() | ||||
{ | ||||
checkDisposed(); | ||||
if (Video == null) | ||||
{ | ||||
throw new InvalidOperationException(); | ||||
} | ||||
// Be sure we can even get something from Theorafile... | ||||
if ( State == MediaState.Stopped || | ||||
Video.theora == IntPtr.Zero || | ||||
Theorafile.tf_hasvideo(Video.theora) == 0 ) | ||||
{ | ||||
// Screw it, give them the old one. | ||||
return videoTexture[0].RenderTarget as Texture2D; | ||||
} | ||||
int thisFrame = (int) (timer.Elapsed.TotalMilliseconds / (1000.0 / Video.fps)); | ||||
if (thisFrame > currentFrame) | ||||
{ | ||||
// Only update the textures if we need to! | ||||
if (Theorafile.tf_readvideo( | ||||
Video.theora, | ||||
yuvData, | ||||
thisFrame - currentFrame | ||||
) == 1 || currentFrame == -1) { | ||||
UpdateTexture(); | ||||
} | ||||
currentFrame = thisFrame; | ||||
} | ||||
// Check for the end... | ||||
bool ended = Theorafile.tf_eos(Video.theora) == 1; | ||||
if (audioStream != null) | ||||
{ | ||||
ended &= audioStream.PendingBufferCount == 0; | ||||
} | ||||
if (ended) | ||||
{ | ||||
// FIXME: This is part of the Duration hack! | ||||
if (Video.needsDurationHack) | ||||
{ | ||||
Video.Duration = timer.Elapsed; // FIXME: Frames * FPS? -flibit | ||||
} | ||||
// Stop and reset the timer. If we're looping, the loop will start it again. | ||||
timer.Stop(); | ||||
timer.Reset(); | ||||
// Kill whatever audio/video we've got | ||||
if (audioStream != null) | ||||
{ | ||||
audioStream.Stop(); | ||||
audioStream.Dispose(); | ||||
audioStream = null; | ||||
} | ||||
// Reset the stream no matter what happens next | ||||
Theorafile.tf_reset(Video.theora); | ||||
// If looping, go back to the start. Otherwise, we'll be exiting. | ||||
if (IsLooped) | ||||
{ | ||||
// Starting over! | ||||
InitializeTheoraStream(); | ||||
// Start! Again! | ||||
timer.Start(); | ||||
if (audioStream != null) | ||||
{ | ||||
audioStream.Play(); | ||||
} | ||||
} | ||||
else | ||||
{ | ||||
// We out | ||||
State = MediaState.Stopped; | ||||
} | ||||
} | ||||
// Finally. | ||||
return videoTexture[0].RenderTarget as Texture2D; | ||||
} | ||||
public void Play(Video video) | ||||
{ | ||||
checkDisposed(); | ||||
// We need to assign this regardless of what happens next. | ||||
Video = video; | ||||
// FIXME: This is a part of the Duration hack! | ||||
if (Video.needsDurationHack) | ||||
{ | ||||
Video.Duration = TimeSpan.MaxValue; | ||||
} | ||||
// Check the player state before attempting anything. | ||||
if (State != MediaState.Stopped) | ||||
{ | ||||
return; | ||||
} | ||||
// Update the player state now, before initializing | ||||
State = MediaState.Playing; | ||||
// Carve out YUV buffer before doing any decoder work | ||||
if (yuvData != IntPtr.Zero) | ||||
{ | ||||
Marshal.FreeHGlobal(yuvData); | ||||
} | ||||
yuvData = Marshal.AllocHGlobal( | ||||
(Video.yWidth * Video.yHeight) + | ||||
(Video.uvWidth * Video.uvHeight * 2) | ||||
); | ||||
// Hook up the decoder to this player | ||||
InitializeTheoraStream(); | ||||
// Set up the texture data | ||||
if (Theorafile.tf_hasvideo(Video.theora) == 1) | ||||
{ | ||||
// The VideoPlayer will use the GraphicsDevice that is set now. | ||||
if (currentDevice != Video.GraphicsDevice) | ||||
{ | ||||
GL_dispose(); | ||||
currentDevice = Video.GraphicsDevice; | ||||
GL_initialize(); | ||||
} | ||||
RenderTargetBinding overlap = videoTexture[0]; | ||||
videoTexture[0] = new RenderTargetBinding( | ||||
new RenderTarget2D( | ||||
currentDevice, | ||||
Video.yWidth, | ||||
Video.yHeight, | ||||
false, | ||||
SurfaceFormat.Color, | ||||
DepthFormat.None, | ||||
0, | ||||
RenderTargetUsage.PreserveContents | ||||
) | ||||
); | ||||
if (overlap.RenderTarget != null) | ||||
{ | ||||
overlap.RenderTarget.Dispose(); | ||||
} | ||||
GL_setupTextures( | ||||
Video.yWidth, | ||||
Video.yHeight, | ||||
Video.uvWidth, | ||||
Video.uvHeight | ||||
); | ||||
} | ||||
// The player can finally start now! | ||||
timer.Start(); | ||||
if (audioStream != null) | ||||
{ | ||||
audioStream.Play(); | ||||
} | ||||
} | ||||
public void Stop() | ||||
{ | ||||
checkDisposed(); | ||||
// Check the player state before attempting anything. | ||||
if (State == MediaState.Stopped) | ||||
{ | ||||
return; | ||||
} | ||||
// Update the player state. | ||||
State = MediaState.Stopped; | ||||
// Wait for the player to end if it's still going. | ||||
timer.Stop(); | ||||
timer.Reset(); | ||||
if (audioStream != null) | ||||
{ | ||||
audioStream.Stop(); | ||||
audioStream.Dispose(); | ||||
audioStream = null; | ||||
} | ||||
Theorafile.tf_reset(Video.theora); | ||||
} | ||||
public void Pause() | ||||
{ | ||||
checkDisposed(); | ||||
// Check the player state before attempting anything. | ||||
if (State != MediaState.Playing) | ||||
{ | ||||
return; | ||||
} | ||||
// Update the player state. | ||||
State = MediaState.Paused; | ||||
// Pause timer, audio. | ||||
timer.Stop(); | ||||
if (audioStream != null) | ||||
{ | ||||
audioStream.Pause(); | ||||
} | ||||
} | ||||
public void Resume() | ||||
{ | ||||
checkDisposed(); | ||||
// Check the player state before attempting anything. | ||||
if (State != MediaState.Paused) | ||||
{ | ||||
return; | ||||
} | ||||
// Update the player state. | ||||
State = MediaState.Playing; | ||||
// Unpause timer, audio. | ||||
timer.Start(); | ||||
if (audioStream != null) | ||||
{ | ||||
audioStream.Resume(); | ||||
} | ||||
} | ||||
#endregion | ||||
#region Private Theora Audio Stream Methods | ||||
private void OnBufferRequest(object sender, EventArgs args) | ||||
{ | ||||
int samples = Theorafile.tf_readaudio( | ||||
Video.theora, | ||||
audioDataPtr, | ||||
AUDIO_BUFFER_SIZE | ||||
); | ||||
if (samples > 0) | ||||
{ | ||||
audioStream.SubmitFloatBufferEXT( | ||||
audioData, | ||||
0, | ||||
samples | ||||
); | ||||
} | ||||
else if (Theorafile.tf_eos(Video.theora) == 1) | ||||
{ | ||||
// Okay, we ran out. No need for this! | ||||
audioStream.BufferNeeded -= OnBufferRequest; | ||||
} | ||||
} | ||||
#endregion | ||||
#region Private Theora Video Stream Methods | ||||
private void UpdateTexture() | ||||
{ | ||||
// Prepare YUV GL textures with our current frame data | ||||
currentDevice.GLDevice.SetTextureDataYUV( | ||||
yuvTextures, | ||||
yuvData | ||||
); | ||||
// Draw the YUV textures to the framebuffer with our shader. | ||||
GL_pushState(); | ||||
currentDevice.DrawPrimitives( | ||||
PrimitiveType.TriangleStrip, | ||||
0, | ||||
2 | ||||
); | ||||
GL_popState(); | ||||
} | ||||
#endregion | ||||
#region Theora Decoder Hookup Method | ||||
private void InitializeTheoraStream() | ||||
{ | ||||
// Grab the first video frame ASAP. | ||||
while (Theorafile.tf_readvideo(Video.theora, yuvData, 1) == 0); | ||||
// Grab the first bit of audio. We're trying to start the decoding ASAP. | ||||
if (Theorafile.tf_hasaudio(Video.theora) == 1) | ||||
{ | ||||
int channels, samplerate; | ||||
Theorafile.tf_audioinfo(Video.theora, out channels, out samplerate); | ||||
audioStream = new DynamicSoundEffectInstance( | ||||
samplerate, | ||||
(AudioChannels) channels | ||||
); | ||||
audioStream.BufferNeeded += OnBufferRequest; | ||||
UpdateVolume(); | ||||
// Fill and queue the buffers. | ||||
for (int i = 0; i < 4; i += 1) | ||||
{ | ||||
OnBufferRequest(audioStream, EventArgs.Empty); | ||||
if (audioStream.PendingBufferCount == i) | ||||
{ | ||||
break; | ||||
} | ||||
} | ||||
} | ||||
currentFrame = -1; | ||||
} | ||||
#endregion | ||||
} | ||||
} | ||||