51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

DirectShow操作摄像头和虚拟摄像头

DShow简介 {#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操作摄像头流程 {#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. 虚拟摄像头注册 {#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); |

  1. 虚拟摄像头实现 {#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); 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 大致代码如下:

|------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 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


赞(2)
未经允许不得转载:工具盒子 » DirectShow操作摄像头和虚拟摄像头