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工作流过程
![](data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)
DirectShow操作摄像头流程
使用CoCreateInstance创建 IGraphBuilder接口(所有接口的“总管”)。
从IGraphBuilder查询出IMediaControl控制接口。
创建ICreateDevEnum接口,枚举出系统所有安装的摄像头。
选择摄像头,并且获取这个摄像头的IBaseFilter接口, 把这个接口添加到IGraphBuilder中 。
选择其他Filter,比如压缩的Filter,Render Filter等,加到IGraphBuilder中。
定义SourceFilter,TransformFilter,RenderFilter,用RenderStream将这些链接起来。这样就构成了一个DShow的连接图。
运行 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);
hr = graphBuilder->QueryInterface(IID_IMediaControl, (void**)&control);
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) {
........
pM->BindToObject(0, 0, IID_IBaseFilter, (void**)&deviceFilter);
}
m_pCaptureGB->RenderStream(&PIN_CATEGORY_PREVIEW, &MEDIATYPE_Video, deviceFilter, m_pSampleGrabberFilter, NULL);
|
虚拟摄像头
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,
FALSE,
FALSE,
&CLSID_NULL,
L"PIN",
1,
&PinTypes
};
REGFILTER2 rf2;
rf2.dwVersion = 1;
rf2.dwMerit = MERIT_DO_NOT_USE;
rf2.cPins = 1;
rf2.rgPins = &VCamPins;
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;
public:
。。。。
STDMETHODIMP GetClassID(...);
STDMETHODIMP Stop() ;
STDMETHODIMP Pause();
STDMETHODIMP Run();
STDMETHODIMP GetState(...);
STDMETHODIMP GetSyncSouce(...);
STDMETHODIMP SetSyncSource(...);
STDMETHODIMP EnumPins(...); 查询当前filter 提供的IPin 接口信息, DirectShow库通过此函数获取当前Filter提供的IPin信息
STDMETHODIMP FindPin(...);
STDMETHODIMP QueryFilterInfo(...);
STDMETHODIMP JoinFIlterGraph(...);
............
};
class VCamStreamPin : public IUnknown,public IPin, public IQualityControl, public IAMStreamConfig, public IKsPropertySet {
protected:
。。。
VCamDShowFilter* m_pFilter;
/ 下面是数据源相关的线程,在 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: .....
IPin 接口
STDMETHODIMP Connect(....); 把 输入PIN和输出PIN连接起来,这个是主要函数,其实就是对应
IGraphBuilder->Connect(devicePin,renderPin);
STDMETHODIMP ReceiveConnection(...);
STDMETHODIMP DIsconnect(...);
STDMETHODIMP ConnectTo(...); 以下基本都是一些状态和数据信息查询
STDMETHODIMP ConnectionMediaType(...);
STDMETHODIMP QueryPinInfo(....);
STDMETHODIMP QueryDirection(...);
.............
IQualityControl
....
/ IAMStreamConfig...
STDMETHODIMP SetFormat(...);
STDMETHODIMP GetFormat(...);
STDMETHODIMP GetNumberOfCapabilities(...);
STDMETHODIMP GetStreamCaps(....);
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);
...... 其他一些判断处理,比如判断MediaType是否匹配等
m_ConnectedPin = pRecvPin;
m_ConnectedPin->AddRef();
hr = m_pInputPin->GetAllocator(&m_pAlloc);
if(FAILED(hr)) {
hr = CoCreateInstance(CLSID_MemoryAllocator,0,CLSCTX_INPROC_SERVER,IID_IMemAllocator,(void **)&m_pAlloc);
}
hr = pRecvPin->ReceiveConnection((IPin*)this, mt);
}
|
其连接过程如下图:
![](data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)
最后,我们要取得数据源
我们可以在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