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