基本的视频捕获和 VMR9






4.83/5 (11投票s)
使用 DirectShow 从网络摄像头捕获视频,并使用 VMR9 进行无窗口渲染。
引言
简单来说,这个小程序将帮助您开始从网络摄像头进行视频捕获,并允许使用 VMR9 进行无窗口渲染。此刻,我想说的是,程序会读取系统中所有可用的过滤器;用户“必须”选择合适的过滤器才能正确运行程序。除了 MSDN 上你能找到的 DirectShow 帮助之外,我希望这小段代码能让你对 DirectShow 的工作方式有所了解,因为 MSDN 的帮助总是像戛然而止一样。我添加了屏幕截图,以便使用程序更加方便。
背景
除了 MFC 之外,还需要 STL、COM 编程基础和 DirectShow 基础。如果您不了解过滤器是如何被读取和使用的,请参考我的其他文章,其中详细解释了过滤器的使用方法,并且还提供了对 BSTR_Compare(..)
方法的解释。相关主题的链接是一个三部分教程,如下所示:
使用代码
要使用该代码,请确保您已更改包含目录的程序路径。特别是,必须正确配置 Windows SDK 的路径。
类
该程序是使用类开发的。以下类图应该能让你对内容有所了解。并非所有方法都得到了解释,因为其中一些方法非常简单,例如,对于具有 DirectShow 基本知识的开发人员来说。
类的解释
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。它设置对其他接口的引用,例如ICaptureGraphBuilder2
、IMediaControl
等,并查询所需的接口以便停止/运行图。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
指针,因此将来自主类CMainGraph
的pGraph
的引用设置为此类中的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
存储了视频渲染器过滤器的友好名称。第二类列表 pListCamFilterMoniker
和 pListVRFilterMoniker
存储了相机和视频渲染器的 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()
。我将分步编写它:
- 此方法开始声明用作输入和输出的引脚。
- 查找相机的“capture”引脚。
- 查找 VMR9 的“VMR9 Input0”引脚。
- 检索组框的坐标。
- 设置渲染坐标。
- 设置 VMR9 的无窗口模式。
- 连接过滤器。
- 最后,启动图。
还记得 Find_Pin(...)
的重载函数吗?嗯,这里是你可以使用它们的地方,我已经在代码中用注释放置了它们。
运行程序的步骤
确保您已设置正确的 Windows SDK 路径,并在看到 GUI 后点击“运行”。
- 步骤 1. 按“查找过滤器”。
- 步骤 2. 选择正确的相机,然后选择 VMR9 过滤器。在此步骤中要小心。
- 步骤 3. 点击“连接”。
- 步骤 4. 希望您能看到视频。
- 步骤 5. 尝试使用“停止/播放”按钮。
屏幕截图
我的双显示器的视频截图。
关注点
最有趣的一点?嗯,如果您无法引用 VMR9 过滤器,则 VMR9 的指针将为零,如果您在使用正确的相机引用的同时使用 RenderStream(...)
,您仍然能够渲染视频!但不是在组框中,而是会调用您的“第三方”软件。这种情况发生在我身上,我花了一周的时间才弄清楚原因并修复它!
代码中的 bug
您会发现 bug!没有处理异常,所以请注意!
该代码使用以下内容构建:
- Windows Server 2008
- Microsoft Visual Studio 2008
- DirectX 10.1
- Microsoft Windows SDK 6.1
历史
- 首次发布 - 2009 年 3 月 23 日。