Introduction
This class contains methods to use the IMediaDet interface that can be found in Microsoft DirectShow. The Media Detector object, among other things, can be used to extract still pictures from several file formats including *.avi, *.wmv and some *.mpeg files.
This class exposes the GetFrameFromVideo
, GetVideoSize
and SaveFrameFromVideo
methods that can be used from any .NET application. The class also takes care of translating HRESULT
s returned from the functions to meaningful .NET exceptions.
Using the Code
Just add a reference to JockerSoft.Media.dll in your project (or include the source code). Remember also to distribute Interop.DexterLib.dll.
All the methods are static
, so to use them just do something like this:
try
{
this.pictureBox1.Image = FrameGrabber.GetFrameFromVideo(strVideoFile, 0.2d);
}
catch (InvalidVideoFileException ex)
{
MessageBox.Show(ex.Message, "Extraction failed");
}
catch (StackOverflowException)
{
MessageBox.Show("The target image size is too big", "Extraction failed");
}
or
try
{
FrameGrabber.SaveFrameFromVideo(strVideoFile, 0.2d, strBitmapFile);
}
catch (InvalidVideoFileException ex)
{
MessageBox.Show(ex.Message, "Extraction failed");
}
Here, we used the simplest of the three overloads of GetFrameFromVideo
and SaveFrameFromVideo
methods, presented in this article.
Points of Interest
The IMediaDet
and linked interfaces/classes are exposed in qedit.dll, that can be found in System32 directory. Fortunately this DLL can be imported automatically using tlbimp
, so no code is needed to wrap it.
To extract images, there are two methods: extract them in memory (using GetBitmapBits - here GetFrameFromVideo
) or extract them and save to a bitmap file (using WriteBitmapBits - here SaveFrameFromVideo
).
WriteBitmapBits
is really simple to be used: we just need to find the video stream on the file, open it and specify an output file name for the bitmap image.
public static void SaveFrameFromVideo(string videoFile,
double percentagePosition, string outputBitmapFile,
out double streamLength, Size target)
{
if (percentagePosition > 1 || percentagePosition < 0)
throw new ArgumentOutOfRangeException("percentagePosition",
percentagePosition, "Valid range is 0.0 .. 1.0");
try
{
MediaDetClass mediaDet;
_AMMediaType mediaType;
if (openVideoStream(videoFile, out mediaDet, out mediaType))
{
streamLength = mediaDet.StreamLength;
if (target == Size.Empty)
target = getVideoSize(mediaType);
else
target = scaleToFit(target, getVideoSize(mediaType));
mediaDet.WriteBitmapBits(streamLength * percentagePosition,
target.Width, target.Height, outputBitmapFile);
return;
}
}
catch (COMException ex)
{
throw new InvalidVideoFileException(getErrorMsg((uint)ex.ErrorCode), ex);
}
throw new InvalidVideoFileException("No video stream was found");
}
You'll notice that two private
methods are used here. They are openVideoStream
and getVideoSize
. Their implementation is straight forward:
private static bool openVideoStream(string videoFile,
out MediaDetClass mediaDetClass, out _AMMediaType aMMediaType)
{
MediaDetClass mediaDet = new MediaDetClass();
mediaDet.Filename = videoFile;
int streamsNumber = mediaDet.OutputStreams;
for (int i = 0; i < streamsNumber; i++)
{
mediaDet.CurrentStream = i;
_AMMediaType mediaType = mediaDet.StreamMediaType;
if (mediaType.majortype == JockerSoft.Media.MayorTypes.MEDIATYPE_Video)
{
mediaDetClass = mediaDet;
aMMediaType = mediaType;
return true;
}
}
mediaDetClass = null;
aMMediaType = new _AMMediaType();
return false;
}
(where MEDIATYPE_Video
is the GUID
used for video files).
private static Size getVideoSize(_AMMediaType mediaType)
{
WinStructs.VIDEOINFOHEADER videoInfo =
(WinStructs.VIDEOINFOHEADER)Marshal.PtrToStructure(mediaType.pbFormat,
typeof(WinStructs.VIDEOINFOHEADER));
return new Size(videoInfo.bmiHeader.biWidth, videoInfo.bmiHeader.biHeight);
}
Using GetBitmapBits
to avoid saving the image on disk is a bit trickier, since we need to deal with direct access to memory.
The first part is identical to SaveFrameFromVideo
, then we have to call GetBitmapBits
with the pBuffer
parameter set to null
to get the size for the buffer of bytes that will contain the 24bpp image (GetBitmapBits
always returns 24bpp images).
Once we have the size of the buffer, we allocate memory on the heap to receive the image (in the first version of this code, memory was allocated on the stack which is fine if the target image is small sized, but if it is big we may get a nice StackOverflowException
because stack memory is rather limited).
After this, we call GetBitmapBits
again, but this time the buffer will be filled with image bytes. Now we create a bitmap from these bytes (remembering that they start with BITMAPINFOHEADER
structure, the size of which is 40 bytes).
public static Bitmap GetFrameFromVideo(string videoFile,
double percentagePosition, out double streamLength, Size target)
{
if (percentagePosition > 1 || percentagePosition < 0)
throw new ArgumentOutOfRangeException("percentagePosition",
percentagePosition, "Valid range is 0.0 .. 1.0");
try
{
MediaDetClass mediaDet;
_AMMediaType mediaType;
if (openVideoStream(videoFile, out mediaDet, out mediaType))
{
streamLength = mediaDet.StreamLength;
if (target == Size.Empty)
target = getVideoSize(mediaType);
else
target = scaleToFit(target, getVideoSize(mediaType));
unsafe
{
Size s= GetVideoSize(videoFile);
int bmpinfoheaderSize = 40;
int bufferSize = (((s.Width * s.Height) * 24) / 8 ) + bmpinfoheaderSize;
IntPtr frameBuffer =
System.Runtime.InteropServices.Marshal.AllocHGlobal(bufferSize);
byte* frameBuffer2 = (byte*)frameBuffer.ToPointer();
mediaDet.GetBitmapBits(streamLength * percentagePosition,
ref bufferSize, ref *frameBuffer2, target.Width, target.Height);
Bitmap bmp = new Bitmap(target.Width, target.Height, target.Width * 3,
System.Drawing.Imaging.PixelFormat.Format24bppRgb,
new IntPtr(frameBuffer2 + bmpinfoheaderSize));
bmp.RotateFlip(RotateFlipType.Rotate180FlipX);
System.Runtime.InteropServices.Marshal.FreeHGlobal(frameBuffer);
return bmp;
}
}
}
catch (COMException ex)
{
throw new InvalidVideoFileException(getErrorMsg((uint)ex.ErrorCode), ex);
}
throw new InvalidVideoFileException("No video stream was found");
}
Known Limitations
- The biggest one is the
StackOverflowException
using stackalloc
. There must be a way to pass to GetBitmapBits
a buffer created on the heap, but I'm not yet very good with this unmanaged stuff.
- This problem has been resolved. The solution is provided by _coder_ in the comments below.
- On my machine, I got random errors when passing certain target sizes to
GetBitmapBits
: when the target size is 125x125 for example, the Bitmap
constructor fails.
- Only sizes multiples of 4 are allowed. Thanks to ujr (see comments).
- "The
IMediaDet interface
does not support VIDEOINFOHEADER2
formats": This means it cannot open some *.mpeg video files.
History
- 27th February, 2006: Initial release
- 17th March, 2006: Replaced memory allocation on the stack
- 27th September, 2007: Some fixes