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 } }