using System;
using System.Threading;
using System.Runtime.InteropServices;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using DirectShowLib;
namespace SeeSharp.Xna.Video
{
///
/// Describes the state of a video player
///
public enum VideoState
{
Playing,
Paused,
Stopped
}
///
/// Enables Video Playback in Microsoft XNA
///
public class VideoPlayer : ISampleGrabberCB, IDisposable
{
#region Media Type GUIDs
private Guid MEDIATYPE_Video = new Guid(0x73646976, 0x0000, 0x0010, 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71);
private Guid MEDIASUBTYPE_RGB24 = new Guid(0xe436eb7d, 0x524f, 0x11ce, 0x9f, 0x53, 0x00, 0x20, 0xaf, 0x0b, 0xa7, 0x70);
private Guid FORMAT_VideoInfo = new Guid(0x05589f80, 0xc356, 0x11ce, 0xbf, 0x01, 0x00, 0xaa, 0x00, 0x55, 0x59, 0x5a);
#endregion
#region Private Fields
///
/// The Main FilterGraph Com Object
///
private FilterGraph fg = null;
///
/// The GraphBuilder interface ref
///
private IGraphBuilder gb = null;
///
/// The MediaControl interface ref
///
private IMediaControl mc = null;
///
/// The MediaEvent interface ref
///
private IMediaEventEx me = null;
///
/// The MediaPosition interface ref
///
private IMediaPosition mp = null;
///
/// The MediaSeeking interface ref
///
private IMediaSeeking ms = null;
///
/// Thread used to update the video's Texture2D data
///
private Thread updateThread;
///
/// Thread used to wait untill the video is complete, then invoke the OnVideoComplete EventHandler
///
private Thread waitThread;
///
/// The Video File to play
///
private string filename;
///
/// Is a new frame avaliable to update?
///
private bool frameAvailable = false;
///
/// Array to hold the raw data from the DirectShow video stream.
///
private byte[] bgrData;
///
/// The RGBA frame bytes used to set the data in the Texture2D Output Frame
///
private byte[] videoFrameBytes;
///
/// Private Video Width
///
private int videoWidth = 0;
///
/// Private Video Height
///
private int videoHeight = 0;
///
/// Private Texture2D to render video to. Created in the Video Player Constructor.
///
private Texture2D outputFrame;
///
/// Average Time per Frame in milliseconds
///
private long avgTimePerFrame;
///
/// BitRate of the currently loaded video
///
private int bitRate;
///
/// Current state of the video player
///
private VideoState currentState;
///
/// Is Disposed?
///
private bool isDisposed = false;
///
/// Current time position
///
private long currentPosition;
///
/// Video duration
///
private long videoDuration;
///
/// How transparent the video frame is.
/// Takes effect on the next frame after this is updated
/// Max Value: 255 - Opaque
/// Min Value: 0 - Transparent
///
private byte alphaTransparency = 255;
#endregion
#region Public Properties
///
/// Automatically updated video frame. Render this to the screen using a SpriteBatch.
///
public Texture2D OutputFrame
{
get
{
return outputFrame;
}
}
///
/// Width of the loaded video
///
public int VideoWidth
{
get
{
return videoWidth;
}
}
///
/// Height of the loaded video
///
public int VideoHeight
{
get
{
return videoHeight;
}
}
///
/// Gets or Sets the current position of playback in seconds
///
public double CurrentPosition
{
get
{
return (double)currentPosition / 10000000;
}
set
{
if (value < 0)
value = 0;
if (value > Duration)
value = Duration;
DsError.ThrowExceptionForHR(mp.put_CurrentPosition(value));
currentPosition = (long)value * 10000000;
}
}
///
/// Returns the current position of playback, formatted as a time string (HH:MM:SS)
///
public string CurrentPositionAsTimeString
{
get
{
double seconds = (double)currentPosition / 10000000;
double minutes = seconds / 60;
double hours = minutes / 60;
int realHours = (int)Math.Floor(hours);
minutes -= realHours * 60;
int realMinutes = (int)Math.Floor(minutes);
seconds -= realMinutes * 60;
int realSeconds = (int)Math.Floor(seconds);
return (realHours < 10 ? "0" + realHours.ToString() : realHours.ToString()) + ":" + (realMinutes < 10 ? "0" + realMinutes.ToString() : realMinutes.ToString()) + ":" + (realSeconds < 10 ? "0" + realSeconds.ToString() : realSeconds.ToString());
}
}
///
/// Total duration in seconds
///
public double Duration
{
get
{
return (double)videoDuration / 10000000;
}
}
///
/// Returns the duration of the video, formatted as a time string (HH:MM:SS)
///
public string DurationAsTimeString
{
get
{
double seconds = (double)videoDuration / 10000000;
double minutes = seconds / 60;
double hours = minutes / 60;
int realHours = (int)Math.Floor(hours);
minutes -= realHours * 60;
int realMinutes = (int)Math.Floor(minutes);
seconds -= realMinutes * 60;
int realSeconds = (int)Math.Floor(seconds);
return (realHours < 10 ? "0" + realHours.ToString() : realHours.ToString()) + ":" + (realMinutes < 10 ? "0" + realMinutes.ToString() : realMinutes.ToString()) + ":" + (realSeconds < 10 ? "0" + realSeconds.ToString() : realSeconds.ToString());
}
}
///
/// Currently Loaded Video File
///
public string FileName
{
get
{
return filename;
}
}
///
/// Gets or Sets the current state of the video player
///
public VideoState CurrentState
{
get
{
return currentState;
}
set
{
switch (value)
{
case VideoState.Playing:
Play();
break;
case VideoState.Paused:
Pause();
break;
case VideoState.Stopped:
Stop();
break;
}
}
}
///
/// Event which occurs when the video stops playing once it has reached the end of the file
///
public event EventHandler OnVideoComplete;
///
/// Is Disposed?
///
public bool IsDisposed
{
get
{
return isDisposed;
}
}
///
/// Number of Frames Per Second in the video file.
/// Returns -1 if this cannot be calculated.
///
public int FramesPerSecond
{
get
{
if (avgTimePerFrame == 0)
return -1;
float frameTime = (float)avgTimePerFrame / 10000000.0f;
float framesPS = 1.0f / frameTime;
return (int)Math.Round(framesPS, 0, MidpointRounding.ToEven);
}
}
///
/// The number of milliseconds between each frame
/// Returns -1 if this cannot be calculated
///
public float MillisecondsPerFrame
{
get
{
if (avgTimePerFrame == 0)
return -1;
return (float)avgTimePerFrame / 10000.0f;
}
}
///
/// Gets or sets how transparent the video frame is.
/// Takes effect on the next frame after this is updated
/// Max Value: 255 - Opaque
/// Min Value: 0 - Transparent
///
public byte AlphaTransparency
{
get
{
return alphaTransparency;
}
set
{
alphaTransparency = value;
}
}
#endregion
#region Constructor
///
/// Creates a new Video Player. Automatically creates the required Texture2D on the specificied GraphicsDevice.
///
/// The video file to open
/// XNA Graphics Device
public VideoPlayer(string FileName, GraphicsDevice graphicsDevice)
{
try
{
// Set video state
currentState = VideoState.Stopped;
// Store Filename
filename = FileName;
// Open DirectShow Interfaces
InitInterfaces();
// Create a SampleGrabber Filter and add it to the FilterGraph
SampleGrabber sg = new SampleGrabber();
ISampleGrabber sampleGrabber = (ISampleGrabber)sg;
DsError.ThrowExceptionForHR(gb.AddFilter((IBaseFilter)sg, "Grabber"));
// Setup Media type info for the SampleGrabber
AMMediaType mt = new AMMediaType();
mt.majorType = MEDIATYPE_Video; // Video
mt.subType = MEDIASUBTYPE_RGB24; // RGB24
mt.formatType = FORMAT_VideoInfo; // VideoInfo
DsError.ThrowExceptionForHR(sampleGrabber.SetMediaType(mt));
// Construct the rest of the FilterGraph
DsError.ThrowExceptionForHR(gb.RenderFile(filename, null));
// Set SampleGrabber Properties
DsError.ThrowExceptionForHR(sampleGrabber.SetBufferSamples(true));
DsError.ThrowExceptionForHR(sampleGrabber.SetOneShot(false));
DsError.ThrowExceptionForHR(sampleGrabber.SetCallback((ISampleGrabberCB)this, 1));
// Hide Default Video Window
IVideoWindow pVideoWindow = (IVideoWindow)gb;
DsError.ThrowExceptionForHR(pVideoWindow.put_AutoShow(OABool.False));
// Create AMMediaType to capture video information
AMMediaType MediaType = new AMMediaType();
DsError.ThrowExceptionForHR(sampleGrabber.GetConnectedMediaType(MediaType));
VideoInfoHeader pVideoHeader = new VideoInfoHeader();
Marshal.PtrToStructure(MediaType.formatPtr, pVideoHeader);
// Store video information
videoHeight = pVideoHeader.BmiHeader.Height;
videoWidth = pVideoHeader.BmiHeader.Width;
avgTimePerFrame = pVideoHeader.AvgTimePerFrame;
bitRate = pVideoHeader.BitRate;
DsError.ThrowExceptionForHR(ms.GetDuration(out videoDuration));
// Create byte arrays to hold video data
videoFrameBytes = new byte[(videoHeight * videoWidth) * 4]; // RGBA format (4 bytes per pixel)
bgrData = new byte[(videoHeight * videoWidth) * 3]; // BGR24 format (3 bytes per pixel)
// Create Output Frame Texture2D with the height and width of the video
outputFrame = new Texture2D(graphicsDevice, videoWidth, videoHeight, 1, TextureUsage.None, SurfaceFormat.Color);
}
catch
{
throw new Exception("Unable to Load or Play the video file");
}
}
#endregion
#region DirectShow Interface Management
///
/// Initialises DirectShow interfaces
///
private void InitInterfaces()
{
fg = new FilterGraph();
gb = (IGraphBuilder)fg;
mc = (IMediaControl)fg;
me = (IMediaEventEx)fg;
ms = (IMediaSeeking)fg;
mp = (IMediaPosition)fg;
}
///
/// Closes DirectShow interfaces
///
private void CloseInterfaces()
{
if (me != null)
{
DsError.ThrowExceptionForHR(mc.Stop());
//0x00008001 = WM_GRAPHNOTIFY
DsError.ThrowExceptionForHR(me.SetNotifyWindow(IntPtr.Zero, 0x00008001, IntPtr.Zero));
}
mc = null;
me = null;
gb = null;
ms = null;
mp = null;
if (fg != null)
Marshal.ReleaseComObject(fg);
fg = null;
}
#endregion
#region Update and Media Control
///
/// Updates the Output Frame data using data from the video stream. Call this in Game.Update().
///
public void Update()
{
// Remove the OutputFrame from the GraphicsDevice to prevent an InvalidOperationException on the SetData line.
if (outputFrame.GraphicsDevice.Textures[0] == outputFrame)
{
outputFrame.GraphicsDevice.Textures[0] = null;
}
// Set video data into the Output Frame
outputFrame.SetData(videoFrameBytes);
// Update current position read-out
DsError.ThrowExceptionForHR(ms.GetCurrentPosition(out currentPosition));
}
///
/// Starts playing the video
///
public void Play()
{
if (currentState != VideoState.Playing)
{
// Create video threads
updateThread = new Thread(new ThreadStart(UpdateBuffer));
waitThread = new Thread(new ThreadStart(WaitForCompletion));
// Start the FilterGraph
DsError.ThrowExceptionForHR(mc.Run());
// Start Threads
updateThread.Start();
waitThread.Start();
// Update VideoState
currentState = VideoState.Playing;
}
}
///
/// Pauses the video
///
public void Pause()
{
// End threads
if (updateThread != null)
updateThread.Abort();
updateThread = null;
if (waitThread != null)
waitThread.Abort();
waitThread = null;
// Stop the FilterGraph (but remembers the current position)
DsError.ThrowExceptionForHR(mc.Stop());
// Update VideoState
currentState = VideoState.Paused;
}
///
/// Stops playing the video
///
public void Stop()
{
// End Threads
if (updateThread != null)
updateThread.Abort();
updateThread = null;
if (waitThread != null)
waitThread.Abort();
waitThread = null;
// Stop the FilterGraph
DsError.ThrowExceptionForHR(mc.Stop());
// Reset the current position
DsError.ThrowExceptionForHR(ms.SetPositions(0, AMSeekingSeekingFlags.AbsolutePositioning, 0, AMSeekingSeekingFlags.NoPositioning));
// Update VideoState
currentState = VideoState.Stopped;
}
///
/// Rewinds the video to the start and plays it again
///
public void Rewind()
{
Stop();
Play();
}
#endregion
#region ISampleGrabberCB Members and Helpers
///
/// Required public callback from DirectShow SampleGrabber. Do not call this method.
///
public int BufferCB(double SampleTime, IntPtr pBuffer, int BufferLen)
{
// Copy raw data into bgrData byte array
Marshal.Copy(pBuffer, bgrData, 0, BufferLen);
// Flag the new frame as available
frameAvailable = true;
// Return S_OK
return 0;
}
///
/// Required public callback from DirectShow SampleGrabber. Do not call this method.
///
public int SampleCB(double SampleTime, IMediaSample pSample)
{
// Return S_OK
return 0;
}
///
/// Worker to copy the BGR data from the video stream into the RGBA byte array for the Output Frame.
///
private void UpdateBuffer()
{
int waitTime = avgTimePerFrame != 0 ? (int)((float)avgTimePerFrame / 10000) : 20;
int samplePosRGBA = 0;
int samplePosRGB24 = 0;
while (true)
{
for (int y = 0, y2 = videoHeight - 1; y < videoHeight; y++, y2--)
{
for (int x = 0; x < videoWidth; x++)
{
samplePosRGBA = (((y2 * videoWidth) + x) * 4);
samplePosRGB24 = ((y * videoWidth) + x) * 3;
videoFrameBytes[samplePosRGBA + 0] = bgrData[samplePosRGB24 + 0];
videoFrameBytes[samplePosRGBA + 1] = bgrData[samplePosRGB24 + 1];
videoFrameBytes[samplePosRGBA + 2] = bgrData[samplePosRGB24 + 2];
videoFrameBytes[samplePosRGBA + 3] = alphaTransparency;
}
}
frameAvailable = false;
while (!frameAvailable)
{ Thread.Sleep(waitTime); }
}
}
///
/// Waits for the video to finish, then calls the OnVideoComplete event
///
private void WaitForCompletion()
{
int waitTime = avgTimePerFrame != 0 ? (int)((float)avgTimePerFrame / 10000) : 20;
try
{
while (videoDuration > currentPosition)
{
Thread.Sleep(waitTime);
}
if (OnVideoComplete != null)
OnVideoComplete.Invoke(this, EventArgs.Empty);
}
catch { }
}
#endregion
#region IDisposable Members
///
/// Cleans up the Video Player. Must be called when finished with the player.
///
public void Dispose()
{
isDisposed = true;
Stop();
CloseInterfaces();
outputFrame.Dispose();
outputFrame = null;
}
#endregion
}
}