using DirectShowLib; using DirectShowLib.DES; using System; using System.Diagnostics; using System.Drawing; using System.Drawing.Imaging; using System.Runtime.InteropServices; using System.Text; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Threading; using PixelFormat = System.Drawing.Imaging.PixelFormat; using Size = System.Windows.Size; namespace WPFMediaKit.DirectShow.MediaPlayers; /// /// The MediaDetector class allows to query meta data from audio/video media files. /// This includes CODEC information and the ability to grab video snapshots. /// public class MediaDetector : DispatcherObject, IDisposable { #region Locals private IMediaDet m_mediaDet; private int m_audioBitsPerSample; private int m_audioChannels; private int m_audioSamplesPerSecond; private double m_audioStreamLength; private Guid m_audioSubType; private Bitmap m_bitmap; private string m_filename; private string m_fourCC; private int m_streamCount; private int m_videoBitsPerPixel; private Size m_videoResolution; private double m_videoStreamLength; private Guid m_videoSubType; private double m_videoFrameRate; #endregion Locals /// /// The video framerate /// public double VideoFrameRate { get { return m_videoFrameRate; } } /// /// The video CODEC tag /// public string FourCC { get { return m_fourCC; } } /// /// The bits per pixel of the video /// public int VideoBitsPerPixel { get { return m_videoBitsPerPixel; } } /// /// The length of the video stream /// public TimeSpan VideoStreamLength { get { return TimeSpan.FromSeconds(m_videoStreamLength); } } /// /// The length of the audio stream /// public TimeSpan AudioStreamLength { get { return TimeSpan.FromSeconds(m_audioStreamLength); } } public Guid AudioSubType { get { return m_audioSubType; } } public Guid VideoSubType { get { return m_videoSubType; } } /// /// The number of bits per sample in the audio stream /// public int AudioBitsPerSample { get { return m_audioBitsPerSample; } } /// /// The HZ of the audio samples /// public int AudioSamplesPerSecond { get { return m_audioSamplesPerSecond; } } /// /// The number of audio channels in audio stream /// public int AudioChannels { get { return m_audioChannels; } } /// /// The filename of the loaded media /// public string Filename { get { return m_filename; } } /// /// The native pixel size of the video, if a /// video stream exists /// public Size VideoResolution { get { return m_videoResolution; } } /// /// The total amount of streams that exist in the media /// public int StreamCount { get { return m_streamCount; } } /// /// Is true if the loaded media has an audio stream /// public bool HasAudio { get; private set; } /// /// Is true if the loaded media has a video stream /// public bool HasVideo { get; private set; } #region IDisposable Members public void Dispose() { FreeResources(); GC.SuppressFinalize(this); } #endregion IDisposable Members [DllImport("kernel32.dll", EntryPoint = "RtlMoveMemory")] private static extern void CopyMemory(IntPtr Destination, IntPtr Source, int Length); /// /// Frees any memory and resets to a default state /// private void FreeResources() { m_audioBitsPerSample = 0; m_audioChannels = 0; m_audioSamplesPerSecond = 0; m_audioStreamLength = 0; m_audioSubType = Guid.Empty; m_filename = ""; m_fourCC = ""; m_streamCount = 0; m_videoBitsPerPixel = 0; m_videoResolution = Size.Empty; m_videoStreamLength = 0; m_videoSubType = Guid.Empty; m_videoFrameRate = 0; HasAudio = false; HasVideo = false; if (m_bitmap != null) m_bitmap.Dispose(); m_bitmap = null; if (m_mediaDet != null) { Marshal.ReleaseComObject(m_mediaDet); m_mediaDet = null; } } /// /// Converts a FourCC code to a string /// private static string ConvertFourCC(int fourcc) { return Encoding.ASCII.GetString(BitConverter.GetBytes(fourcc)); } /// /// Loads a media file. /// /// The full path of the media file to load public void LoadMedia(string filename) { VerifyAccess(); FreeResources(); m_filename = filename; try { if (string.IsNullOrEmpty(m_filename)) return; LoadMedia(); } catch (Exception ex) { FreeResources(); throw new WPFMediaKitException("Failed to load " + filename, ex); } } private void LoadMedia() { /*Create the COM object and query the IMediaDet interface */ m_mediaDet = new MediaDet() as IMediaDet; if (m_mediaDet == null) throw new NullReferenceException("Could not create an instance of MediaDet COM"); int hr = m_mediaDet.put_Filename(m_filename); DsError.ThrowExceptionForHR(hr); /* We find out how many streams exist in the * media file. These can be audio, video, etc */ hr = m_mediaDet.get_OutputStreams(out m_streamCount); DsError.ThrowExceptionForHR(hr); /* Loop over each of the streams and extract info from them */ for (int i = 0; i < m_streamCount; i++) { /* Set the interface to look at a specific stream */ hr = m_mediaDet.put_CurrentStream(i); DsError.ThrowExceptionForHR(hr); Guid majorType; /* Get the major type of the media */ hr = m_mediaDet.get_StreamType(out majorType); DsError.ThrowExceptionForHR(hr); var mediaType = new AMMediaType(); double framerate; /* Gets the AMMediaType so we can read some * metadata on the stream */ hr = m_mediaDet.get_StreamMediaType(mediaType); DsError.ThrowExceptionForHR(hr); if (majorType == MediaType.Video) { ReadVideoFormat(mediaType); hr = m_mediaDet.get_FrameRate(out framerate); m_videoFrameRate = framerate; } else if (majorType == MediaType.Audio) { ReadAudioFormat(mediaType); } /* We need to free this with the helper * because it has an unmanaged pointer * and we don't want any leaks */ DsUtils.FreeAMMediaType(mediaType); } } /// /// Reads the audio stream information from the media file /// private void ReadAudioFormat(AMMediaType mediaType) { m_audioSubType = mediaType.subType; int hr = m_mediaDet.get_StreamLength(out m_audioStreamLength); DsError.ThrowExceptionForHR(hr); if (mediaType.formatType == FormatType.WaveEx) { HasAudio = true; var waveFormatEx = (WaveFormatEx)Marshal.PtrToStructure(mediaType.formatPtr, typeof(WaveFormatEx)); m_audioChannels = waveFormatEx.nChannels; m_audioSamplesPerSecond = (waveFormatEx.nSamplesPerSec); m_audioBitsPerSample = waveFormatEx.wBitsPerSample; } } /// /// Reads the video stream information for the media file /// private void ReadVideoFormat(AMMediaType mediaType) { m_videoSubType = mediaType.subType; int hr = m_mediaDet.get_StreamLength(out m_videoStreamLength); DsError.ThrowExceptionForHR(hr); if (mediaType.formatType == FormatType.VideoInfo) /* Most common video major type */ { HasVideo = true; /* 'Cast' the unmanaged pointer to our managed struct so we can read the meta data */ var header = (VideoInfoHeader)Marshal.PtrToStructure(mediaType.formatPtr, typeof(VideoInfoHeader)); m_fourCC = ConvertFourCC(header.BmiHeader.Compression); m_videoBitsPerPixel = header.BmiHeader.BitCount; m_videoResolution = new Size(header.BmiHeader.Width, header.BmiHeader.Height); } else if (mediaType.formatType == FormatType.VideoInfo2) /* Usually for interlaced video */ { HasVideo = true; /* 'Cast' the unmanaged pointer to our managed struct so we can read the meta data */ var header = (VideoInfoHeader2)Marshal.PtrToStructure(mediaType.formatPtr, typeof(VideoInfoHeader2)); m_fourCC = ConvertFourCC(header.BmiHeader.Compression); m_videoResolution = new Size(header.BmiHeader.Width, header.BmiHeader.Height); m_videoBitsPerPixel = header.BmiHeader.BitCount; /* TODO: Pull out VideoInfoHeader2 specifics */ } } /// /// Gets an image snapshot from the media file that was opened /// /// The media time position for the requested thumbnail /// Returns a BitmapSource of the video position. public unsafe BitmapSource GetImage(TimeSpan position) { VerifyAccess(); const int BITS_PER_PIXEL = 3; if (string.IsNullOrEmpty(m_filename)) throw new WPFMediaKitException("A media file must be successfully loaded first."); if (!HasVideo) throw new WPFMediaKitException("The media does not have a video stream"); double secondsPos = position.TotalSeconds; /* Our pointer to the bitmap's pixel buffer */ IntPtr pBuffer = IntPtr.Zero; /* The WPF bitmap source we return */ BitmapSource bmpSource = null; try { /* The size of our buffer */ int bufferSize; /* Queries the size of the bitmap buffer first */ int hr = m_mediaDet.GetBitmapBits(secondsPos, out bufferSize, IntPtr.Zero, (int)VideoResolution.Width, (int)VideoResolution.Height); if (hr == 0) { /* Allocate some unmanaged memory, big enough for our bitmap bytes */ pBuffer = Marshal.AllocCoTaskMem(bufferSize); /* Get the pixel buffer for the thumbnail */ hr = m_mediaDet.GetBitmapBits(secondsPos, out bufferSize, pBuffer, (int)VideoResolution.Width, (int)VideoResolution.Height); DsError.ThrowExceptionForHR(hr); /* The bitmap header exists is the buffer. We 'cast' it out to read it */ var bitmapHeader = (BitmapInfoHeader)Marshal.PtrToStructure(pBuffer, typeof(BitmapInfoHeader)); /* We use a pointer so we can do some pointer * arithmetic. This method should be compatible * with 32/64bit processes */ var pBitmapData = (byte*)pBuffer.ToPointer(); pBitmapData += bitmapHeader.Size; /* This will be the pointer to the bitmap pixels */ var bitmapData = new IntPtr(pBitmapData); /* We create a GDI bitmap, so we can flip it before we * load it into a WPF BitmapSource */ if (m_bitmap == null) m_bitmap = new Bitmap(bitmapHeader.Width, bitmapHeader.Height, PixelFormat.Format24bppRgb); /* The GDI bitmap's pixels are locked so we can grab a pointer to the pixel buffer */ BitmapData bmpData = m_bitmap.LockBits(new Rectangle(0, 0, bitmapHeader.Width, bitmapHeader.Height), ImageLockMode.WriteOnly, PixelFormat.Format24bppRgb); /* The we copy our pixel buffer to the GDI bitmap's pixel buffer */ CopyMemory(bmpData.Scan0, bitmapData, (int)VideoResolution.Width * (int)VideoResolution.Height * BITS_PER_PIXEL); m_bitmap.UnlockBits(bmpData); /* The bitmap is bottom up, so it needs to be flipped */ m_bitmap.RotateFlip(RotateFlipType.Rotate180FlipX); /* Lock the GDI pixel buffer so we can load them into a WPF BitmapSource */ bmpData = m_bitmap.LockBits(new Rectangle(0, 0, bitmapHeader.Width, bitmapHeader.Height), ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb); bmpSource = BitmapSource.Create((int)VideoResolution.Width, (int)VideoResolution.Height, 96, 96, PixelFormats.Bgr24, null, bmpData.Scan0, bmpData.Stride * (int)VideoResolution.Height, bmpData.Stride); m_bitmap.UnlockBits(bmpData); } } catch (Exception ex) { Debug.WriteLine("GetImage error: " + ex.Message); } finally { if (pBuffer != IntPtr.Zero) Marshal.FreeCoTaskMem(pBuffer); } return bmpSource; } }