程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> VC >> 關於VC++ >> 利用DirectShow開發自己的Filter

利用DirectShow開發自己的Filter

編輯:關於VC++

學習directshow已經有幾天了,下面將自己的學習心得寫下來,希望對其他的人有幫助。Filter實質是個COM組件,所以學習開發Filter之前你應該對com的知識有點了解。Com組件的實質是一個實現了純虛指針接口的C++對象。關於com的東西,這裡不多講。

一、給vc配置DirectShow的開發環境

無論開發Filter還是開發Dshow的應用程序都要配置一下開發環境的,其實就是包含一下dshow用到的頭文件和動態庫。選擇Tools菜單下面的Options。在彈出的Option對話框配置如下:

圖1 添加頭文件

選擇動態庫文件添加到工程中

圖2 添加動態庫

二、創建工程以及Filter的入口函數

創建工程:

一般情況下,創建Filter使用一個普通的Win32 DLL項目。而且,一般Filter項目不使用MFC。這時,應用程序通過CoCreateInstance函數Filter實例; Filter與應用程序在二進制級別的協作。另外一種方法,也可以在MFC的應用程序項目中創建Filter。

在vc裡新建一個工程,選擇win32動態庫,如下圖

圖3

圖4

這樣生成了一個簡單的DLL,只有一個Dllmain入口函數。下面我要給這個filter添加入口函數了。Filter是個基於DLL的com組件,所以一般的Filter都要實現下面幾個入口函數:

DllMain
DllGetClassObject
DllCanUnloadNow
DllRegisterServer
DllUnregisterServer

首先定義導出函數:

要導出這些函數有兩種方法,一是在定義函數時使用導出關鍵字_declspec(dllexport),另外一種方法是在創建DLL文件時使用模塊定義文件.Def。使用導出函數關鍵字_declspec(dllexport)創建MyDll.dll就是在 .h文件中定義定義函數如下:

extern "C" _declspec(dllexport)BOOL DllRegisterServer; 等等

為了用.def文件創建DLL,往該工程中加入一個文本文件,命名為MyDll.def,再在該文件中加入如下代碼:

LIBRARY MyFilter.ax
EXPORTS
DllMain PRIVATE
DllGetClassObject PRIVATE
DllCanUnloadNow PRIVATE
DllRegisterServer PRIVATE
DllUnregisterServer PRIVATE

其中LIBRARY語句說明該def文件是屬於相應DLL的,EXPORTS語句下列出要導出的函數名稱。我們可以在.def文件中的導出函數後加@n,如Max@1,Min@2,表示要導出的函數順序號,在進行顯式連時可以用到它。該DLL編譯成功後,打開工程中的Debug目錄,同樣也會看到MyDll.dll和MyDll.lib文件。

然後要定義這些函數的實現了,其實這些工作dshow的基類裡都已經替我們做好了,我們所要做的就拿來用就是了,最重要的三個函數的實現一般如下 

STDAPI DllRegisterServer()
{
  return AMovieDllRegisterServer2(TRUE);
}
STDAPI DllUnregisterServer()
{
  return AMovieDllRegisterServer2(FALSE);
}
extern "C" BOOL WINAPI DllEntryPoint(HINSTANCE, ULONG, LPVOID);
BOOL APIENTRY DllMain(HANDLE hModule, DWORD dwReason,  LPVOID lpReserved)
{
   return DllEntryPoint((HINSTANCE)(hModule), dwReason, lpReserved);
}

其中DllEntryPoint 是在C:\DX90SDK\Samples\C++\DirectShow\BaseClasses\dllentry.cpp定義的,如果感興趣我們可以去看看它的定義。AMovieDllRegisterServer2函數是在下面 C:\DX90SDK\Samples\C++\DirectShow\BaseClasses\dllsetup.cpp這個文件定義的,具體實現可以自己看看。

到了這裡你恐怕要做點工作,還是要設置一下你的項目環境,否則恐怕你編譯是通不過的,因為你用到了基類的一些東西,所以你要將你的dshow基類的定義和庫文件包含進來。首先包含:

#include Streams.h

其次在Project –Setting菜單下配置自己的Filter輸出的名字和連接的lib文件

圖5

其中library modules裡的包含的動態庫如下

c:\DX90SDK\Samples\C++\DirectShow\BaseClasses\debug\strmbasd.lib msvcrtd.lib quartz.lib vfw32.lib winmm.lib kernel32.lib advapi32.lib version.lib largeint.lib user32.lib gdi32.lib comctl32.lib ole32.lib olepro32.lib oleaut32.lib uuid.lib

此時你編譯一下,好像還是通不過,它提示有一個全局的用於實現COM接口的變量沒有定義,不著急,下面我們就開始實現Filter的com接口。

三、如何實現Filter 的類廠對象

我們知道一個Filter是一個com組件,所以它com特性的實現其實在其基類中實現的,比如IUnknown接口,我們直接從基類派生出我們的Filter後,它就支持com接口了,它就是一個com組件了。

所有的com組件為了實現二進制的封裝,所以連創建的接口都封裝了,因此每個com對象都有個類對象(也叫類廠對象,本身也是com對象,用來創建com組件)來創建com組件。

下面溫習一下com組件的創建過程,其中涉及到幾個函數:

當客戶端要創建一個com組件時,它通過底層的COM API函數 CoGetClassObject()使用SCM的服務,這個函數請SCM把一個指針綁定到客戶端請求的com組件的類對象上,其實在CoGetClassObject()裡它裝載了該DLL的庫,通過該dll的導出函數DllGetClassObject();DllGetClassObject根據客戶端提供的com組件CLASSID,返回該com組件類對象的指針。下面com組件的創建和SCM無關了。

客戶端利用組件的類對象(類廠對象)的IClassFactory::CreateInstance方法創建com組件。

Filter在這裡使用了一個類廠模板類來當作Filter的類廠對象。下面看看類廠在DShow是怎麼工作的。

類廠對象也是一個com組件。本來DllGetClassObject是應該由我們自己完成一個函數,在directshow基類裡已經完成了,我們不用管它了。它的功能就是來尋找這個DLL中的類廠對象,看是否有符合客戶端請求的類廠對象。

DLL裡聲明了一個全局的類廠模板數組,當DllGetClassObject請求類廠對象的時候,它就搜索這個數組,看是否有和CLSID匹配的類廠對象。當它找到一個匹配的CLSID,它就創建一個類廠對象,然後講類廠指針返回給CoGetClassObject,然後客戶端可以根據返回去的類廠指針,調用 IClassFactory::CreateInstance方法創建組件,類廠就根據數組裡定義的方法創建com組件。

factory template包含下列變量:

const WCHAR * m_Name; // Name
const CLSID * m_ClsID; // CLSID
LPFNNewCOMObject m_lpfnNew; // Function to create an instance of the component
LPFNInitRoutine m_lpfnInit; // Initialization function (optional)
const AMOVIESETUP_FILTER * m_pAMovieSetup_Filter; // Set-up information (for filters)

其中的兩個函數指針m_lpfnNew and m_lpfnInit使用下面的定義:

typedef CUnknown *(CALLBACK *LPFNNewCOMObject)(LPUNKNOWN pUnkOuter, HRESULT *phr);
typedef void (CALLBACK *LPFNInitRoutine)(BOOL bLoading, const CLSID *rclsid);

你可以參照如下的方式定義你的類廠對象:

CUnknown * WINAPI CMyFilter::CreateInstance(LPUNKNOWN pUnk, HRESULT *pHr)
{ 
  CMyFilter *pFilter = new CMyFilter(NAME("my Filter"), pUnk, pHr);
  if (pFilter== NULL)
  {
    *pHr = E_OUTOFMEMORY;
  }
  return pFilter;
}

你可以聲明自己的類廠數組如下:

CFactoryTemplate g_Templates[1] =
{
  {
   L"my filter", // Name
   &CLSID_MYFilter, // CLSID
   CMyFilter::CreateInstance, // Method to create an instance of MyComponent
   NULL, // Initialization function
   &sudInfTee // Set-up information (for filters)
  }
};
int g_cTemplates = sizeof(g_Templates) / sizeof(g_Templates[0]);

如果在這個com組件中你要支持多個filter,你可以在這個數組中繼續添加就是了。

四、如何實現自己的 Filter

在這裡就要講如何創建自己的Filter了,下面我們以寫一個CTransformFilter為例:

1、選擇一個基類,聲明自己的類。

創建filter很簡單,你只要根據自己的需要選擇不同的基類Filter派生出自己的Filter,它就已經支持com特性了。

從邏輯上考慮,在寫Filter之前,選擇一個合適的Filter基類是至關重要的。為此,你必須對幾個Filter的基類有相當的了解。在實際應用中,Filter的基類並不總是選擇CBaseFilter的。相反,因為我們絕大部分寫的都是中間的傳輸Filter(Transform Filter),所以基類選擇CTransformFilter和CTransInPlaceFilter的居多。如果我們寫的是源Filter,我們可以選擇CSource作為基類;如果是Renderer Filter,可以選擇CBaseRenderer或CBaseVideoRenderer等。

總之,選擇好Filter的基類是很重要的。當然,選擇Filter的基類也是很靈活的,沒有絕對的標准。能夠通過CTransformFilter實現的Filter當然也能從CBaseFilter一步一步實現。

下面筆者就從本人的實際經驗出發,對Filter基類的選擇提出幾點建議供大家參考。首先,你必須明確這個Filter要完成什麼樣的功能,即要對Filter項目進行需求分析。請盡量保持Filter實現的功能的單一性。如果必要的話,你可以將需求分解,由兩個(或者更多的)功能單一的Filter去實現總的功能需求。

其次,你應該明確這個Filter大致在整個Filter Graph的位置,這個Filter的輸入是什麼數據,輸出是什麼數據,有幾個輸入Pin、幾個輸出Pin等等。你可以畫出這個Filter的草圖。弄清這一點十分重要,這將直接決定你使用哪種“模型”的Filter。比如,如果Filter僅有一個輸入Pin和一個輸出Pin,而且一進一處的媒體類型相同,則一般采用CTransInPlaceFilter作為Filter的基類;如果媒體類型不一樣,則一般選擇CTransformFilter作為基類。

再者,考慮一些數據傳輸、處理的特殊性要求。比如Filter的輸入和輸出的Sample並不是一一對應的,這就一般要在輸入Pin上進行數據的緩存,而在輸出Pin上使用專門的線程進行數據處理。這種情況下,Filter的基類選擇CSource為宜(雖然這個Filter並不是源Filter)。當Filter的基類選定了之後,Pin的基類也就相應選定了。接下去,就是Filter和Pin上的代碼實現了。有一點需要注意的是,從軟件設計的角度上來說,應該將你的邏輯類代碼同Filter的代碼分開。下面,我們一起來看一下輸入Pin的實現。你需要實現基類所有的純虛函數,比如CheckMediaType等。在CheckMediaType內,你可以對媒體類型進行檢驗,看是否是你期望的那種。因為大部分Filter采用的是推模式傳輸數據,所以在輸入Pin上一般都實現了Receive方法。有的基類裡面已經實現了Receive,而在Filter類上留一個純虛函數供用戶重載進行數據處理。這種情況下一般是無需重載Receive方法的,除非基類的實現不符合你的實際要求。而如果你重載了Receive方法,一般會同時重載以下三個函數EndOfStream、BeginFlush和EndFlush。我們再來看一下輸出Pin的實現。一般情況下,你要實現基類所有的純虛函數,除了CheckMediaType進行媒體類型檢查外,一般還有DecideBufferSize以決定Sample使用內存的大小,GetMediaType提供支持的媒體類型。

最後,我們看一下Filter類的實現。首先當然也要實現基類的所有純虛函數。除此之外,Filter還要實現CreateInstance以提供COM的入口,實現NonDelegatingQueryInterface以暴露支持的接口。如果我們創建了自定義的輸入、輸出Pin,一般我們還要重載GetPinCount和GetPin兩個函數。

這裡我主要為了舉例,所以簡單寫的filter沒有Pin接口,但在我的demo裡的Filter,卻是有個out pin和一個input pin。我的Filter類的定義如下:

class CMyFilter : public CCritSec, public CBaseFilter
{
public:
  CMyFilter(TCHAR *pName,LPUNKNOWN pUnk,HRESULT *hr);
  virtual ~CMyFilter();
  static CUnknown * WINAPI CreateInstance(LPUNKNOWN pUnk, HRESULT *phr);
  CBasePin *GetPin(int n);
  int GetPinCount();
}

注:因為基類是一個純虛的基類,所以在你的filter一定要派生一個其中的純虛函數,否則編譯器會提示你的派生類也是一個純虛類,你在創建這個com組件對象的時候,純虛類是沒法創建對象的。

2、給自己的Filter生成一個CLSID

你可以用Guidgen or Uuidgen給自己的Filter生成一個128位的ID號,然後利用DEFINE_GUID宏在Filter的頭文件聲明該Filter的CLSID;

[myFilter.h]
// {1915C5C7-02AA-415f-890F-76D94C85AAF1}
DEFINE_GUID(CLSID_MYFilter,
0x1915c5c7, 0x2aa, 0x415f, 0x89, 0xf, 0x76, 0xd9, 0x4c, 0x85, 0xaa, 0xf1);

這個CLSID_MYFilter在類廠數組用到,在注冊Filter時也要用到。

3、CMyFilter類的簡單實現

這個類純粹為了演示用,所以特別簡單,你可以參考我的demo,那個filter寫的功能比較全。

CMyFilter::CMyFilter(TCHAR *pName,LPUNKNOWN pUnk,HRESULT *hr)
      :CBaseFilter(NAME("my filter"), pUnk, this, CLSID_MYFilter)
{ }
CMyFilter::~CMyFilter()
{}
// Public method that returns a new instance.
CUnknown * WINAPI CMyFilter::CreateInstance(LPUNKNOWN pUnk, HRESULT *pHr)
{ 
  CMyFilter *pFilter = new CMyFilter(NAME("my Filter"), pUnk, pHr);
  if (pFilter== NULL)
  {
    *pHr = E_OUTOFMEMORY;
  }
  return pFilter;
}
CBasePin * CMyFilter::GetPin(int n)
{
  return NULL;
}
int CMyFilter::GetPinCount()
{
   return 0;
}

這樣基本上就實現了一個filter,但是這個filter沒有與之相聯系的PIN,但是實現Filter的基本過程就時這樣了,至於邏輯上的東西,比如Filter和pin如何連接,數據流是如何流動的,你都要去看看sdk了,按照上面的步驟你就可以寫一個Filter的框架出來。

下面我們總結一下寫一個Filter至少需要那些東西。

1、Filter的實現類

在這裡就是CMyFilter類,在這個類裡你可以實現自己的邏輯上的功能,包括定義你的filter的特性,給你的filter配備pin接口等。

2 com組件的引出函數,五個全局函數:

DllMain //dll的入口函數
DllGetClassObject //獲得com組件的類廠對象
DllCanUnloadNow //com組件是否可以卸載
DllRegisterServer //注冊com組件
DllUnregisterServer //卸載com組件
其中DllGetClassObject 已經由基類完成,你自己只要完成三個函數即可,
DllMain,DllRegisterServer,DllUnregisterServer。

3、com組件的類廠對象

類廠對象是用來生成Filter對象的,用的模板類定義了一個全局的模板類對象數組,一般格式如下:

CFactoryTemplate g_Templates[1] =
{
  {
   L"my filter", // Name
   &CLSID_MYFilter, // CLSID
   CMyFilter::CreateInstance, // Method to create an instance of MyComponent
   NULL, // Initialization function
   &sudInfTee  // Set-up information (for filters)
  }
};
int g_cTemplates = sizeof(g_Templates) / sizeof(g_Templates[0]);

4、關於你自己定義的Filter以及Pin的信息

這些是一個全局的結構變量,用於描述你的Filter和你定義的pin,在注冊Filter的時候會用到,如下:

AMOVIESETUP_FILTER 描述一個Filter
AMOVIESETUP_PIN 描述pin
AMOVIESETUP_MEDIATYPE 描述數據類型

下面的代碼描述了一個Filter帶有一個output PIN:

static const WCHAR g_wszName[] = L"Some Filter";
AMOVIESETUP_MEDIATYPE sudMediaTypes[] = {
  { &MEDIATYPE_Video, &MEDIASUBTYPE_RGB24 },
  { &MEDIATYPE_Video, &MEDIASUBTYPE_RGB32 },
};
AMOVIESETUP_PIN sudOutputPin = {
  L"", // Obsolete, not used.
  FALSE, // Is this pin rendered?
  TRUE, // Is it an output pin?
  FALSE, // Can the filter create zero instances?
  FALSE, // Does the filter create multiple instances?
  &GUID_NULL, // Obsolete.
  NULL, // Obsolete.
  2, // Number of media types.
  sudMediaTypes  // Pointer to media types.
};
AMOVIESETUP_FILTER sudFilterReg = {
  &CLSID_SomeFilter, // Filter CLSID.
  g_wszName, // Filter name.
  MERIT_NORMAL, // Merit.
  1, // Number of pin types.
  &sudOutputPin  // Pointer to pin information.
};

最後如果你還是調試通不過,看看你是否包含了下面的頭文件:

#include <streams.h>
#include <initguid.h>
#include <tchar.h>
#include <stdio.h>

智慧的魚 aoosang 2004-09-01

本文配套源碼

  1. 上一頁:
  2. 下一頁:
欄目導航
Copyright © 程式師世界 All Rights Reserved