虛擬攝像頭之DirectShow虛擬攝像頭開發

                                                      by fanxiushu 2018-04-06 轉載或引用請註明原始作者。

之前CSDN上的博客,較多的文章闡述了虛擬攝像頭的開發,而且是兩種類別的。


1,比如使用老的內核流框架開發WDM虛擬攝像頭驅動,博客鏈接:
https://blog.csdn.net/fanxiushu/article/details/8496747  (虛擬攝像頭驅動原理及開發)
它使用StreamClassRegisterAdapter 註冊初始化驅動框架,屬於較老的框架模型,
系統函數StreamClassRegisterAdapter從stream.sys導出。

2,虛擬USB攝像頭驅動,博客鏈接:
https://blog.csdn.net/fanxiushu/article/details/52761644     (USB設備驅動開發之擴展(利用USB虛擬總線驅動模擬USB攝像頭))
它使用模擬USB接口的攝像頭方式,讓操作系統自動把它當成USB攝像頭,來達到模擬虛擬攝像頭的效果。
然而本質是Windows當識別到USB接口是符合UVC標準的攝像頭的時候,會自動加載它自帶的usbvideo.sys驅動,
這個驅動負責生成攝像頭的功能。 我們仔細研究usbvideo.sys,其實不難發現,它使用ks.sys 框架,這個就是較新的流內核框架模型。
ks.sys使用KsInitializeDriver 來註冊初始化驅動框架。 

今天我們介紹一個比較淺顯的在應用層就能模擬虛擬攝像頭的辦法。
利用應用層的DirectShow,模擬出虛擬攝像頭。
然而他的侷限也是挺大的,他只能適用於其他恰好也是使用DirectShow框架來獲取攝像頭數據的程序。

先來看看Windows發展史上,主要使用哪些應用層底層框架來操作攝像頭。
1, VFW(Video Of Windows)這個是Windows最先使用的框架,也是最古老的,
       好處是接口函數夠簡單,在有些兼容或者小型用層場所用用也沒什麼問題。
2,DirectShow,這個是橫跨 WINXP, WIN7,WIN8,WIN10都得到很好支持的框架,
      然而接口函數也挺複雜。
3,Media Foundation, 這個是WIN7之後,開發的新框架,目的是爲了替換DirectShow,
     從而達到更好的性能和適應流媒體發展的需求。

從發展的眼光來看,最新開發的程序,應該使用Media Foundation 框架是最好的選擇。
從現狀來看,大量的操作攝像頭的程序都是使用DirectShow框架,有小部分還在使用VFW框架。
新近開發的程序或者對老版本程序的更新中,相當數量的程序使用Media Foundation框架代替了 DirectShow。
這就造成了豐富多彩的局面。而且Media Foundation 代替DirectShow也是趨勢。
這對DirectShow虛擬攝像頭來說不是個好消息,
VFW框架和Media Foundation框架,都只能識別驅動級別的攝像頭,並不能識別DirectShow攝像頭。
更有甚者,UWP框架的程序(就是Windows10平臺新出來的,爲了 “大一統“ 目的,這個目前也是小衆,能否發展做大誰知道)。
對驅動級別的攝像頭限制更大,已經不再支持老內核流框架。
因爲我之前開發的基於StreamClassRegisterAdapter的老內核流初始化的虛擬攝像頭驅動,
雖然能被普通的Media Foundation框架的程序識別和正常使用, 但是WIN10最新的Skype和Camera程序並不能識別。
這兩款軟件都是UWP框架程序。
微軟放棄在UWP程序中支持老內核流驅動框架,應該是爲了更好的兼容其他非X86平臺,雖然目前依然非常小衆。
有時間的話,會開發基於最新流內核框架(ks.sys)的虛擬攝像頭驅動,來真正驗證skype和camera的問題。
可喜的是:基於USB的虛擬攝像頭,能在所有的情況下都運行的非常好!

其實這個 “大一統“ 對軟件開發者來說是個非常好的想法。
目前現狀有Windows,iOS/MacOS, Android三大系統鼎立,
每種系統,都有自己的一套底層函數接口和編程方式,而且區別也非常的大。
這給開發人員和開發成本帶來不少損耗。而開發的程序的功能基本都一樣。
但是也需要分別在這些系統上編譯和接入對應的底層接口。
比如做手機的公司要招聘iOS和Android至少兩個組開發人員,做的都是同樣的一款產品。
幸好WP手機GameOver了,否則還得招聘WP開發人員,這對手機開發公司和人員來說未嘗不是好消息!
當然對微軟來說是個遺憾,一個把操作系統玩得純熟的公司,居然沒法做好手機操作系統!
這是老天在給微軟開了一個巨大的玩笑,然而更多的是自己作死造成的。

如果這些系統把底層接口封裝成一個統一的接口而且能保證性能不減低,開發語言使用統一的,
比如使用C/C++做稍微底層的,java或其他語言做界面上層等,這會給開發者帶來不少好處,
然而理想總是豐滿的,現實總是骨感的。
現實中我們依然要爲了兼容各種操作系統而做重複的事。
我想如果讓開發者幹掉其他兩家讓一家獨大,一定會積極響應,因爲少了不少麻煩,當然這會造成另外一些比如壟斷等政治問題。
我想這也是HTML5和JavaScript目前流行的原因之一,
因爲他是平臺無關的:一套代碼不用重新編譯,各種平臺都能運行。

回到我們的DirectShow虛擬攝像頭上來。
我們來看看使用DirectShow操作攝像頭的流程是如何的:
首先使用CoCreateInstance創建 IGraphBuilder接口,這個接口是管理所有接口的“總管”。
之後從IGraphBuilder查詢出IMediaControl控制接口。
然後就是創建ICreateDevEnum接口,從此接口枚舉出系統所有安裝的攝像頭,枚舉的當然也包括DSHOW虛擬攝像頭。
選擇你感興趣的攝像頭,並且獲取這個攝像頭的IBaseFilter接口, 把這個接口添加到IGraphBuilder中 。
再然後選擇其他Filter,比如壓縮的Filter,Render Filter等,都加到IGraphBuilder中,
之後,查詢每個Filter的PIN接口,把匹配的PIN接口連接起來。這樣就構成了一個DSHOW的連接圖。
要使整個“”圖“” 動起來,只需要運行 IMediaControl 的Run函數,這樣攝像頭的數據就會流經每個Filter,
最終達到 Render Filter 在終端展現出來。

大致僞代碼如下:
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);
}

/////創建其他Filter,比如renderFilter,然後全部添加到IGraphBuilder中.
graphBuilder->AddFilter(deviceFilter, L"DeviceFilter"); //添加攝像頭的filter到GraphBuilder中,
graphBuilder->AddFilter(renderFilter, L"RenderFilter"); //添加其他Filter到GraphBuilder中,

//利用 IBaseFilter 接口的EnumPins函數查找合適PIN,假設上面兩個Filter的PIN分別是 devicePin, renderPin, 
對這些PIN再做些其他配置等。。。。

最後調用 IGraphBuilder的Connect函數把PIN連接起來,如下
graphBuilder->Connect(devicePin,renderPin);

這樣初始化完成, 
調用 control->Run , 即可讓其運行起來。

以上就是使用DSHOW操作攝像頭的通用流程,要讓DirectShow虛擬攝像頭能被正確識別和運行,
需要遵照上面流程,實現各種接口。

首先,要被ICreateDevEnum 接口識別到我們的虛擬攝像頭,肯定得先註冊我們的DSHOW攝像頭。
DirectShow框架已經幫我們提供了這樣的註冊函數。
我們的虛擬攝像頭需要實現在DLL動態庫中
(本來剛開始想實現在EXE中,想通過進程間COM方式,結果以失敗告終,所以認爲DirectShow框架只認識DLL方式的Filter),
這個DLL需要具備COM接口動態庫的一切基本條件,
需要有DllRegisterServer, DllUnregisterServer, DllGetClassObject,DllCanUnloadNow四個導出函數。
我們需要首先按照普通進程內COM註冊方式把DLL註冊進系統, 
然後就是我們爲了讓DSHOW框架枚舉到我們的虛擬DirectShow設備,需要做的特別處理:
創建 IFilterMapper2接口,調用接口函數RegisterFilter ,把我們的虛擬攝像頭註冊進去。
這樣ICreateDevEnum 接口就能識別到了。
大致僞代碼如下:
        IFilterMapper2* pFM = NULL;
        hr = CoCreateInstance(CLSID_FilterMapper2, NULL, CLSCTX_INPROC_SERVER, IID_IFilterMapper2, (void**)&pFM);

REGPINTYPES PinTypes = {
    &MEDIATYPE_Video,
    &MEDIASUBTYPE_NULL
};
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 註冊。
pFM->RegisterFilter(CLSID_VCamDShow, L"Fanxiushu DShow VCamera", &pMoniker, &CLSID_VideoInputDeviceCategory, NULL, &rf2);
把以上代碼添加到DllRegisterServer導出函數中,當調用DllRegisterServer註冊COM組件的時候,也就把DSHOW虛擬攝像頭註冊進去了,
同樣註銷也是類似處理。
其中 CLSID_VCamDShow 是我們自己定義的GUID,用來標誌我們的虛擬攝像頭接口。
系統也會根據這個GUID來獲取我們的接口進行後續的操作。

之後就是我們需要實現的主要內容,本來如果使用DirectShow的SDK開發庫,可以比較容易實現這部分內容。
本着一直造輪子的習慣,這次也不例外,採用完全從零開始的開發方式,
稍後提供到GITHUB和CSDN上的代碼可以看到這一點。
如果你不喜歡,或者不想去了解DirectShow的工作原理,
大可不必理會我這種比較“瘋狂”的做法,也不必下載我的這份代碼給你平添無謂的煩惱。
畢竟DirectShow的SDK代碼也是亂糟糟的挺複雜,而且遲早會被Media Foundation替代。

閱讀下面的內容需要具備一些Windows平臺的COM組件的基礎知識
(其實整個DSHOW攝像頭開發都應該具備COM組件基礎知識,否則舉步維艱)。
其實我們從零開始做一個COM組件沒有這麼可怕,甚至針對某些特殊情況,可能還比各種封裝開發包簡潔和容易理解一些。

我們的DSHOW攝像頭,除了必須實現的
DllRegisterServer, DllUnregisterServer, DllGetClassObject,DllCanUnloadNow四個導出函數外,
重頭戲就是實現我們的類對象,必須繼承IBaseFilter接口,爲了兼容順便實現IAMovieSetup 接口。
IBaseFilter接口是DSHOW FIlter的基礎導出接口,每個Filter下有一個或者多個PIN接口,因此我們還必須實現IPin接口,
光 IBaseFilter和IPin接口,一共就需要實現20,30多個接口函數,看起來有點多,其實理解了,也沒這麼麻煩。
爲了配置IPin接口,還必須實現 IAMStreamConfig 和IKsPropertySet,這兩個接口導出函數並不多,就幾個。
我們的虛擬攝像頭就只有一個Output Pin 接口,爲了簡單,在 Filter就只提供一個 IPin就可以了。
大致的數據結構描述如下所示:

class VCamDShow: 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 VCamStream : public IUnknown,
    public IPin, 
    public IQualityControl, public IAMStreamConfig, public IKsPropertySet
{
protected:

       。。。 //內部數據變量和私有函數
       VCamDShow*   m_pFilter;         // 所屬的Filter,對應上面定義的VCamDShow數據結構。

       ///// 下面是數據源相關的線程,在
StreamTreadLoop 中循環採集數據,並且通過 IMemInputPin 把數據傳輸給輸入PIN。
       
HANDLE  m_hThread; ///
HANDLE  m_event;
BOOL    m_quit;   ////
static DWORD CALLBACK thread(void* _p) {
VCamStream* p = (VCamStream*)_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(...); /////
      
};
以上看起來接口函數挺多,其實整體結構不復雜的,而且主要實現這兩個類對象基本就搞定DSHOW虛擬攝像頭了。
具體代碼可以稍後去下載我提供到GITHUB或CSDN上的源代碼。

正如上面的查詢攝像頭的僞代碼所說,
ICreateDevEnum 接口查詢到我們感興趣的攝像頭,
當綁定到這個攝像頭獲取IBaseFilter接口,調用 
IMoniker 的 BindToObject 函數,
雖然沒有BindToObject 源代碼,但可以知道大致流程:
BindToObject查找CLSID_VCamDShow(我們自定義的GUID)等信息,
調用系統函數CoCreateInstance函數創建我們的對象並且獲取IBaseFilter接口,
CoCreateInstance 系統函數通過註冊表查找我們註冊的DLL所在位置,找到並且加載DLL,同時調用DllGetClassObject獲取
類工廠,調用類工廠的CreateInstance創建我們的類,也就是上面的 VCamDShow類, 從而獲取到IBaseFilter接口。
類工廠數據結構也是挺簡單的,這裏無非就是提供 IClassFactory接口,
主要實現CreateInstance方法,在此方法new我們的VCamDShow 類對象。詳細信息可查閱提供到GITHUB和CSDN上的代碼。

找到並且獲取到IBaseFilter指針後,接下來就是調用 IGraphBuilder->AddFilter 添加到 DirectShow的Graph中,
這個時候 IBaseFilter的JoinFilterGraph方法被調用,我們在此方法中其實簡單保存IFilterGraph接口指針,
方便後面調用,同時查詢IMediaEventSink接口,用於通知事件。

在連接輸入PIN和輸出PIN之前,需要對這些PIN的MediaType類型做些配置,
就是這個PIN提供哪些類型,比如是RGB,還是YUV,YUY2等,尺寸是640X480,還是1280X720等等信息。
只有當兩個PIN的MediaType類型匹配,纔會連接成功。
這個時候 IAMStreamConfig 接口的 SetFormat ,GetFormat等函數就會被調用,用於設置具體的Meidia類型。
我們在實現
IAMStreamConfig這些函數 時候,預先配置一些當前PIN支持的Media類型,這樣當外部調用SetFormat設置Media的時候,
根據這些類型做選擇,支持的就設置成功,不支持的就返回失敗,具體可查詢我提供的源代碼。

之後就是兩個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);

       。。。。。 
}

連接成功後,整個DirectShow初始化完成,就可以開始播放,
外部調用 IMediaControlI->Run, 我們的 IBaseFilter的Run,Pause等函數就會被調用,
我們在這些函數中設置運行狀態,執行初始化等操作。

至此,一整套DirectShow攝像頭運行流程似乎都跑通了,但似乎忘記了一個重要的地方,數據源呢?
因此,我們可以在VCamStream 類裏邊創建一個線程,在這個線程裏定時循環採集數據,
並且通過 IMemInputPin接口把採集的數據傳輸給連接上來的輸入PIN。
如上面VCamStream 數據結構申明的一樣,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。
         }
          。。。。
}

到此爲止,纔算真正完成了DirectShow虛擬攝像頭驅動的核心部分。

GITHUB代碼地址:
https://github.com/fanxiushu/vcam_dshow

CSDN上代碼地址:
https://download.csdn.net/download/fanxiushu/10329777

下圖是在QQ中運行效果:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章