2.3 DirectShow视频采集方案
流媒体处理技术以其复杂性和技术性一直受到人们的关注。随着网络技术的不断发展,流媒体在网络上得到了广泛地应用。如何能够简单、有效地进行流媒体处理,已成为一个焦点问题。为此,Microsoft推出了DirectShow,DirectShow是Microsoft推出的基于Windows平台的流媒体处理开发包,它与DirectX一起发布。DirectShow对流媒体的捕捉、回放提供了强大的支持,使用它还可以在基于WDM驱动的采集卡上进行数据捕捉。本节将介绍有关DirectShow的相关知识。
2.3.1 DirectShow系统结构分析
DirectShow主要由过滤器(Filter Graph)图表构成。过滤图表中包含了各种Filter,这些Filter能够按一定顺序连接在一起,构成一条流水线。从功能的角度划分,Filter大体可以分为3类,Source Filters、Transform Filters和Rendering Filters。Source Filters主要负责获取数据,可以是一个文件、一个采集卡、声卡或数码相机等。Transform Filters负责数据的转换、传输。例如各种编码器、解码器等。Rendering Filters负责数据的最终去向,例如将数据传送到声卡、显卡或存储为文件。
在开发DirectShow应用程序时,通常需要设计一个过滤图表(Filter Graph),向过滤图表中添加相应的过滤器,最后连接过滤器的引脚就完成了功能的设计。例如,实现一个简单的视频预览功能,需要向过滤图表中添加一个视频捕捉源过滤器和一个Video Renderer过滤 器,将视频捕捉源过滤器的输出引脚与Video Renderer过滤器的输入引脚相连就可以了。而在程序中只需要按照设计过滤图表的捕捉添加过滤器并连接过滤器引脚就可以了。在连接过滤器引脚时需要注意:只能是输出过滤器引脚与输入过滤器引脚相连,两个输出过滤器或两个输入过滤器引脚是不能相连的。
为了在程序中使用DirectShow,需要单独安装DirectX,当前DirectX的最新版本为9.0,即DirectX9.0,用户可以从Microsoft的官方网站上免费下载。在安装DirectX之后,程序中需要引用“dshow.h”头文件,并导入“Strmiids.lib”库文件和“quartz.lib”库文件才可以使用DirectShow。代码如下:
#pragma comment (lib,"Strmiids")
#pragma comment (lib,"quartz")
#include <dshow.h>
2.3.2 Filter图表设计
为了方便用户设计过滤图表,DirectX提供了一个Graph Edit工具。用户可以单击“开始”菜单下的“Microsoft DirectX 9 SDK\DirectX Utilities\Graph Edit”菜单项打开Graph Edit工具,如图2.10所示。

图2.10 Graph Edit工具
下面笔者介绍如何使用Graph Edit工具设计过滤图表,过滤图表的功能是实现视频的预览功能。具体步骤如下:
(1)在图2.4中单击“Graph/Insert Filters”菜单项打开“添加过滤器”窗口,如图2.11所示。

图2.11 添加过滤器窗口
(2)在“Video Capture Sources”节点下选择一个视频捕捉源过滤器,单击“Insert Filter”按钮将其添加到过滤图表中,如图2.12所示

图2.12 添加视频捕捉源过滤器窗口
提示:如果系统中没有安装摄像头及其驱动程序,该节点下将不会有视频捕捉源过滤器。
(3)在“DirectShow Filters”节点下选择“Video Renderer”过滤器,将其添加到图表中,如图2.13所示。

图2.13 添加“Video Renderer”过滤器窗口
(4)利用鼠标将视频捕捉源过滤器的“Capture”引脚与“Video Renderer”过滤器的“VMR Input0”引脚相连,如图2.14所示。

图2.14 连接过滤器引脚窗口
(5)单击工具栏中的“
”按钮运行过滤图表,将显示一个视频预览窗口,如图2.15所示。

图2.15 视频预览窗口
2.3.3 枚举系统设备
使用Graph Edit工具,用户可以非常方便地获得与某一系统设备相关的过滤器。但是,在程序中该如何获得这些过滤器呢?
用户可以采用枚举的方式列举系统中安装的设备。以列举系统中的视频捕捉设备为例,首先定义一个设备列举接口ICreateDevEnum的一个指针,调用CoCreateInstance方法创建ICreateDevEnum实例,然后定义一个列举监视器IEnumMoniker的一个指针,调用ICreateDevEnum实例的CreateClassEnumerator方法创建IEnumMoniker实例,最后以循环的方式调用IEnumMoniker实例的Next方法遍历系统设备,调用IEnumMoniker实例的BindToObject方法将系统设备绑定到过滤器上。
在上面的描述中,ICreateDevEnum实例的CreateClassEnumerator方法的第一个参数确定枚举的系统设备。例如,第一个参数为CLSID_VideoInputDeviceCategory,表示将要枚举系统中的视频捕捉卡,为CLSID_VideoCompressorCategory,表示枚举系统中的视频压缩器。下面的代码演示了如何枚举系统中的视频捕捉卡。
//枚举视频设备
ICreateDevEnum *pDevEnum = NULL;
CoCreateInstance(CLSID_SystemDeviceEnum, NULL, CLSCTX_INPROC,
IID_ICreateDevEnum, (void **)&pDevEnum);
IEnumMoniker *pClassEnum = NULL;
pDevEnum->CreateClassEnumerator(CLSID_VideoInputDeviceCategory, &pClassEnum, 0);
ULONG cFetched;
while (pClassEnum->Next(1, &pMoniker, &cFetched) == S_OK)
{
pMoniker->BindToObject(0, 0, IID_IBaseFilter, (void**)&pSrc);
pMoniker->Release();
break;
}
pClassEnum->Release();
而下面的代码则用于判断系统中是否安装了指定的视频压缩器。
ICreateDevEnum *pDevEnum = NULL;
CoCreateInstance(CLSID_SystemDeviceEnum, NULL, CLSCTX_INPROC,
IID_ICreateDevEnum, (void **)&pDevEnum);
IEnumMoniker *pClassEnum = NULL;
//列举视频压缩设备
pDevEnum->CreateClassEnumerator(CLSID_VideoCompressorCategory, &pClassEnum, 0);
while (pClassEnum->Next(1, &pMoniker, &cFetched) == S_OK)
{
IPropertyBag* pProp= NULL;
pMoniker->BindToStorage(0, 0, IID_IPropertyBag, (void**)&pProp);
VARIANT varName;
varName.vt = VT_BSTR;
pProp->Read(L"FriendlyName", &varName,0);
CString str = varName.bstrVal;
if (str.Find("Microsoft Video 1",0)!= -1)
{
pMoniker->BindToObject(0, 0, IID_IBaseFilter, (void**)&pCompress);
pMoniker->Release();
break;
}
VariantClear(&varName);
}
pClassEnum->Release();
2.3.4 查找Filter Pin
每一个过滤器(Filter)至少应有一个引脚(Pin),或者是输入引脚或者是输出引脚。有些过滤器还拥有多个引脚,即又输入引脚又有输出引脚。但是过滤器的输入、输出引脚并不是对应的,有些过滤器可以有多个输入引脚,而只有一个输出引脚或者没有输出引脚。在程序中为了连接过滤器间的引脚,通常需要获得过滤器的各个引脚。用户可以使用IEnumPins接口来枚举某一个过滤器的输入、输出引脚。过滤器IBaseFilter提供了一个EnumPins方法用于生成一个IEnumPins接口实例,这样,通过调用IEnumPins的Next方法便可以访问各个引脚了。下面的代码定义了一个FindPin函数,用于获得某个过滤器的输入或输出引脚。
//查找引脚
IPin* CKinescopeDlg::FindPin(IBaseFilter *pFilter, PIN_DIRECTION dir)
{
IEnumPins* pEnumPins;
IPin* pOutpin;
PIN_DIRECTION pDir;
pFilter->EnumPins(&pEnumPins);
while (pEnumPins->Next(1,&pOutpin,NULL)==S_OK)
{
pOutpin->QueryDirection(&pDir);
if (pDir==dir)
{
return pOutpin;
}
}
return 0;
}
用户可以按下面的方式获得某个过滤器的输入、输出引脚。
IPin * pComOut,*pComIn ;
pComIn = FindPin(pCompress,PINDIR_INPUT);
pComOut = FindPin(pCompress,PINDIR_OUTPUT);
2.3.5 连接Filter Pin
使用Graph Edit工具,用户可以利用鼠标非常方便地连接两个过滤器间的引脚。但是在程序中却没这么简单了。首先需要按照2.3.4节介绍的方法获得两个过滤器的输入、输出引脚,然后将第一个过滤器的输出引脚连接到第二个过滤器的输入引脚,其中,连接两个引脚需要调用IGraphBuilder接口的ConnectDirect方法。下面的代码演示了如何连接两个过滤器的引脚。
IPin * pComOut,*pComIn ;
pComIn = FindPin(pCompress,PINDIR_INPUT);
pComOut = FindPin(pCompress,PINDIR_OUTPUT);
IPin* pOutpin = FindPin(pSrc,PINDIR_OUTPUT); //pSrc的输出引脚
HRESULT result ;
result = pGraph->ConnectDirect(pOutpin,pComIn,NULL);
2.3.6 视频预览设计方案
在开发视频应用程序时,一个最基本的功能是视频预览。本节将介绍如何应用DirectShow实现视频预览,效果如图2.16所示。

图2.16 视频预览设计方案
在使用DirectShow开发应用程序时,通常需要先设计过滤图表,然后根据图表来设计应用程序。在设计视频预览过滤图表时,只需要两个步骤,第一个步骤是添加视频捕捉的源过滤器,第二个步骤是添加Video Render过滤器,并连接两个过滤器引脚。过滤图表具体设计步骤如下:
(1)启动Graph Edit工具, 在Graph Edit工具中单击“Graph \Insert Filters”菜单项打开“添加过滤器窗口”,在“Video Capture Sources”节点下选择一个视频捕捉源过滤器,单击“Insert Filter”按钮将其添加到过滤图表中,如图2.11所示。
(2)在“DirectShow Filters”节点下将“Video Render”过滤器添加到图表中,如图2.17所示。

图2.17 过滤图表设计1
(3)连接过滤器引脚,如图2.18所示。

图2.18 过滤图表设计2
至此,完成视频预览过滤图表的设计。单击“Play”按钮运行过滤图表,将弹出一个视频预览窗口,如图2.19所示。

图2.19 DerictShow预览窗口
程序具体步骤如下:
实例位置:光盘\mr\2\2.3\2.3.6\01
(1)创建一个基于对话框的工程,在对话框类的头文件中引用“dshow.h”头文件。
#pragma comment (lib,"Strmiids")
#pragma comment (lib,"quartz")
#include <dshow.h>
(2)在对话框类中定义如下成员变量。
IMediaControl *pMediaControl ; //媒体控制
IGraphBuilder *pGraph; //过滤图表
IBaseFilter *pSrc,*pPreview; //过滤器
IMoniker *pMoniker; //监视器
(3)向对话框类中添加FindPin方法,查找过滤器的引脚。
//查找引脚
IPin* CPreviewDlg::FindPin(IBaseFilter *pFilter, PIN_DIRECTION dir)
{
IEnumPins* pEnumPins;
IPin* pOutpin;
PIN_DIRECTION pDir;
pFilter->EnumPins(&pEnumPins);
while (pEnumPins->Next(1,&pOutpin,NULL)==S_OK)
{
pOutpin->QueryDirection(&pDir);
if (pDir==dir)
{
return pOutpin;
}
}
return 0;
}
(4)在应用程序类的InitInstance方法中初始化Com接口。
//初始化Com
CoInitialize(NULL);
(5)在对话框初始化时枚举视频捕捉设备,设计过滤图表。
ICaptureGraphBuilder2 * pBuilder = NULL;
pGraph = NULL;
pMediaControl = NULL;
//枚举视频设备
ICreateDevEnum *pDevEnum = NULL;
CoCreateInstance(CLSID_SystemDeviceEnum, NULL, CLSCTX_INPROC,
IID_ICreateDevEnum, (void **)&pDevEnum);
IEnumMoniker *pClassEnum = NULL;
pDevEnum->CreateClassEnumerator(CLSID_VideoInputDeviceCategory, &pClassEnum, 0);
ULONG cFetched;
if (pClassEnum->Next(1, &pMoniker, &cFetched) == S_OK)
{
pMoniker->BindToObject(0, 0, IID_IBaseFilter, (void**)&pSrc);
pMoniker->Release();
}
pClassEnum->Release();
CoCreateInstance(CLSID_CaptureGraphBuilder2,0,
CLSCTX_INPROC_SERVER,IID_ICaptureGraphBuilder2,(void**)&pBuilder);
CoCreateInstance(CLSID_FilterGraph, NULL, CLSCTX_INPROC_SERVER,
IID_IGraphBuilder, (void **)&pGraph);
pBuilder->SetFiltergraph(pGraph);
pGraph->QueryInterface(IID_IMediaControl,(void**)&pMediaControl);
pGraph->AddFilter(pSrc,L"avi");
pPreview = NULL;
CoCreateInstance(CLSID_VideoRenderer,0,CLSCTX_ALL,
IID_IBaseFilter,(void**)&pPreview);
if (pPreview != NULL)
{
pGraph->AddFilter(pPreview,L"preview");
//连接引脚
IPin * pSourceOut;
pSourceOut = FindPin(pSrc,PINDIR_OUTPUT);
IPin* pPreIn = FindPin(pPreview,PINDIR_INPUT);
pGraph->ConnectDirect(pSourceOut,pPreIn,NULL);
//获取预览窗口
IVideoWindow * pViewWnd= NULL;
pPreview->QueryInterface(IID_IVideoWindow,(void**)&pViewWnd);
if (pViewWnd)
{
//设置预览窗口的拥有者
pViewWnd->put_Owner((long)m_hWnd);
pViewWnd->put_Left(1);
pViewWnd->put_Top(1);
//获取预览窗口风格
long style;
pViewWnd->get_WindowStyle(&style);
style = style & ~WS_CAPTION;
style = style & ~WS_DLGFRAME;
style = style & WS_CHILD;
pViewWnd->put_WindowStyle(style);
//设置预览窗口宽度和高度
CRect rc;
GetClientRect(rc);
pViewWnd->put_Height(rc.Height()-60);
pViewWnd->put_Width(rc.Width()-2);
}
pMediaControl->Run();
}
2.3.7 事件通知设计方案
在使用DirectShow开发应用程序时,通常需要DirectShow与应用程序之间进行交互。例如,在使用DirectShow播放影视文件时,在文件播放完毕后,应用程序需要得到通知,并通知用户文件播放完毕。DirectShow采用了事件通知的机制与用户应用程序进行交互。当过滤器的状态改变时,或者重画视频窗口时,都将产生相应的事件,由DirectShow处理并转发给应用程序,应用程序能够接收到该事件,并根据事件的不同类型作出相应的处理。DirectShow提供了3个有关事件通知的接口,分别为“IMediaEventSink”、“IMediaEvent”和“IMediaEventEx”。其中,IMediaEventEx接口是IMediaEvent接口的扩展,在应用程序中经常使用该接口类处理事件。IMediaEventEx接口的主要方法如下:
1.GetNotifyFlags方法
该方法用于获取事件通知是否被激活。语法如下:
HRESULT GetNotifyFlags( long *lplNoNotifyFlags);
参数说明:
lplNoNotifyFlags:确定事件通知是否被激活。如果为零,表示事件通知被激活,为AM_MEDIAEVENT_NONOTIFY,表示事件通知不可用。
2.SetNotifyFlags方法
该方法用于设置事件通知是否被激活。语法如下:
HRESULT SetNotifyFlags( long lNoNotifyFlags);
lplNoNotifyFlags:如果为零,表示激活事件通知,为AM_MEDIAEVENT_NONOTIFY,表示禁止事件通知。
3.SetNotifyWindow方法
该方法用于注册一个窗口来处理事件通知。语法如下:
HRESULT SetNotifyWindow( OAHWND hwnd, long lMsg, long lInstanceData);
参数说明:
hwnd:表示处理事件通知的窗口句柄。
lMsg:表示与事件通知关联的窗口消息。
lInstanceData:作为lMsg消息处理函数的lParam参数被传递。
4.GetEventHandle方法
该方法用于获取人工重置事件对象的句柄。语法如下:
HRESULT GetEventHandle(OAEVENT *hEvent);
参数说明:
hEvent:表示返回的人工重置事件对象的句柄。
5.GetEvent方法
该方法用于从消息队列中返回下一个事件通知。语法如下:
HRESULT ( long *lEventCode, long *lParam1, long *lParam2, long msTimeout);
参数说明:
lEventCode:表示接收的事件通知代码。
lParam1:表示第一个事件参数。
lParam2:表示第2个事件参数。
msTimeout:表示超时时间,如果为NFINITE,将一直等待,直到有事件通知为止。
6.FreeEventParams方法
该函数用于释放事件参数。语法如下:
HRESULT FreeEventParams( long lEventCode, long lParam1, long lParam2);
参数说明:
lEventCode:表示接收的事件通知代码。
lParam1:表示第一个事件参数。
lParam2:表示第2个事件参数。
7.CancelDefaultHandling方法
该方法用于取消DirectShow对事件进行的默认处理。语法如下:
HRESULT CancelDefaultHandling( long lEvCode);
参数说明:
lEvCode:表示事件通知代码。
8.RestoreDefaultHandling方法
该方法用于恢复DirectShow对默认消息的处理。语法如下:
HRESULT RestoreDefaultHandling( long lEvCode);
参数说明:
lEvCode:表示事件通知代码。
在应用程序中可以有两种方式来接收和处理DirectShow中的事件。第一种方式是让DirectShow向应用程序的窗口中发送消息。第2种方式是通过事件对象(这里的对象是指Windows内核对象)。向对而言,第一种方式比较简单,也比较常用,而第2种方式多用于控制台应用程序或多线程应用程序。下面笔者将分别以这两种方式来实现事件通知消息。为了演示应用程序获得了DirectShow发送的事件通知,下面的两个例子均以播放一个AVI文件为例,在DirectShow播放完AVI文件,将向应用程序发出通知,应用程序获得通知后,将弹出对话框提供用户,如图2.20、图2.21所示。

图2.20 事件通知方式1效果图

图2.21 事件通知方式2效果图
l 基于窗口消息的事件通知
实例位置:光盘\mr\2\2.3\2.3.7\01
(1)创建一个基于对话框的工程,在对话框中添加按钮、图像控件,如图2.22所示。

图2.22 事件通知方式1对话框设计
(2)在类向导的“Member Variables”选项卡中为图像控件命名,如图2.23所示。

图2.23 类向导窗口
(3)在对话框类的头文件中引用“dshow.h”头文件,并导入相应的库文件。
#pragma comment (lib,"Strmiids")
#pragma comment (lib,"quartz")
#include <dshow.h>
(4)在应用程序的InitInstance方法中初始化Com库。
BOOL CNotifyEventApp::InitInstance()
{
AfxEnableControlContainer();
#ifdef _AFXDLL
Enable3dControls();
#else
Enable3dControlsStatic()
#endif
CoInitialize(NULL);
CNotifyEventDlg dlg;
m_pMainWnd = &dlg;
int nResponse = dlg.DoModal();
if (nResponse == IDOK)
{
}
else if (nResponse == IDCANCEL)
{
}
return FALSE;
}
(5)改写应用程序的ExitInstance方法,在应用程序退出时卸载Com库。
int CNotifyEventApp::ExitInstance()
{
CoUninitialize();
return CWinApp::ExitInstance();
}
(6)在对话框类中添加如下成员变量。
IMediaControl *pMediaControl ; //媒体控制
IGraphBuilder *pGraph; //过滤图表
IBaseFilter *pSrc; //过滤器
IMoniker *pMoniker; //监视器
BOOL m_Previewed; //是否进行预览
IMediaEventEx *pEvent; //事件
IVideoWindow * pViewWnd; //预览窗口句柄
(7)自定义一个消息,用于在完成文件播放时向窗口中发送通知。
#define CM_NOTIFY WM_USER+1
(8)向对话框类中添加自定义消息处理函数OnGraphNotify。
void CNotifyEventDlg::OnGraphNotify(WPARAM wParam, LPARAM lParam)
{
if (pEvent)
{
LONG eventcode = 0,param1 =0,param2 = 0;
while (SUCCEEDED(pEvent->GetEvent(&eventcode,¶m1,¶m2,0)))
{
pEvent->FreeEventParams(eventcode,param1,param2);
switch (eventcode)
{
case EC_COMPLETE:
{
m_Previewed = FALSE;
pMediaControl->Stop();
pMediaControl->Release();
pGraph->Release();
pViewWnd->put_Visible(FALSE);
MessageBox("预览完成","提示");
m_Panel.ShowWindow(SW_HIDE);
m_Panel.ShowWindow(SW_SHOW);
break;
}
}
}
}
}
(9)在对话框的消息映射部分添加消息映射宏,将消息与消息处理函数关联起来。
BEGIN_MESSAGE_MAP(CNotifyEventDlg, CDialog)
//{{AFX_MSG_MAP(CNotifyEventDlg)
ON_WM_SYSCOMMAND()
ON_WM_PAINT()
ON_WM_QUERYDRAGICON()
ON_BN_CLICKED(IDC_ChOOSE, OnChooSE)
ON_MESSAGE(CM_NOTIFY,OnGraphNotify)
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
(10)处理“设置文件”按钮的单击事件,开始播放AVI文件。
void CNotifyEventDlg::OnChooSE()
{
CFileDialog fDlg(TRUE,NULL,NULL,OFN_HIDEREADONLY |
OFN_OVERWRITEPROMPT,"avi文件|*.avi");
if (fDlg.DoModal()==IDOK)
{
CString strFile= fDlg.GetPathName();
ICaptureGraphBuilder2 * pBuilder = NULL;
pGraph = NULL;
pMediaControl = NULL;
CoCreateInstance(CLSID_CaptureGraphBuilder2,0,CLSCTX_INPROC_SERVER,
IID_ICaptureGraphBuilder2,(void**)&pBuilder);
CoCreateInstance(CLSID_FilterGraph, NULL, CLSCTX_INPROC_SERVER,
IID_IGraphBuilder, (void **)&pGraph);
pBuilder->SetFiltergraph(pGraph);
pGraph->QueryInterface(IID_IMediaControl,(void**)&pMediaControl);
pGraph->RenderFile(strFile.AllocSysString(),NULL);
//获取预览窗口
pViewWnd= NULL;
pGraph->QueryInterface(IID_IVideoWindow,(void**)&pViewWnd);
if (pViewWnd)
{
//设置预览窗口的拥有者
pViewWnd->put_Owner((long)m_Panel. m_hWnd);
pViewWnd->put_Left(1);
pViewWnd->put_Top(1);
//获取预览窗口风格
long style;
pViewWnd->get_WindowStyle(&style);
style = style & ~WS_CAPTION;
style = style & ~WS_DLGFRAME;
style = style & WS_CHILD;
pViewWnd->put_WindowStyle(style);
//设置预览窗口宽度和高度
CRect rc;
m_Panel.GetClientRect(rc);
pViewWnd->put_Height(rc.Height());
pViewWnd->put_Width(rc.Width());
}
pGraph->QueryInterface(IID_IMediaEventEx, (void **)&pEvent);
pEvent->SetNotifyWindow((OAHWND)m_hWnd,CM_NOTIFY,0);
pMediaControl->Run();
m_Previewed = TRUE;
}
}
l 基于事件对象的通知
实例位置:光盘\mr\2\2.3\2.3.7\02
(1)创建一个基于对话框的工程,在对话框中添加按钮、图像控件,如图2.24所示。

图2.24 事件通知方式2话框设计
(2)在类向导的“Member Variables”选项卡中为图像控件命名。
(3)在对话框类的头文件中引用“dshow.h”头文件,并导入相应的库文件。
#pragma comment (lib,"Strmiids")
#pragma comment (lib,"quartz")
#include <dshow.h>
(4)在应用程序的InitInstance方法中初始化Com库。
BOOL CDirectShowEventApp::InitInstance()
{
AfxEnableControlContainer();
#ifdef _AFXDLL
Enable3dControls();
#else
Enable3dControlsStatic();
#endif
CoInitialize(NULL);
CDirectShowEventDlg dlg;
m_pMainWnd = &dlg;
int nResponse = dlg.DoModal();
if (nResponse == IDOK)
{
}
else if (nResponse == IDCANCEL)
{
}
return FALSE;
}
(5)改写应用程序的ExitInstance方法,在应用程序退出时卸载Com库。
int CDirectShowEventApp::ExitInstance()
{
CoUninitialize();
return CWinApp::ExitInstance();
}
(6)在对话框类中添加如下成员变量。
IMediaControl *pMediaControl ; //媒体控制
IGraphBuilder *pGraph; //过滤图表
IBaseFilter *pSrc; //过滤器
IMoniker *pMoniker; //监视器
BOOL m_Previewed; //是否进行预览
IMediaEventEx *pEvent; //事件
IVideoWindow * pViewWnd; //视频预览窗口
BOOL m_Completed; //预览是否完成
HANDLE m_hThread; //线程句柄
(7)在对话框初始化时初始化成员变量。
BOOL CDirectShowEventDlg::OnInitDialog()
{
CDialog::OnInitDialog();
ASSERT((IDM_ABOUTBOX & 0xFFF0) == IDM_ABOUTBOX);
ASSERT(IDM_ABOUTBOX < 0xF000);
CMenu* pSysMenu = GetSystemMenu(FALSE);
if (pSysMenu != NULL)
{
CString strAboutMenu;
strAboutMenu.LoadString(IDS_ABOUTBOX);
if (!strAboutMenu.IsEmpty())
{
pSysMenu->AppendMenu(MF_SEPARATOR);
pSysMenu->AppendMenu(MF_STRING, IDM_ABOUTBOX, strAboutMenu);
}
}
SetIcon(m_hIcon, TRUE);
SetIcon(m_hIcon, FALSE);
m_Completed = FALSE;
m_Previewed = FALSE;
m_hThread = NULL;
return TRUE;
}
(8)自定义一个消息CM_COMPLETE,在线程函数中如果检测到DirectShow完成了文件的播放,将发送该消息。
#define CM_COMPLETE WM_USER+1
(9)向对话框类中添加自定义消息CM_COMPLETE的消息处理函数。
void CDirectShowEventDlg::Done(WPARAM wParam, LPARAM lParam)
{
if (m_hThread)
{
TerminateThread(m_hThread,0);
m_hThread = NULL;
}
m_Previewed = FALSE;
pMediaControl->Stop();
pMediaControl->Release();
pGraph->Release();
pViewWnd->put_Visible(FALSE);
m_Panel.ShowWindow(SW_HIDE);
m_Panel.ShowWindow(SW_SHOW);
MessageBox("预览完成!","提示");
}
(10)向对话框类中添加消息映射宏,将CM_COMPLETE消息关联到消息处理函数Done上。
BEGIN_MESSAGE_MAP(CDirectShowEventDlg, CDialog)
//{{AFX_MSG_MAP(CDirectShowEventDlg)
ON_WM_SYSCOMMAND()
ON_WM_PAINT()
ON_WM_QUERYDRAGICON()
ON_BN_CLICKED(IDC_SETFILE, OnSetFile)
ON_MESSAGE(CM_COMPLETE,Done)
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
(11)添加一个全局的线程函数,在线程函数中接收并处理DirectShow中的事件。
DWORD WINAPI ThreadProc(LPVOID lpParameter )
{
CDirectShowEventDlg* pWnd = (CDirectShowEventDlg*)lpParameter;
HANDLE hEvent;
pWnd->pEvent->GetEventHandle( (OAEVENT*) &hEvent);
long code,p1,p2;
BOOL done = FALSE;
while (!done)
{
if (WaitForSingleObject(hEvent,80)==WAIT_OBJECT_0)
{
while (SUCCEEDED( pWnd->pEvent->GetEvent(&code,&p1,&p2,0)))
{
pWnd->pEvent->FreeEventParams(code,p1,p2);
if (code==EC_COMPLETE)
{
pWnd->m_Completed = TRUE;
pWnd->SendMessage(CM_COMPLETE);
done=true;
}
}
}
}
return 0 ;
}
(12)处理“设置文件”按钮的单击事件,开始播放Avi文件,在文件播放完毕弹出对话框。
void CDirectShowEventDlg::OnSetFile()
{
if (! m_Previewed)
{
CFileDialog fDlg(TRUE,NULL,NULL,OFN_HIDEREADONLY
| OFN_OVERWRITEPROMPT,"avi文件|*.avi");
if (fDlg.DoModal()==IDOK)
{
CString strFile= fDlg.GetPathName();
ICaptureGraphBuilder2 * pBuilder = NULL;
pGraph = NULL;
pMediaControl = NULL;
CoCreateInstance(CLSID_CaptureGraphBuilder2,0,
CLSCTX_INPROC_SERVER,IID_ICaptureGraphBuilder2,(void**)&pBuilder);
CoCreateInstance(CLSID_FilterGraph, NULL, CLSCTX_INPROC_SERVER,
IID_IGraphBuilder, (void **)&pGraph);
pBuilder->SetFiltergraph(pGraph);
pGraph->QueryInterface(IID_IMediaControl,(void**)&pMediaControl);
pGraph->RenderFile(strFile.AllocSysString(),NULL);
//获取预览窗口
pViewWnd= NULL;
pGraph->QueryInterface(IID_IVideoWindow,(void**)&pViewWnd);
if (pViewWnd)
{
//设置预览窗口的拥有者
pViewWnd->put_Owner((long)m_Panel. m_hWnd);
pViewWnd->put_Left(1);
pViewWnd->put_Top(1);
//获取预览窗口风格
long style;
pViewWnd->get_WindowStyle(&style);
style = style & ~WS_CAPTION;
style = style & ~WS_DLGFRAME;
style = style & WS_CHILD;
pViewWnd->put_WindowStyle(style);
//设置预览窗口宽度和高度
CRect rc;
m_Panel.GetClientRect(rc);
pViewWnd->put_Height(rc.Height());
pViewWnd->put_Width(rc.Width());
}
m_hThread = NULL;
pGraph->QueryInterface(IID_IMediaEventEx, (void **)&pEvent);
m_Completed = FALSE;
DWORD threadID;
m_hThread = CreateThread(NULL,0,ThreadProc,(void*)this,0,&threadID);
pMediaControl->Run();
m_Previewed = TRUE;
}
}
}
(13)在对话框关闭时判断线程是否结束,如果没有结束,则结束线程。
void CDirectShowEventDlg::OnCancel()
{
if (m_hThread)
{
TerminateThread(m_hThread,0);
m_hThread = NULL;
}
CDialog::OnCancel();
}
2.3.8 视频录像设计方案
相对于VFW,使用DirectShow开发视频录像程序比较容易。与开发视频预览程序相同,使用DirectShow开始视频录像程序,首先需要设计一个过滤图表,然后根据过滤图表设计应用程序。下面笔者介绍视频录像过滤图表的设计过程。
(1)在Graph Edit工具中单击“Graph \Insert Filters”菜单项打开“添加过滤器窗口”,在“Video Capture Sources”节点下选择一个视频捕捉源过滤器,单击“Insert Filter”按钮将其添加到过滤图表中,如图2.6所示。
(2)在“Video Compressors”节点下选择一个压缩器,将其添加到图表中,如图2.25所示。

图2.25 添加压缩器窗口
(3)在“DirectShow Filters”节点下将“AVI Mux”过滤器添加到图表中,如图2.26所示。

图2.26 添加“AVI Mux”过滤器窗口
(4)在“DirectShow Filters”节点下将“File writer”过滤器添加到图表中,此时将弹出一个文件选择窗口,让用户将“File writer”过滤器关联到一个文件上,如图2.27所示。

图2.27 文件选择窗口
(5)为“File writer”过滤器关联一个文件后,它将被添加到过滤图表中,如图2.28所示。

图2.28 添加“File writer”过滤器窗口
(6)利用鼠标连接过滤器的各个引脚,如图2.29所示。

图2.29 连接过滤器引脚窗口
至此完成了过滤图表的设计,单击“
”按钮运行过滤图表,DirectShow会将捕捉的视频数据进行压缩,并存储在“c:\Encode.avi”文件中。
接下来,笔者将根据图2.29所示的过滤图表设计应用程序,效果如图2.30所示。

图2.30 视频录像设计方案
具体步骤如下:
实例位置:光盘\mr\2\2.3\2.3.8\01
(1)创建一个基于对话框的工程,向对话框中添加按钮、编辑框、静态文本等控件,如图2.31所示。

图2.31 视频录像设置窗口
(2)在对话框的头文件中引用“dshow.h”头文件。
#pragma comment (lib,"Strmiids")
#pragma comment (lib,"quartz")
#include <dshow.h>
(3)在应用程序初始化时初始化Com接口。
CoInitialize(NULL);
(4)在对话框类中定义如下成员变量。
IMediaControl *pMediaControl ; //媒体控制
IMoniker *pMoniker; //监视器
IBaseFilter *pSrc ,*pMux ,*pWriter,*pCompress; //过滤器
IFileSinkFilter2 *pSink ; //文件接收
IGraphBuilder *pGraph; //过滤图表
BOOL m_IsPause; //是否暂停
BOOL m_IsRecorded; //是否进行了录音
(5)向对话框类中添加FindPin方法,查找过滤器的指定引脚。
//查找引脚
IPin* CKinescopeDlg::FindPin(IBaseFilter *pFilter, PIN_DIRECTION dir)
{
IEnumPins* pEnumPins;
IPin* pOutpin;
PIN_DIRECTION pDir;
pFilter->EnumPins(&pEnumPins);
while (pEnumPins->Next(1,&pOutpin,NULL)==S_OK)
{
pOutpin->QueryDirection(&pDir);
if (pDir==dir)
{
return pOutpin;
}
}
return 0;
}
(6)向对话框资源中添加按钮、编辑框、静态文本等控件,如图2.32所示。

图232 对话框资源设计窗口
(7)处理“…”按钮的单击事件,弹出一个文件保存对话框,让用户输入或选择一个文件。
void CKinescopeDlg::OnOK()
{
CFileDialog FileDlg(FALSE,"AVI",NULL,OFN_HIDEREADONLY |
OFN_OVERWRITEPROMPT,"视频文件|*.avi|",this);
if (FileDlg.DoModal()==IDOK)
{
CString str;
str = FileDlg.GetPathName();
m_File.SetWindowText(str);
}
}
(8)处理“录像”按钮的单击事件,根据过滤图表,在程序中添加相应的过滤器,并连接过滤器的引脚。
void CKinescopeDlg::OnKinscope()
{
CString str;
m_File.GetWindowText(str);
if (str.IsEmpty())
{
MessageBox("请选择或输入文件");
return;
}
ICaptureGraphBuilder2 * pBuilder = NULL;
pGraph = NULL;
pMediaControl = NULL;
pCompress = NULL;
//枚举视频设备
ICreateDevEnum *pDevEnum = NULL;
CoCreateInstance(CLSID_SystemDeviceEnum, NULL, CLSCTX_INPROC,
IID_ICreateDevEnum, (void **)&pDevEnum);
IEnumMoniker *pClassEnum = NULL;
pDevEnum->CreateClassEnumerator(CLSID_VideoInputDeviceCategory, &pClassEnum, 0);
ULONG cFetched;
if (pClassEnum->Next(1, &pMoniker, &cFetched) == S_OK)
{
pMoniker->BindToObject(0, 0, IID_IBaseFilter, (void**)&pSrc);
pMoniker->Release();
}
pClassEnum->Release();
//列举视频压缩设备
pDevEnum->CreateClassEnumerator(CLSID_VideoCompressorCategory, &pClassEnum, 0);
while (pClassEnum->Next(1, &pMoniker, &cFetched) == S_OK)
{
IPropertyBag* pProp= NULL;
pMoniker->BindToStorage(0, 0, IID_IPropertyBag, (void**)&pProp);
VARIANT varName;
varName.vt = VT_BSTR;
pProp->Read(L"FriendlyName", &varName,0);
CString str = varName.bstrVal;
if (str.Find("Microsoft Video 1",0)!= -1)
{
pMoniker->BindToObject(0, 0, IID_IBaseFilter, (void**)&pCompress);
pMoniker->Release();
break;
}
VariantClear(&varName);
}
pClassEnum->Release();
if (pCompress==NULL)
{
MessageBox("没有发现Microsoft Video 1压缩器!","提示",MB_ICONASTERISK);
return;
}
CoCreateInstance(CLSID_CaptureGraphBuilder2,0,CLSCTX_INPROC_SERVER,
IID_ICaptureGraphBuilder2,(void**)&pBuilder);
CoCreateInstance(CLSID_FilterGraph, NULL, CLSCTX_INPROC_SERVER,
IID_IGraphBuilder, (void **)&pGraph);
pBuilder->SetFiltergraph(pGraph);
pGraph->QueryInterface(IID_IMediaControl,(void**)&pMediaControl);
pGraph->AddFilter(pSrc,L"avi");
pGraph->AddFilter(pCompress,L"com");
CoCreateInstance(CLSID_AviDest,NULL, CLSCTX_ALL,
IID_IBaseFilter,(void**)&pMux);
pGraph->AddFilter(pMux,L"Mux");
CoCreateInstance(CLSID_FileWriter, NULL, CLSCTX_ALL,
IID_IBaseFilter, (void **)&pWriter);
pGraph->AddFilter(pWriter,L"Writer");
pWriter->QueryInterface(IID_IFileSinkFilter2,(void**)&pSink);
pSink->SetFileName(str.AllocSysString(),NULL);
//压缩器输入\输出引脚
IAMVideoCompression * pAMCompress ;
IPin * pComOut,*pComIn ;
pComIn = FindPin(pCompress,PINDIR_INPUT);
pComOut = FindPin(pCompress,PINDIR_OUTPUT);
pComOut->QueryInterface(IID_IAMVideoCompression,(void**)&pAMCompress);
HRESULT hret;
hret = pAMCompress->put_KeyFrameRate(8);
IPin* pOutpin = FindPin(pSrc,PINDIR_OUTPUT); //pSrc的输出引脚
IPin* pInpin,*pOut; //pMux的输入\输出引脚
pInpin = FindPin(pMux,PINDIR_INPUT);
pOut= FindPin(pMux,PINDIR_OUTPUT);
IPin* pInpin1= FindPin(pWriter,PINDIR_INPUT);//pWriter的输入引脚
//连接引脚
HRESULT result ;
result = pGraph->ConnectDirect(pOutpin,pComIn,NULL);
result = pGraph->ConnectDirect((IPin *)pComOut,pInpin,NULL);
result = pGraph->ConnectDirect(pOut,pInpin1,NULL);
pMediaControl->Run();
pAMCompress->Release();
m_IsRecorded = TRUE;
}
(9)处理“停止录像”按钮的单击事件,停止录像操作。
void CKinescopeDlg::OnStop()
{
if (m_IsRecorded)
{
pMediaControl->Stop();
}
}






