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并在终端显示出来。

主要代码实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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命名进行注册表写入,   其主要代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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接口,大致数据结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
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大致伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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);

       ifFAILED(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 大致代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
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