截图(Grabbing a Poster Frame)

来源:互联网 发布:淘宝可以分期付款吗 编辑:程序博客网 时间:2024/05/18 04:01

本文描述了如何使用 "媒体侦测器"(Media Detector - MediaDet) 显示数字媒体文件中的一帧,"媒体侦测器" 对象是由 DirectShow Editing Services 提供。
"媒体侦测器"是一个助手对象,他可以从媒体源文件 (media source file) 中获得格式信息,他也可以从视频流文件中截取位图 (Bitmap)。
假设视频流文件是可定位 (seekable) 的,你就可以从该文件的任何位置获取图像。返回的图像格式一定 24 位 RGB。
"媒体侦测器" 并不是一个 "过滤器" (Filter),你的应用程序也不需要使用 "过滤器图表管理器" (Filter Graph Manager),也不需要创建"
过滤器图表" (filter graph)。"媒体侦测器"会创建"过滤器图表",其中包含"样本截取过滤器" (Sample Grabber Filter),为了获取位图,"
过滤器图表"定位并暂停"过滤器图表",然后从"样本截取过滤器"中获取位图。应用程序通过 IMediaDet 接口,和"媒体侦测器"进行通信。

"媒体侦测器"是把"过滤器图表"封装到助手对象中的不错的例子,从而把图表相关的细节与应用程序隔离(shield)开来。
"媒体侦测器"有两种模式,当你第一次创建该对象时,他出于"信息获取"(information gathering)模式。你可以在该模式下,指定媒体文件,
并从该文件中包含的每一个流(stream)中获得信息,例如格式类型,帧比率(frame rate),文件播放长度。如果媒体文件中包含视频流,你可以
把"媒体侦测器"切换到另一种模式 - 位图截取模式("bitmap grab" mode ),并从源文件中获得位图。一旦你切换到该模式下 ,将无法再切回
原来的模式,如需处理文件中的其他流或其他文件,你就必须重新创建一个新的"媒体侦测器"实例。
我们分4步来完成解图:

1.创建窗口
2.添加一个菜单项
3.实现帧截取(Frame-Grabbing)功能
4.在客户区(Client Area)绘制位图

1.创建窗口
为应用程序创建一个框架窗口,包含 WinMain 函数和一个窗口处理(window procedure)函数。在消息循环(message loop )之前通过调用 CoInitialize 来初始化 COM 库,在退出消息循环时,调用 CoUninitialize 。以下是窗口处理函数:

LRESULT CALLBACK MainWndProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam)
{
    static BITMAPINFOHEADER *pbmi = NULL;
    static BYTE *pBuffer = NULL;
    switch (msg)
    {
    case WM_CLOSE:
        DestroyWindow(hwnd);
        break;
    case WM_DESTROY:
        if (pbmi) delete [] pbmi;
        PostQuitMessage(0);
        break;
    default:
        return DefWindowProc(hwnd, msg, wparam, lparam);
    }
    return 0;
}
当你要从"媒体侦测器"中获取一帧图像时,他会返回一个缓存(buffer),该缓存包含 BITMAPINFOHEADER 结构,以及图像数据。所以我们在窗口处理函数定义了两个静态变量:pbmi 是 BITMAPINFOHEADER 指针,pBuffer 是位图数据的指针。应用程序必须通过 new 来为 pbmi 分配缓存,所以必须在窗口销毁(destroy)之前 delete 掉。而 pBuffer 指针其实只是 pbmi 指针的一个偏移量,所以无需 delete。

2.添加一个菜单项添加一个命令让用户可以从媒体文件中截取一帧。创建菜单项,其资源 ID 是 IDM_BITMAP,并添加以下 case 语句到窗口处理函数中:

case WM_COMMAND:
    switch (LOWORD(wparam))
    {
    case IDM_BITMAP:
        {
            HRESULT hr = DoShowBitmap(hwnd, &pbmi);
            if (SUCCEEDED(hr))
            {
                pBuffer = reinterpret_cast<BYTE*>(pbmi) +

                    sizeof(BITMAPINFOHEADER);
                InvalidateRect(hwnd, NULL, TRUE);
            }
            else
            {
                MessageBox(hwnd, TEXT("Cannot display the image."),
                    TEXT("Error"), MB_OK | MB_ICONERROR);
            }
        }
        break;  // IDM_BITMAP
    }
    break;  // WM_COMMAND

DoShowBitmap 函数将为 pbmi 分配缓存。如果函数成功执行,位图数据地址(pBuffer),可以通过 pbmi 指针计算获得。在 DoShowBitmap 函数中显示一个打开文件对话框,让用户选择一个媒体文件,然后通过调用 GetBitmap 函数来截取位图:

HRESULT DoShowBitmap(HWND hwnd, BITMAPINFOHEADER** ppbmih)
{

    OPENFILENAME ofn;       // 通用对话框结构
    // 初始化 OPENFILENAME (略).
    // 显示一个打开文件对话框 
    if (GetOpenFileName(&ofn) != TRUE) // 打开失败
    {
        return E_FAIL;
    }
    return GetBitmap(ofn.lpstrFile, ppbmih);
}

3.实现帧截取(Frame-Grabbing)功能
以下就来实现 GetBitmap 函数,该函数进行如下操作:
1.创建"媒体侦测器"对象
2.指定媒体文件
3.检查媒体文件中包含的每一流,如果是视频流的话,就取得视频原始的高度和宽度。
4.获取图像帧,指定定位时间和目标图像文件的高度,宽度。

调用 CoCreateInstance 来创建"媒体侦测器"
CComPtr<IMediaDet> pDet;
hr = pDet.CoCreateInstance(__uuidof(MediaDet));

使用 IMediaDet::put_Filename 方法指定媒体文件,该方法使用 BSTR 参数。

hr = pDet->put_Filename(bstrFilename);

获取文件中包含流的数量,然后通过循环语句检查每个流的媒体类型(media type),IMediaDet::get_OutputStreams 方法可以获得流数量,IMediaDet::put_CurrentStream 方法用来指派检查某个流,当找到视频流时,就退出循环语句。

long lStreams;
bool bFound = false;
hr = pDet->get_OutputStreams(&lStreams);
for (long i = 0; i < lStreams; i++)
{
    GUID major_type;
    hr = pDet->put_CurrentStream(i);
    hr = pDet->get_StreamType(&major_type);
    if (major_type == MEDIATYPE_Video)  // 找到视频流
    {
        bFound = true;
        break;
    }
}

if (!bFound) return VFW_E_INVALIDMEDIATYPE;

如果未能找到视频流,函数就退出。
在先前的代码中,IMediaDet::get_StreamType 方法返回的只是媒体主类型 (major type) 的 GUID,如果你只想获得媒体主类型的话,调用该方法即可,如果希望获得视频的高宽等信息时,你必须检查媒体文件的格式,所以你需要全部的媒体类型描述,可以通过IMediaDet::get_StreamMediaType 方法来获得,该方法返回 AM_MEDIA_TYPE 结构。"媒体侦测器"把所以视频流转换为非压缩格式,其中包含VIDEOINFOHEADER 格式块。
long width = 0, height = 0;
AM_MEDIA_TYPE mt;
hr = pDet->get_StreamMediaType(&mt);
if (mt.formattype == FORMAT_VideoInfo)
{
    VIDEOINFOHEADER *pVih = (VIDEOINFOHEADER*)(mt.pbFormat);
    width = pVih->bmiHeader.biWidth;
    height = pVih->bmiHeader.biHeight;
    // 我们需要高度的绝对值,不考虑图像的方向
    if (height < 0) height *= -1;
}
else {
    return VFW_E_INVALIDMEDIATYPE; // This should not happen, in theory.
}
FreeMediaType(mt);

get_StreamMediaType 方法分配的格式块,函数调用者必须在使用完毕后释放他,可以通过调用函数 FreeMediaType 来完成。

好了,你现在就可以截取图像了,先调用 IMediaDet::GetBitmapBits 方法,参数是 NULL,来取得缓存:
long lSize;
hr = pDet->GetBitmapBits(0, &lSize, NULL, width, height);

该调用返回所需的缓存大小(参数 lSize),函数的第一个参数是用来指定流中的定位时间,该例子使用零,对于高度和宽度还是使用原始视频的大小,如果你改变该大小,"媒体侦测器"会伸缩位图来适合你指定的大小。

如果该方法执行成功,分配好缓存,然后再调用一次 GetBitmapBits 方法。
if (SUCCEEDED(hr))
{
    char *pBuffer = new char[lSize];
    if (!pBuffer) return E_OUTOFMEMORY;
    hr = pDet->GetBitmapBits(0, NULL, pBuffer, width, height);
    if (FAILED(hr))
        delete [] pBuffer;
}

以下是完整的函数:
HRESULT GetBitmap(LPTSTR pszFileName, BITMAPINFOHEADER** ppbmih)
{
    HRESULT hr;
    CComPtr<IMediaDet> pDet;
    hr = pDet.CoCreateInstance(__uuidof(MediaDet));
    if (FAILED(hr)) return hr;
    // Convert the file name to a BSTR.
    CComBSTR bstrFilename(pszFileName);
    hr = pDet->put_Filename(bstrFilename);
    if (FAILED(hr)) return hr;
    long lStreams;
    bool bFound = false;
    hr = pDet->get_OutputStreams(&lStreams);

    if (FAILED(hr)) return hr;
    for (long i = 0; i < lStreams; i++)
    {
        GUID major_type;
        hr = pDet->put_CurrentStream(i);
        if (SUCCEEDED(hr))
        {
            hr = pDet->get_StreamType(&major_type);
        }
        if (FAILED(hr)) break;
        if (major_type == MEDIATYPE_Video)
        {
            bFound = true;
            break;
        }
    }
    if (!bFound) return VFW_E_INVALIDMEDIATYPE;
    long width = 0, height = 0;
    AM_MEDIA_TYPE mt;
    hr = pDet->get_StreamMediaType(&mt);
    if (SUCCEEDED(hr))
    {
        if ((mt.formattype == FORMAT_VideoInfo) &&
            (mt.cbFormat >= sizeof(VIDEOINFOHEADER)))
        {
            VIDEOINFOHEADER *pVih = (VIDEOINFOHEADER*)(mt.pbFormat);
            width = pVih->bmiHeader.biWidth;
            height = pVih->bmiHeader.biHeight;
            // We want the absolute height, don't care about orientation.
            if (height < 0) height *= -1;
        }
        else
        {
            hr = VFW_E_INVALIDMEDIATYPE; // Should not happen, in theory.
        }
        FreeMediaType(mt);
    }
    if (FAILED(hr)) return hr;
    // Find the required buffer size.
    long size;
    hr = pDet->GetBitmapBits(0, &size, NULL, width, height);
    if (SUCCEEDED(hr))
    {
        char *pBuffer = new char[size];
        if (!pBuffer) return E_OUTOFMEMORY;
        try {
            hr = pDet->GetBitmapBits(0, NULL, pBuffer, width, height);
            if (SUCCEEDED(hr))
            {
                // Delete the old image, if any.
                if (*ppbmih) delete[] (*ppbmih);
                (*ppbmih) = (BITMAPINFOHEADER*)pBuffer;
            }
            else
            {
                delete [] pBuffer;
            }
        }
        catch (...) {
            delete [] pBuffer;
            return E_OUTOFMEMORY;
        }
    }
    return hr;
}

4.在客户去绘制位图

使用 SetDIBitsToDevice 函数即可,该例子仅仅是简单地把位图画到客户区的左上角,而不考虑窗口的大小:
case WM_PAINT:

    {
        PAINTSTRUCT ps;
        HDC hdc = BeginPaint(hwnd, &ps);
        if (pbmi)
        {
            int result = SetDIBitsToDevice(hdc, 0, 0,
                pbmi->biWidth,
                pbmi->biHeight,
                0, 0, 0,
                pbmi->biHeight,
                pBuffer,
                reinterpret_cast<BITMAPINFO*>(pbmi),
                DIB_RGB_COLORS);
        }
        EndPaint(hwnd, &ps);
    }
    break;