65.9K
CodeProject 正在变化。 阅读更多。
Home

基本的视频捕获和 VMR9

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.83/5 (11投票s)

2009年3月17日

CPOL

9分钟阅读

viewsIcon

100510

downloadIcon

10816

使用 DirectShow 从网络摄像头捕获视频,并使用 VMR9 进行无窗口渲染。

引言

简单来说,这个小程序将帮助您开始从网络摄像头进行视频捕获,并允许使用 VMR9 进行无窗口渲染。此刻,我想说的是,程序会读取系统中所有可用的过滤器;用户“必须”选择合适的过滤器才能正确运行程序。除了 MSDN 上你能找到的 DirectShow 帮助之外,我希望这小段代码能让你对 DirectShow 的工作方式有所了解,因为 MSDN 的帮助总是像戛然而止一样。我添加了屏幕截图,以便使用程序更加方便。

背景

除了 MFC 之外,还需要 STL、COM 编程基础和 DirectShow 基础。如果您不了解过滤器是如何被读取和使用的,请参考我的其他文章,其中详细解释了过滤器的使用方法,并且还提供了对 BSTR_Compare(..) 方法的解释。相关主题的链接是一个三部分教程,如下所示:

使用代码

要使用该代码,请确保您已更改包含目录的程序路径。特别是,必须正确配置 Windows SDK 的路径。

该程序是使用类开发的。以下类图应该能让你对内容有所了解。并非所有方法都得到了解释,因为其中一些方法非常简单,例如,对于具有 DirectShow 基本知识的开发人员来说。

classdiagram.jpg

类的解释

CMainGraph

这是主类。该类保存构建器图引用、捕获图引用以及控制图的方法。相机和 VMR9 等过滤器会添加到此类的对象中。

class CMainGraph
{
public:
    CMainGraph(void);
    ~CMainGraph(void);
protected:
    // Main graph pointer
    IGraphBuilder* pGraph;
    // Main capture graph
    ICaptureGraphBuilder2* pCaptureGraph2;
    // System device enumerator
    ICreateDevEnum* pFilterEnum;
    // Device moniker
    IMoniker *pFilterMonik;
    // pointer to media playback control interface
    IMediaControl* pControl;
public:
    // Main COM initialization function
    void Init_COM(void);
    // Displays a message box when a COM error occurs
    void HR_Failed(HRESULT hr);
    // Run graph
    void Run_Graph();
    // Stop the main graph
    void Stop_Graph(void);
    // Get Device/Filter Enumerator
    ICreateDevEnum* Get_Enumerator(void);
    // Return the main graph pointer
    IGraphBuilder* Get_MainGraph_Ptr(void);
    // Get the main capture graph pointer
    ICaptureGraphBuilder2* Get_CaptureGraph_Ptr(void);
};
  • void Init_COM(void);:此类的方法会初始化 COM。它设置对其他接口的引用,例如 ICaptureGraphBuilder2IMediaControl 等,并查询所需的接口以便停止/运行图。
  • ICreateDevEnum* Get_Enumerator(void);:此方法返回对 ICreateDevEnum 的引用。返回的指针随后会传递给另外两个过滤器,相机和 VMR9,它们使用枚举来实例化过滤器,或者说相机设备和编解码器。
  • IGraphBuilder* Get_MainGraph_Ptr(void);:此方法返回 pGraph。继承此类的方法需要通过此方法引用主图指针。
  • ICaptureGraphBuilder2* Get_CaptureGraph_Ptr(void);:此方法返回 ICaptureGraphBuilder2 引用。此接口对于利用相机捕获功能也非常重要。

CFilter

CFilter 类继承自 CMainGraph。该类包含表示过滤器名称、指向 IBaseFilter 的指针和 IMoniker 的字段。此类是相机类和 VM9 类的父类。

class CFilter: 
    public CMainGraph
{
public:
    CFilter(void);
    CFilter(IMoniker*);
    ~CFilter(void);
protected:
    // FriendlyName as seen in graphedit.exe
    BSTR bstrFilterName;
    // Pointer to filter interface
    IBaseFilter* pFilter;
    // pointer to filter moniker 
    IMoniker* pFilterMoniker;
public:
    // Displays a message box when a COM error occurs
    void HR_Failed(HRESULT hr);
    //Compares two fitler names - true if filter found on system
    bool BSTR_Compare(BSTR bstrFilterName, BSTR bstrDeviceName);
    // Find pin by name
    IPin* Find_Pin(BSTR bstrPinName);
    // Find Pin
    IPin* Find_Pin(PIN_DIRECTION PinDir,IPin *pFilterPin);
    // Find a required pin
    IPin* Find_Pin(PIN_DIRECTION PIN_DIR, GUID PIN_CAT, GUID MEDIA_TYPE);
    // Filter initiating function
    IBaseFilter *Filter_Init(IMoniker*);
    //Function that connects two filter pins
    void Filter_Connect(IPin* pPinOut , IPin* pPinIn);
    // Function to add filter to main graph
    void Filter_Addto_Graph(IBaseFilter* pFilter,BSTR bstrName);
    // Set the main graph pointer
    void Set_MainGraph_Ptr(IGraphBuilder* pGraph);
    // Set main capture graph pointer
    void Set_CaptureGraph_Ptr(ICaptureGraphBuilder2* pCG);
    
};
  • IPin* Find_Pin(BSTR bstrPinName);:这是一个重载方法。在尝试查找引脚时,我们可以使用引脚的“FriendlyName”。找到后,引脚会作为返回引脚,然后用于根据调用将其与相机或视频渲染器连接起来。
  • IPin* Find_Pin(PIN_DIRECTION PIN_DIR,GUID PIN_CAT,GUIDE MEDIA_TYPE);:除了其他重载方法外,最重要的方法之一。此方法用于根据引脚的方向查找引脚,即它是输入引脚还是输出引脚?如果将其与相机过滤器一起使用并尝试查找“Capture”引脚,则 PIN_DIR 将等于 PINDIR_OUPUT。我们可以指定 PIN_CAT 等于 PIN_CATEGORY_CAPTURE,最后一个参数可用于指定仅音频、仅视频还是混合;例如,由于我们仅使用视频,因此 MEDIA_TYPE 将等于 MEDIATYPE_Video
  • IPin* Find_Pin(PIN_DIRECTION PinDir,IPin *pFilterPin);:最后一个重载方法,可用于根据方向查找引脚;请记住,此处传递的引脚在成功实例化后返回。

所有三个方法都几乎使用相同的代码来查找引脚,即通过遍历过滤器。请记住,这些方法只能在成功初始化过滤器后调用。方法内的代码如下所示:

HRESULT hr;
IEnumPins *pEPin = NULL;// Pin enumeration
IPin *pPin = NULL;// Pins
if (SUCCEEDED(this->pFilter->EnumPins(&pEPin)))
{
    while (hr = pEPin->Next(1, &pPin, 0), hr == S_OK)// loop through filter
    {
        //Get hold of the pin as seen in GraphEdit
        hr = pFilter->FindPin(bstrPinName,&pPin);
        if(SUCCEEDED(hr))
        {
            return pPin;
        }
    }    
}
return NULL;
void Set_CaptureGraph_Ptr(ICaptureGraphBuilder2* pCG);
  • IBaseFilter *Filter_Init(IMoniker*);:过滤器通过此方法进行初始化。
  • void Filter_Connect(IPin* pPinOut , IPin* pPinIn);:过滤器通过此方法进行连接。
  • void Filter_Addto_Graph(IBaseFilter* pFilter,BSTR bstrName);:将过滤器添加到主图,即 pGraph
  • void Set_MainGraph_Ptr(IGraphBuilder* pGraph);:由于此方法也继承了 pGraph 指针,因此将来自主类 CMainGraphpGraph 的引用设置为此类中的 pGraph。请确保相机和 VMR9 过滤器都引用相同的 pGraph 指针。
  • void Set_CaptureGraph_Ptr(ICaptureGraphBuilder2* pCG);:与上面解释的 Set_MainGraph_Ptr(..) 相同,只是 pGraph 指针不同。

CFilterList

CFilterList 类保留了过滤器的 STL 列表。当您运行程序时,您将看到两个组合框,它们分别包含相机过滤器和其它过滤器的列表。那么,我为什么要使用 STL 列表呢?嗯,显然是为了存储不同类型的过滤器信息。但首先,以下是该类的列表。

class CFilterList
{
public:
    CFilterList(void);
    ~CFilterList(void);
public:
    // STL List to hold filters/device friendly names
    list<BSTR> listCamFilters;
    list<BSTR>::iterator iterCam;
    list<BSTR> listVRFilters;
    list<BSTR>::iterator iterVR;
    // STL list to hold monikers
    list<IMoniker*> pListCamFilterMoniker;
    list<IMoniker*>::iterator itermCam;
    list<IMoniker*> pListVRFilterMoniker;
    list<IMoniker*>::iterator itermVR;
    // Filter/Device reader
    void Filter_Read(GUID FILTER_CLSID,ICreateDevEnum* pFilterEnum);
    // Displays a message box when a COM error occurs
    void HR_Failed(HRESULT hr);
    //Compares two fitler names - true if filter found on system
    bool BSTR_Compare(BSTR bstrFilterName, BSTR bstrDeviceName);
};

两个单独的 STL 列表存储了与过滤器相关的两种类型的信息。第一个列表 listCamFilters 包含 BSTR 类型,实际上是相机过滤器的友好名称。第二个列表 listVRFilters 存储了视频渲染器过滤器的友好名称。第二类列表 pListCamFilterMonikerpListVRFilterMoniker 存储了相机和视频渲染器的 monikers 列表。您可以清楚地推断出每个列表的迭代器将用于什么。那么,为什么仍然使用 STL 呢?嗯,一旦枚举了设备并使用了 monikers,比如说我们实例化了这些设备(尽管实例化是由另一个方法完成的),我只是将所有这些过滤器保存在 STL 列表中。我使用 BSTR 类型列表在组合框中保存友好名称,而 IMoniker 类型 STL 列表则保留与每个友好名称对应的 moniker 列表。现在,在运行的程序中,当点击视频渲染器列表时,会从列表中选取友好名称,并启动搜索。在搜索过程中,程序还会通过 BSTR_Compare(...) 的“true”返回值来筛选 STL moniker 列表,这意味着找到了一个过滤器,并调用了过滤器实例化。STL 中的 moniker 会被发送到过滤器实例化方法。虽然您可能会觉得这很复杂,但只需查看下面的视频渲染器过滤器的代码,就会更容易理解其余代码。

void CVideoCaptureDlg::OnCbnSelchangeVrList()
{
    //Temporary listbox
    CComboBox *pComboVRFilter = static_cast<CComboBox*>(this->GetDlgItem(IDC_VR_LIST));
    int selectedIndex = pComboVRFilter->GetCurSel();
    CString strFilterName;
    pComboVRFilter->GetLBText(selectedIndex,strFilterName);
    //Find the required filter moniker
    FLObject.itermVR = FLObject.pListVRFilterMoniker.begin();
    BSTR temp = SysAllocString(strFilterName);
    for(
        FLObject.iterVR = FLObject.listVRFilters.begin();
        FLObject.iterVR != FLObject.listVRFilters.end();
        FLObject.iterVR++
        )
        {
            //check if there is a filter on the Video Renderers list for Video Render
            if((FLObject.BSTR_Compare(temp,*FLObject.iterVR)) == true)
            {
                //Initiate Filter
                VMR9Object.pVMR9 = VMR9Object.Filter_Init((*FLObject.itermVR));
                if(VMR9Object.pVMR9!=NULL)
                {
                    //Add to main graph
                    VMR9Object.Filter_Addto_Graph(VMR9Object.pVMR9,temp);
                    break;
                }
            }
            FLObject.itermVR++;
        }
    //Enable the connect filters button
    this->GetDlgItem(IDC_CONNECT_FILTERS)->EnableWindow(1);
}

void CFilterList::Filter_Read(GUID FILTER_CLSID,ICreateDevEnum* pFilterEnum):最重要的方法之一,用于在按下“查找过滤器”按钮时读取过滤器。此方法开始填充 STL 列表,其部分代码如下所示。

if(SUCCEEDED(hr)) 
{

    //check device category
    if(FILTER_CLSID == CLSID_VideoInputDeviceCategory)
    {
        //store the moniker in the camera STL list
        listCamFilters.push_front(SysAllocString(varName.bstrVal));
    
        pListCamFilterMoniker.push_front(pDeviceMonik);
    }
    else
    {
        //store the moniker in the video renderer STL list
        listVRFilters.push_front(SysAllocString(varName.bstrVal));
        pListVRFilterMoniker.push_front(pDeviceMonik);
    
    }
}
else HR_Failed(hr);

CCamerFilter

CCameraFilter 是类中最小的一个,如下所示。

class CCameraFilter :
    public CFilter
{
public:
    CCameraFilter(void);
    ~CCameraFilter(void);
    // Cam Filter
    IBaseFilter* pCamFilter;
};

它有一个 IBaseFilter 类型的指针成员,该成员保存了相机的引用。由于大部分功能都在 CFilter 类中定义,因此这里没有什么特别的操作。

CVMR9Filter

CVMR9Filter 类是所有类中最棘手的。其列表如下。

class CVMR9Filter :
    public CFilter
{
public:
    CVMR9Filter(void);
    ~CVMR9Filter(void);
    // VMR9 Filter
    IBaseFilter* pVMR9;
    // Set the VMR9 windowless mode
    void Set_Windowess_Mode(HWND hwndApp,LPRECT DrawRect);
    // Render stream for filters. i.e. connect
    void Filter_RenderStream(GUID PIN_TYPE,GUID MEDIA_TYPE,IBaseFilter*);
};

我应该将该类命名为 CVRFilter,但我最初的意图是只使用 VMR9 过滤器。我最终创建了一个通用类,只是没有勇气更改所有变量。所以,请记住,这是一个更通用的类,但有一个很大的例外,即 Set_Windowless_Mode(...) 方法。这是 VMR9 的亮点,我使用了它,因此我仍然认为类名是合适的。捕获的视频然后以无窗口模式渲染,并渲染在由我称之为 IDC_VIDEO_FRAME 的组框坐标定义的位置。此组框的位置决定了渲染坐标。另一个有趣的方法是 Filter_RenderStream(...)。通常,连接过滤器可以通过简单的方式完成,我使用了名为 Filter_Connect() 的方法。此方法接收两个引脚和两个过滤器并将它们连接起来。但是,对于 VMR9,我们可以使用内置方法 RenderStream(NULL,NULL,src,NULL,dest);。这是 ICaptureBuilderGraph2 接口暴露的一个方法。该方法可以将相机过滤器“src”连接到视频渲染器“dest”过滤器。我已使用此方法将相机连接到 VMR9 过滤器。代码如下所示:

// Render stream for filters. i.e. connect
void CVMR9Filter::Filter_RenderStream(GUID PIN_TYPE,GUID MEDIA_TYPE,IBaseFilter *pSrcFilter)
{
    HRESULT hr;
    hr = this->pCaptureGraph2->RenderStream(NULL,NULL,pSrcFilter,NULL,this->pFilter);
    if(SUCCEEDED(hr))
        {
        }
    else HR_Failed(hr);
}

我们正在使用 Filter_RenderStream(...) 方法。它也应该能正常工作。

一切发生的地方

由于程序是基于对话框的,大多数方法调用都来自 *VideoCaptureDlg.cpp*;该文件包含调用方法以实例化过滤器、连接它们并启动视频渲染的逻辑。CVideoCaptureDlg 类的主要方法如下:

// Find filters
afx_msg void OnBnClickedFindFilters();
// Connect filters
afx_msg void OnBnClickedConnectFilters();
// Camera list selected
afx_msg void OnCbnSelchangeCamList();
// Video Renderer list selected
afx_msg void OnCbnSelchangeVrList();
// Play/Stop
afx_msg void OnBnClickedPlaystopButton();

尽管这是一个非常长的文件,但最有趣的是事件处理程序 OnBnClickedConnectFilters()。我将分步编写它:

  1. 此方法开始声明用作输入和输出的引脚。
  2. 查找相机的“capture”引脚。
  3. 查找 VMR9 的“VMR9 Input0”引脚。
  4. 检索组框的坐标。
  5. 设置渲染坐标。
  6. 设置 VMR9 的无窗口模式。
  7. 连接过滤器。
  8. 最后,启动图。

还记得 Find_Pin(...) 的重载函数吗?嗯,这里是你可以使用它们的地方,我已经在代码中用注释放置了它们。

运行程序的步骤

确保您已设置正确的 Windows SDK 路径,并在看到 GUI 后点击“运行”。

  • 步骤 1. 按“查找过滤器”。
  • 步骤 2. 选择正确的相机,然后选择 VMR9 过滤器。在此步骤中要小心。
  • 步骤 3. 点击“连接”。
  • 步骤 4. 希望您能看到视频。
  • 步骤 5. 尝试使用“停止/播放”按钮。

屏幕截图

1.jpg

2.jpg

3.jpg

4.jpg

5.jpg

我的双显示器的视频截图。

关注点

最有趣的一点?嗯,如果您无法引用 VMR9 过滤器,则 VMR9 的指针将为零,如果您在使用正确的相机引用的同时使用 RenderStream(...),您仍然能够渲染视频!但不是在组框中,而是会调用您的“第三方”软件。这种情况发生在我身上,我花了一周的时间才弄清楚原因并修复它!

代码中的 bug

您会发现 bug!没有处理异常,所以请注意!

该代码使用以下内容构建:

  • Windows Server 2008
  • Microsoft Visual Studio 2008
  • DirectX 10.1
  • Microsoft Windows SDK 6.1

历史

  • 首次发布 - 2009 年 3 月 23 日。
© . All rights reserved.