在WPF项目中使用摄像头的例子
说明:WPF中没有现成的摄像头支持控件,需要自己用DShow来实现。在网上找了很多实例程序都不能满足要求,最后在别人的基础上自己完成了这部分代码。下面是详细介绍。 功能:在WPF项目中定义一个区域,在其中显示摄像头当前拍到的内容,并且可以同时把拍下的内容保存为avi文件。
在WPF应用程序中嵌入Win32控件
WPF提供的用于视频播放的控件都没有与摄像头交互的能力,所以必须用传统的DirectShow技术来提供对摄像头的支持。这就涉及到WPF应用程序和Win32控件交互的问题。这是本项目中最棘手的问题,实际也是最先完成的部分。
提供摄像头支持的类是由非托管C++代码实现的,其接口定义如下:
public class DShowConnector
{
public:
//建立DirectShow环境,完成大部分初始化工作
BOOL Setup( HWND hWnd );
//设定窗口高度和宽度
BOOL FinalizeLayout( int width, int height );
//启动摄像头,预览场景
BOOL Start();
//关闭摄像头
BOOL Stop();
//暂定屏幕回放
BOOL Pause();
//启动摄像头,预览场景并将视频保存到文件
BOOL Capture();
//停止保存到文件
BOOL StopCapture();
//清理资源
BOOL Cleanup();
};
要在WPF应用程序中使用Win32控件,最好的办法是使用C++/CLI,这是一种支持托管代码的C++版本,要使用它只需在项目或文件的编译选项中添加/clr编译开关,并且保证代码和编译选项中没有和/clr开关冲突的项即可。
由于Visual C++不支持被编译过的XAML,故为了仍能自由地使用WPF的优良特性,我在工程中添加了两个项目:项目一是标准的WPF项目,生成的是exe可执行文件,用来构造用户界面并完成几乎所有用户交互操作;项目二是一个开启了/clr编译开关的C++项目,专门提供摄像头支持,生成的是dll库文件。在解决方案的项目依赖项设置中设置前者依赖于后者,并将后者输出的dll文件添加至前者的引用项中,这样在WPF项目中便可以直接调用C++/CLI项目的接口。下面介绍此接口的实现方法。
WPF定义了专门用于交互操作的HwndHost类,在WPF中使用Win32控件的核心就是要从该类派生出用于创建包含Win32内容的对象,并重写该类的BuildWindowCore和DestryWindowCore方法。前者用于返回一个可以当作宿主的HWND,后者负责在不再需要HWND的时候清理资源。因本项目中需要的视频采集预览窗口完全没有用户交互动作,故可省略消息循环机制的实现。
public ref class WebCamClass : public HwndHost
{
private:
DShowConnector *pConnector;
HWND handle;
public:
//Constructor
WebCamClass()
{
pConnector = NULL;
}
virtual Size ArrangeOverride(Size finalSize) override
{
pConnector->FinalizeLayout(finalSize.Width,finalSize.Height);
return finalSize;
}
virtual HandleRef BuildWindowCore(HandleRef hwndParent) override
{
//创建一个“中间”窗口,作为WPF窗口的子窗口,视频窗口的父窗口
handle = CreateWindowEx(0, L"Static",
NULL,
WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN,
10, 10, // x, y
20,20, // height, width
(HWND) hwndParent.Handle.ToPointer(), // parent hwnd
0, // hmenu
0, // hinstance
0); // lparam
//实例化pConnecter指针
pConnector = new DShowConnector();
//建立与摄像头的连接
if(!pConnector->Setup(handle))
{
pConnector->Cleanup();
throw gcnew Exception("Could not connect to camera");
}
return HandleRef(this, IntPtr(handle));
}
virtual void DestroyWindowCore(HandleRef hwnd) override
{
//清理资源
pConnector->Cleanup();
delete pConnector;
}
};
另外,在WebCamClass中还定义了Start、Stop、Capture、StopCapture四个公有成员函数,它们只是DShowConnector类中Start、Pause、Capture、StopCapture函数的简单封装,在此不再赘述。
DirectShow体系结构分析
要完成上节提到的DShowConnector类,首先要对DirectShow的架构有所了解。下图是一张经典的DirectShow的运行地位和结构图:
<!--[if !vml]--><!--[endif]-->
其中虚线以下运行在R0级别的驱动程序在安装好Windows系统和摄像头驱动后便已经就绪,我们只需要按照特定的接口使用它们。DirectShow最核心的概念是Filter Graph模型,也就是图中央最大的矩形框内的部分,它负责媒体数据流的处理。参与数据处理的各个模块称为Filter,各个不同功能的Filter在Filter Graph中按一定的顺序连接成一条流水线协同工作。
Filter Graph设计
DirectShow中的Filter大致分为三类:Source Filters、Transform Filters和Rendering Filters。Source Filters主要负责从数据源取得数据,然后将数据往下传输;Transform Filters负责数据的格式转换、传输;Rendering Filters主要负责数据的最终去向。本项目中Source Filter的数据源即是摄像头,数据的最终去向在Preview状态下是渲染到屏幕窗口,在Capture状态下既要渲染到屏幕窗口又要记录到文件。
Microsoft Windows Platform SDK 6.1中提供了一个工具软件GraphEdit,利用它可以方便地构造出自己需要的Filter Graph,构造完成后可直接运行查看效果。
首先,摄像头就绪后,在GraphEdit中枚举系统里所有已经注册的Filter,可以找到168-USB PC Camera,用它作为Source Filter即可。
<!--[if !vml]--><!--[endif]-->
以Video Renderer作为Rending Filter,连接好之后形成一条完整的链路。
可以看到本项目所使用的168-USB PC Camera只有一个输出端口(更常见的情况是提供Preview和Capture两个输出端口),因此为了能够同时为屏幕输出和文件输出两条链路提供数据流,需要加入一个特殊的Filter--Smart Tee。最终设计的Filter Graph如下:
<!--[if !vml]--><!--[endif]-->
用摄像头进行视频的捕捉及回放
在GraphEdit中只能预览Filter Graph设计的效果,具体的代码实现还要手动来一步步完成。
视频捕捉程序的基本流程如下:
<!--[if !vml]--><!--[endif]-->
需定义的全局变量:
IGraphBuilder *m_pGraph;
IVideoWindow *m_pWindow;
IMediaControl *m_pControl;
IBaseFilter *m_pCapture;
其中m_pWindow和m_pControl都是通过IGraphBuilder::QueryInterface方法绑定到m_pGraph的辅助接口,前者用于设置窗口属性(FinalizeLayout函数中用它来调整窗口尺寸),后者用于控制整个Graph的工作的启动和停止(实现DShowConnector类中Start和Stop函数的关键)。
m_pCapture不同于一般的Filter用CoCreateInstance函数创建,而是在枚举系统设备的过程中使用BindToObject方法来创建。枚举函数实现如下:
HRESULT DShowConnector::EnumForCaptureSource()
{
HRESULT hr;
ICreateDevEnum *pDevEnum = NULL;
IEnumMoniker *pEnum = NULL;
IMoniker *pMoniker = NULL;
VARIANT varName;
hr = CoCreateInstance(CLSID_SystemDeviceEnum, NULL,CLSCTX_INPROC_SERVER,
IID_ICreateDevEnum,(void**)&pDevEnum);
if (SUCCEEDED(hr))
{
hr = pDevEnum->CreateClassEnumerator(CLSID_VideoInputDeviceCategory,&pEnum, 0);
hr = pEnum->Next(1, &pMoniker, NULL);
if(SUCCEEDED(hr))
{
IPropertyBag *pPropBag;
hr = pMoniker->BindToStorage(0, 0, IID_IPropertyBag, (void**)(&pPropBag));
VariantInit(&varName);
hr = pPropBag->Read(L"Description", &varName, 0);
if (FAILED(hr))
{
hr = pPropBag->Read(L"FriendlyName", &varName, 0);
}
SAFE_RELEASE(pPropBag);
hr = pMoniker->BindToObject(0, 0, IID_IBaseFilter, (void**)&m_pCapture);
}
}
SAFE_RELEASE(pMoniker);
SAFE_RELEASE(pEnum);
SAFE_RELEASE(pDevEnum);
return hr;
}
Rending Filter也不必手工创建,IGraphBuilder提供了智能连接方法IGraphBuilder::Render,只要将输出端口传递给该函数,它会自动加入必要的Filter完成剩余部分Filter Graph的构建,直至连接至Rending Filter。
IPin *pOut;
hr = GetPin(m_pCapture,PINDIR_OUTPUT,&pOut);
hr = m_pGraph->Render(pOut);
从Source Filter到Rending Filter的连接全部完成后,就可以调用IGraphBuilder::QueryInterface方法实例化m_pWindow和m_pControl:
hr = m_pGraph->QueryInterface(IID_IMediaControl, (void**)&m_pControl);
hr = m_pGraph->QueryInterface(IID_IVideoWindow, (void**)&m_pWindow);
在获取m_pWindow指针后,需要利用其接口将视频播放窗口设置为WebCamClass类中创建的“中间窗口”的子窗口。这也是Setup函数的最后一步工作。
m_pWindow->put_Owner((OAHWND)hWnd);
m_pWindow->put_WindowStyle(WS_CHILD);
其中hWnd是Setup函数的形参,在WebCamClass::BuildWindowCore函数中将其创建的“中间窗口”的句柄传递进来。
上述工作完成后,在Start函数中调用m_pControl->Run()便可开始捕捉视频。
将采集的视频写入文件
对DShowConnector::GetPin函数进行的调试验证了168-USB PC Camera生成的Filter只有一个输出端口,因此为了能对采集到的数据同时进行两路处理,程序的挑战度大大上升。幸运的是,通过学习摸索找到了一个合适的(堪称完美的)智能接口——ICaptureGraphBuilder2。利用它可以方便地创建视频采集Filter Graph,然后再将它添加到IGraphBuilder中。最重要的是,调用ICaptureGraphBuilder2:: RenderStream实现Preview链路时,不管Capture Filter是否有Preview Pin,Capture Graph Builder都能自动正确地处理。如本例中Capture Filter只有一个输出口Capture Output Pin,则Capture Graph Builder会自动插入一个Smart Tee Filter再连接。这样实际得到的就是之前在GraphEdit中设计的Graph。
要使用ICaptureGraphBuilder2,首先要在类定义中添加全局变量声明:
ICaptureGraphBuilder2 *m_pBuild;
在创建IGraphBuilder的同时也要创建ICaptureGraphBuilder2,之后要将二者相关联:
hr = CoCreateInstance(CLSID_FilterGraph, NULL, CLSCTX_INPROC_SERVER,
IID_IGraphBuilder, (void**)&m_pGraph);
hr = CoCreateInstance(CLSID_CaptureGraphBuilder2, 0, CLSCTX_INPROC_SERVER,
IID_ICaptureGraphBuilder2, (void**)&m_pBuild);
hr = m_pBuild->SetFiltergraph(m_pGraph);
之后同样是枚举系统设备,创建Source Filter,将其添加至Filter Graph。但不必再手工获取Source Filter的输出端口,而是直接将Filter的指针传递给ICaptureGraphBuilder2:: RenderStream函数:
hr = m_pBuild->RenderStream(&PIN_CATEGORY_PREVIEW, &MEDIATYPE_Video,
m_pCapture, NULL, NULL);
RenderStream的第一个参数说明这是一条Preview链路,将使用默认渲染设备作为Rending Filter。
之后的获取媒体控制接口和窗口控制接口过程与前面一节无异。到现在便用ICaptureGraphBuilder2实现了与前面完全相同的功能,但此时要增加Capture链路却变得极其简单,只需再调用一次RenderStream函数:
IBaseFilter *pMux;
hr = m_pBuild->SetOutputFileName(&MEDIASUBTYPE_Avi, Filename, &pMux, NULL);
hr = m_pBuild->RenderStream(&PIN_CATEGORY_CAPTURE, &MEDIATYPE_Video,
m_pCapture, NULL, pMux);
pMux->Release();
这里RenderStream函数的第一个参数说明这是一条Capture链路,最后一个参数是Rending Filter的指针,在设定Preview链路时,采用的是默认Rending Filter,故此参数为NULL,而Capture链路需要将数据流保存至文件,所以要手动创建一个Filter,即pMux。
RenderStream的倒数第二个参数还可设置用来对视频进行编码压缩的Filter,考虑到此项目中硬盘空间尚不成问题,故此参数暂留为NULL。
至此DShowConnector类便基本完成了。在最终的项目方案里没有用到Preview状态,故StopCapture函数也只是简单地调用了Stop函数。