DirectShow摄像头和虚拟摄像头

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,实现整个摄像头逻辑。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,907评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,987评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,298评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,586评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,633评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,488评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,275评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,176评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,619评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,819评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,932评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,655评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,265评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,871评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,994评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,095评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,884评论 2 354

推荐阅读更多精彩内容