DirectShow摄像头和虚拟摄像头

143 篇文章 8 订阅

DShow简介

        DirectShow(简称 DShow) 是一个 Windows 平台上的流媒体框架,提供了高质量的多媒体流采集和回放功能。支持使用 WDM 驱动或早期的 VFW 驱动来进行多媒体流的采集。横跨WINXP,WIN7,WIN8,WIN10,适配性好,稳定性高。DirectShow位于应用层中。它使用一种叫Filter Graph的模型来管理整个数据流的处理过程;参与数据处理的各个功能模块叫Filter;各个Filter 在Filter Graph中按一定的顺序连接成一条"流水线"协同工作。( 可以看出TFilterGraph是个Filter的容器 )按照功能来分,Filter大致分为三类:Source Filters、Transform Filters和Rendering Filters。

Source Filters主要负责取得数据,数据源可以是文件、因特网、或者计算机里的采集卡、数字摄像机等;

Transform Fitlers主要负责数据的格式转换、传输;

Rendering Filtes主要负责数据的最终去向,我们可以将数据送给声卡、显卡进行多媒体的演示,也可以输出到文件进行存储。

下图简单展示了DShow工作流过程

 

DirectShow操作摄像头流程

1. 使用CoCreateInstance创建 IGraphBuilder接口(所有接口的“总管”)。

2. 从IGraphBuilder查询出IMediaControl控制接口。

3. 创建ICreateDevEnum接口,枚举出系统所有安装的摄像头。

4. 选择摄像头,并且获取这个摄像头的IBaseFilter接口, 把这个接口添加到IGraphBuilder中 。

5. 选择其他Filter,比如压缩的Filter,Render Filter等,加到IGraphBuilder中。

6. 定义SourceFilter,TransformFilter,RenderFilter,用RenderStream将这些链接起来。这样就构成了一个DShow的连接图。

   7.运行 IMediaControl 的Run函数,要使整个“”图“” 动起来,这样摄像头的数据就会流经每个Filter,最终到达RenderFilter并在终端显示出来。

主要代码实现如下

hr = CoCreateInstance(CLSID_FilterGraph, NULL, CLSCTX_INPROC,IID_IGraphBuilder, (void**)&graphBuilder); ///创建 IGraphBuilder接口

hr = graphBuilder->QueryInterface(IID_IMediaControl, (void**)&control); //查询IMediaControl

CComPtr<ICreateDevEnum> DevEnum; ///创建枚举摄像头设备接口

hr = CoCreateInstance(CLSID_SystemDeviceEnum, NULL, CLSCTX_INPROC_SERVER, IID_ICreateDevEnum, (void**)&DevEnum);

CComPtr<IEnumMoniker> pEM;//枚举

IMoniker* pM; //查询到的每个设备

hr = DevEnum->CreateClassEnumerator(CLSID_VideoInputDeviceCategory, &pEM, 0);

while (pEM->Next(1, &pM, &fetch) == S_OK) {

     ///开始枚举每个设备,如果是我们的虚拟DSHOW摄像头,也会被枚举到

     ........

    ///选择我们感兴趣的摄像头, 获取Filter接口,比如deviceFilter名字

    pM->BindToObject(0, 0, IID_IBaseFilter, (void**)&deviceFilter);

}

//调用RenderStream把graph里的filter链接起来

m_pCaptureGB->RenderStream(&PIN_CATEGORY_PREVIEW, &MEDIATYPE_Video,

deviceFilter, m_pSampleGrabberFilter, NULL); 调用control->Run , 即可让其运行起来

虚拟摄像头

1.虚拟摄像头注册

        在windows系统中,虚拟摄像头的注册是通过在注册表中添加摄像头信息实现的,windows规定修改注册表的程序需要在DLL动态库中实现, 这个DLL要具备COM接口动态库的基本条件,需要实现DllRegisterServer, DllUnregisterServer, DllGetClassObject,DllCanUnloadNow四个导出函数。并且在DllRegisterServer函数中实现虚拟摄像头注册,然后就可以使用regsvr32命名进行注册表写入,   其主要代码如下:

IFilterMapper2* pFM = NULL;

hr = CoCreateInstance(CLSID_FilterMapper2, NULL, CLSCTX_INPROC_SERVER, IID_IFilterMapper2, (void**)&pFM);

REGFILTERPINS VCamPins = {

    L"Pins",

    FALSE, ///

    TRUE,  /// output

    FALSE, /// can hav none

    FALSE, /// can have many

    &CLSID_NULL, // obs

    L"PIN",

    1,

    &PinTypes

};

REGFILTER2 rf2;

rf2.dwVersion = 1;

rf2.dwMerit = MERIT_DO_NOT_USE;

rf2.cPins = 1;

rf2.rgPins = &VCamPins;

 //根据上边提供的信息,调用RegisterFilter 注册。

hr = pFM->RegisterFilter(CLSID_VCamDShow, L"TAL_Camera", &pMoniker, &CLSID_VideoInputDeviceCategory, NULL, &rf2);

2.虚拟摄像头实现

        DShow虚拟摄像头,除了必须实现的 DllRegisterServer, DllUnregisterServer, DllGetClassObject,DllCanUnloadNow四个导出函数外,还需要开发虚拟摄像头类,这个类必须继承IBaseFilter接口,IBaseFilter是DShow Filter的基础导出接口,每个Filter下有一个或者多个PIN接口,因此还必须实现IPIN接口,大致数据结构如下:

class VCamDShowFilter: public IUnknown,public IBaseFilter, public IAMovieSetup

{

protected:

。。。//内部数据变量和私有函数

      VCamStream*     m_Stream; /// 这个就是我们的 IPin接口, 就只需要一个就可以了,          VCamStream数据结构下面会描述。

public:

        //IUnknow 接口

。。。。

       // IBaseFilter 接口

      STDMETHODIMP GetClassID(...);///

      STDMETHODIMP Stop() ;/// 停止, IMediaControl接口调用

      STDMETHODIMP Pause(); ///暂停,

      STDMETHODIMP Run();  ///运行

      STDMETHODIMP GetState(...); ///获取运行,暂停,停止等状态

      STDMETHODIMP GetSyncSouce(...);   

      STDMETHODIMP SetSyncSource(...);

      STDMETHODIMP  EnumPins(...);     查询当前filter 提供的IPin 接口信息, DirectShow库通过此函数获取当前Filter提供的IPin信息

      STDMETHODIMP  FindPin(...);  //

      STDMETHODIMP QueryFilterInfo(...); ///获取当前Filter信息

      STDMETHODIMP JoinFIlterGraph(...); /// 把当前filter加入到DirectShow图中,其实就是对应 IGraphBuilder->AddFilter 调用时候被调用。

      ............

};

class VCamStreamPin : public IUnknown,public IPin, public IQualityControl, public IAMStreamConfig, public IKsPropertySet

{

protected:

。。。//内部数据变量和私有函数

       VCamDShowFilter*   m_pFilter;         // 所属的Filter,对应上面定义的VCamDShow数据结构。

       / 下面是数据源相关的线程,在 StreamTreadLoop 中循环采集数据,并且通过 IMemInputPin 把数据传输给输入PIN。

       HANDLE  m_hThread; ///

       HANDLE  m_event;

       BOOL    m_quit;   

       static DWORD CALLBACK thread(void* _p)

       {

              VCamStreamPin* p = (VCamStreamPin*)_p;

              CoInitializeEx(NULL, COINIT_MULTITHREADED);

              p->StreamTreadLoop();

              CoUninitialize();

              return 0;

       }

      void StreamTreadLoop();

public:

      //IUnknow 接口

      .....

      IPin 接口

      STDMETHODIMP  Connect(....); 把 输入PIN和输出PIN连接起来,这个是主要函数,其实就是对应  

                                                                      IGraphBuilder->Connect(devicePin,renderPin);

      STDMETHODIMP  ReceiveConnection(...); ///接收连接

      STDMETHODIMP  DIsconnect(...);  ///断开与其他PIN的连接

      STDMETHODIMP  ConnectTo(...);  以下基本都是一些状态和数据信息查询

      STDMETHODIMP  ConnectionMediaType(...); ///

      STDMETHODIMP  QueryPinInfo(....);

      STDMETHODIMP  QueryDirection(...); ///

      .............

      IQualityControl

      ....

      / IAMStreamConfig...

      STDMETHODIMP SetFormat(...); ///

      STDMETHODIMP  GetFormat(...); ///

      STDMETHODIMP  GetNumberOfCapabilities(...); ///

      STDMETHODIMP  GetStreamCaps(....);

      /// IKsPropertySet

      STDMETHODIMP  Get(...); ///

      STDMETHODIMP  Set(...);

      STDMETHODIMP  QuerySupported(...); /

};

       正如上面的查询摄像头的伪代码所说, ICreateDevEnum 接口查询到我们感兴趣的摄像头,

当绑定到这个摄像头获取IBaseFilter接口,调用  IMoniker 的  BindToObject 函数,

虽然没有 BindToObject 源代码,但可以知道大致流程:

BindToObject查找CLSID_VCamDShow(我们自定义的GUID)等信息,

调用系统函数CoCreateInstance函数创建我们的对象并且获取IBaseFilter接口,

CoCreateInstance 系统函数通过注册表查找我们注册的DLL所在位置,找到并且加载DLL,同时调用DllGetClassObject获取

类工厂,调用类工厂的CreateInstance创建我们的类,也就是上面的 VCamDShowFilter类, 从而获取到IBaseFilter接口。

找到并且获取到IBaseFilter指针后,接下来就是调用 IGraphBuilder->AddFilter 添加到 DirectShow的Graph中,

这个时候 IBaseFilter的JoinFilterGraph方法被调用,我们在此方法中其实简单保存IFilterGraph接口指针。

两个PIN连接, 当外部调用 IGraphBuilder ->Connect(vcamerPin , renderPin); vcamerPin就是我们的摄像头的输出PIN。

对应IPin的Connect或者ReceiveConnection接口函数就会被调用。

在Connect函数中,我们查找各种合适的MediaType做匹配,找到后就可开始连接,

ReceiveConnection函数中根据提供的MediaType直接进行连接操作,

假设执行具体连接的函数是 HRESULT doConnect(IPin* pRecvPin, const AM_MEDIA_TYPE* mt );

因为我们是虚拟DSHOW摄像头,我们的PIN是输出PIN,是数据源。

我们必须把我们的数据源传输给连接上来的输入PIN,否则就是废品,如何实现这个核心要求呢。

其实输入PIN必须要实现IMemInputPin 接口,这个接口就是用来传递数据的。

我们在获取输入PIN的IMemInputPin接口后,调用Receive方法就能把数据传输给输入PIN了。

而Receive方法需要传递 IMediaSample 接口作为参数,IMediaSample需要通过 IMemAllocator 接口的GetBuffer方法获取。

因此我们在 doConnect函数中,除了获取IMemInputPin接口外,还必须创建IMemAllocator 接口。

doConnect大致伪代码如下:

HRESULT VCamDShow::doConnect(IPin* pRecvPin, const AM_MEDIA_TYPE* mt )

{

       .....

       pRecvPin->QueryInterface(IID_IMemInputPin, (void**)&m_pInputPin); // 从输入PIN 获取IMEMInputPIN接口,

       ...... 其他一些判断处理,比如判断MediaType是否匹配等

       m_ConnectedPin = pRecvPin;  ///保存 输入PIN指针。

       m_ConnectedPin->AddRef();

       ///创建 IMemAllocator接口

       hr = m_pInputPin->GetAllocator(&m_pAlloc);

       if(FAILED(hr)) {

              hr = CoCreateInstance(CLSID_MemoryAllocator,0,CLSCTX_INPROC_SERVER,IID_IMemAllocator,(void **)&m_pAlloc);

       }

       ///通知输入PIN,完成连接

       hr = pRecvPin->ReceiveConnection((IPin*)this, mt);

}

其连接过程如下图:

 

最后,我们要取得数据源

我们可以在VCamStreamPin 类里边创建一个线程,在这个线程里定时循环采集数据,

并且通过 IMemInputPin接口把采集的数据传输给连接上来的输入PIN。

如上面VCamStreamPin 数据结构申明的一样。StreamTreadLoop 大致代码如下:

void VCamStream::StreamTreadLoop()

{

        DWORD TMO = 33;

 ///

       while (!m_quit) {

              WaitForSingleObject(m_event, TMO);

              if (m_quit)break;

              if (m_pFilter->m_State != State_Running) { //不是运行状态

                     continue;

       }

       IMediaSample* sample = NULL;

       HRESULT hr = E_FAIL;

       if (m_pAlloc) {

            hr = m_pAlloc->GetBuffer(&sample, NULL, NULL, 0);

       }

                .......................省略其他处理

       LONG length = sample->GetSize();

      char* buffer = NULL;

      hr = sample->GetPointer((BYTE**)&buffer);

     //这个是一个回调函数,我们可以自定义这个回调函数,并且在里边填写视频帧数据。

      m_pFilter->m_callback( buffer, length ,。。。);  

      m_pInputPin->Receive(sample);  获取到的视频数据,传递给输入PIN。

}

数据帧的数据通过SourceFilter的输入pin,流到RenderFilter,实现整个摄像头逻辑。



作者:剑指霄壤
链接:https://www.jianshu.com/p/37c8de76271a
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

//指定视频采集设备的友好名字,为它创建一个Filter IBaseFilter * CTestPreviewDlg::CreateVideoDevice(const char * inFriendlyName) { return CreateHardwareFilter(CLSID_VideoInputDeviceCategory,inFriendlyName); } //根据设备的友好名字,创建一个代表该设备的Filter IBaseFilter * CTestPreviewDlg::CreateHardwareFilter(GUID inCategory,const char * inFriendlyName) { //创建一个系统枚举组件对象 ICreateDevEnum * enumHardware = NULL; HRESULT hr = CoCreateInstance(CLSID_SystemDeviceEnum,NULL,CLSCTX_ALL, IID_ICreateDevEnum,(void**)&enumHardware); if(FAILED(hr)) { return NULL; } IBaseFilter * hardwareFilter = NULL; IEnumMoniker * enumMoniker = NULL; //为指定的目录创建枚举器 hr = enumHardware->CreateClassEnumerator(inCategory,&enumMoniker,0); if(enumMoniker) { enumMoniker->Reset(); ULONG fetched = 0; IMoniker * moniker = NULL; char friendlyName[256]; //枚举得到该目录下所有的设备,逐个进行名字匹配 while(!hardwareFilter && SUCCEEDED(enumMoniker->Next(1,&moniker, &fetched)) && fetched) { if(moniker) { IPropertyBag * propertyBag = NULL; VARIANT name; friendlyName[0] = 0; hr = moniker->BindToStorage(0,0,IID_IPropertyBag,(void**)&propertyBag); //读取设备的友好名字 if(SUCCEEDED(hr)) { name.vt = VT_BSTR; hr = propertyBag->Read(L"Friendlyname",&name,NULL); } if(SUCCEEDED(hr)) { WideCharToMultiByte(CP_ACP,0,name.bstrVal,-1, friendlyName,256,NULL,NULL); //如果当前设备的友好名字与用户指定的设备名字相同, //则将当前设备标识绑定为Filter形式 if(strcmp(friendlyName,inFriendlyName) == 0) { moniker->BindToObject(0,0,IID_IBaseFilter, (void**)&hardwareFilter); } } //释放使用过的接口 if(propertyBag) { propertyBag->Release(); propertyBag = NULL; } moniker->Release(); } } enumMoniker->Release(); } enumHardware->Release(); return hardwareFilter; }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值