9/25/2010

無痛地開發DirectShow應用程式 (2)

我認為開發DirectShow應用程式時,最要緊的是確立要寫的東西。雖然DirectShow提供很多功能,彈性很高,但在功能面越來越完整,程式碼越來越多時,整體的彈性越來越差。到最後程式碼就受限於提供的功能本身,只是小小的改動還可以。一但大改,就令人痛苦萬分。我記得這是作系統分析的基本功,也是每個程式設計人員應該知道的事實。

DirectShow的模型,基本上是這樣的。它提供一個容器物件,稱為Graph,在這個容器內,應用程式可以加入或移除任意多的Filter,還提供IMediaSeeking、IMediaControl及IMediaEventEx來達成整個串流的前進、後退、播放、停止、暫停、快轉及獲取進度資訊等。利用Graph及各種Builder,就可以管理這些Filter,並且將它們組織出合用的功能。要初始化一個Graph,利用以下敍述即可:

CComPtr m_gbuilder;
...
HRESULT hr=E_FAIL;

hr=m_gbuilder.CoCreateInstance(CLSID_FilterGraph); ASSERT(SUCCEEDED(hr));
GraphBuilder介面是由Graph物件所提供的最簡單的一個Builder。它提供智慧型連接功能(Intelligent Connect),並且分別實作在四個函式上:

  1. AddSourceFilter:自動添加相對應的Source Filter,其判斷基準為: (1)通訊協定的識別子(2)檔案的副檔名(3)檔案的二進位特徵。
  2. Connect:自動嚐試連接傳入的一對上下游Pin,若是無法連線,就使用
    IFilterMapper2::EnumMatchingFilters
    列舉出相容於上游的Filter,再次進行匹配。
  3. Render:自動建構出一條源自傳入的Filter到Renderer的可行路徑。
  4. RenderFile:根據檔案格式,自動加入所需Filter,並建構出一條可行路徑到Renderer。

Builder的種類有好幾種,比如DVDGraphBuilder跟CaptureGraphBuilder,如果想要省事。就應該多多使用它們。

一般來說,Graph還要決定時鐘,時鐘是用來同步整個串流輸出的依據,一般會由Filter、應用程式或Graph自己提供。在沒有時鐘的情況下,Graph會以全速運作。如果今天的目標是要做個轉檔程式的話,就應該去除Graph的時鐘。

雖然隨著應用的不同,使用的Filter也不一樣,但總體來說,除了數位電視卡的應用以外,其它的Graph基本上都大同小異。因此,根據在Graph中的分工,Filter大致可以分成:
  1. Source: 又分為一般的Reader或是Live source,一般的Reader也常跟Splitter/Demuxer作在一起,可以讀入二進位檔案並剖析。若是Live source的話,它就是指從網路上或是硬體上將資料抓回來的Filter,常見的有ksproxy.ax(由ks.sys和廠商的miniport driver構成Kernel Stream,再交由ksproxy.ax扔出來,常見於Camera)、ASF Reader等等。
  2. Splitter/Demuxer:簡單講,就是用來剖析二進位資料的Filter,常常跟Source作在一起,它們有能力將容器檔案(如avi, mkv或rmvb)的內容分成數條軌道,比如說音軌、影像軌或字幕軌。一般都是翻譯成分歧器或是解多工器。
  3. Decoder:就是解碼器啦,影音資料常常是被壓縮過的,要靠它才能還原成一幀幀的點陣影像,如H.264、VC-1、AC3等等。另外也有專門針對藏在影像內的資訊作解碼的Decoder,像Line-21解碼器就是。另外,解碼器若支援DXVA的話,就可以將資料透過DirectX的DXVA API交由GPU解碼並且直接播放。
  4. Transformer:應用程式可以自行導入一些額外的Filter來達成一些特別功能,如SampleGrabber可以程式化,作到影像強化、編修等功能、或是格式上的轉換,如Color Space Converter可以轉換YUV空間到RGB空間等等。
  5. Renderer:把影像繪置在某種平面上。一般常見的平面當然是顯示卡了,但是我們也可以自行開發能把影像印到印表機上的Renderer,只是好像沒什麼意義就是了。Orz...對了,如果作業系統是WinXP的話,推薦使用VMR7,若是在Vista以上則推薦使用EVR。至於VMR9嘛,不是很理想。尤其是遇到Aero Glass特效或是獨佔模式的程式時,問題特別多。
  6. Encoder:編碼器,這就不用再多說了。
  7. Writer/Muxer:多工器,作的事剛好跟Splitter相反。
Filter在互相連接時一般會由Graph發起,並且由上游往下游作出格式查詢、格式交涉、交涉記憶體分配器等動作。另外,在資料傳輸時會有兩種模式:推跟拉模式(Push、Pull Mode)。不過,如果只是純粹使用DirectShow,倒是不用知道這麼多。

以下是數段簡單的,能夠驅動Camera的程式碼:

bool CSecRecDlg::InitGraph(void)
{
 DECLARE_DBG_SCOPE(CSecRecDlg::InitGraph, true)

 HRESULT hr=E_FAIL;

 hr=m_gbuilder.CoCreateInstance(CLSID_FilterGraph); ASSERT(SUCCEEDED(hr));
 hr=m_cgbuilder2.CoCreateInstance(CLSID_CaptureGraphBuilder2); ASSERT(SUCCEEDED(hr));

 hr=m_gbuilder.QueryInterface(&m_mcontrol); ASSERT(SUCCEEDED(hr));
 hr=m_cgbuilder2->SetFiltergraph(m_gbuilder); ASSERT(SUCCEEDED(hr));

 AddToROT();
 return true;
}

bool CSecRecDlg::PrepareGraph(wchar_t const *sFilePath)
{
 DECLARE_DBG_SCOPE(CSecRecDlg::PrepareGraph, true)

 CComboBox *ctrl=NULL;
 CComPtr asf, rndr, sgf;
 CComPtr amsconfig;
 CComPtr wmprofile;
 CComPtr sgrabber;
 AM_MEDIA_TYPE *pSAMT=NULL, *pVAMT=NULL;
 bool bUseEVR=true;
 HRESULT hr=E_FAIL;

 ASSERT(sFilePath);
 ASSERT(m_gbuilder);
 ASSERT(m_vid);
 ASSERT(m_aud);

 {
  TCHAR buf[256]=_T("");
  int n0;
  double n1;

  GetDlgItem(IDC_DUR)->GetWindowText(buf, 255); n0=_ttoi(buf);
  (n0<1 || n0>100)?
   GetDlgItem(IDC_DUR)->SetWindowText(TMSG(_T("%d\n"), m_nDurLim)):
   m_nDurLim=n0;

  GetDlgItem(IDC_DIFF)->GetWindowText(buf, 255); n1=_tstof(buf);
  (n1<0.1 || n1>0.99)?
   GetDlgItem(IDC_DIFF)->SetWindowText(TMSG(_T("%.2lf\n"), m_nDiffLim)):
   m_nDiffLim=n1;

  m_bIsTest=((CButton *)GetDlgItem(IDC_TEST))->GetCheck()?true:false;
  m_bIsAutoOff=((CButton *)GetDlgItem(IDC_AUTOOFF))->GetCheck()?true:false;
 }

 //Load selected media type
 if(!GetFormat(pVAMT, wmprofile))
  return false;

 //Video source
 hr=m_gbuilder->AddFilter(m_vid, L"Video source"); ASSERT(SUCCEEDED(hr));
 hr=m_cgbuilder2->FindInterface(&PIN_CATEGORY_CAPTURE, NULL, m_vid, IID_IAMStreamConfig, (void **)&amsconfig); ASSERT(SUCCEEDED(hr));
 hr=amsconfig->SetFormat(pVAMT); ASSERT(SUCCEEDED(hr));

 //Audio source
 if(!m_bIsTest)
  hr=m_gbuilder->AddFilter(m_aud, L"Audio source"); ASSERT(SUCCEEDED(hr));

 //ASF writer
 if(!m_bIsTest)
 {
  CComPtr cawriter;

  hr=m_cgbuilder2->SetOutputFileName(&MEDIASUBTYPE_Asf, sFilePath, &asf, NULL); ASSERT(SUCCEEDED(hr));
  hr=asf->QueryInterface(IID_IConfigAsfWriter, (void **)&cawriter); ASSERT(SUCCEEDED(hr));
  hr=cawriter->ConfigureFilterUsingProfile(wmprofile); ASSERT(SUCCEEDED(hr));
  hr=cawriter->SetIndexMode(TRUE); ASSERT(SUCCEEDED(hr));
 }

 //Video renderer
 hr=rndr.CoCreateInstance(CLSID_EnhancedVideoRenderer);
 {
  CComPtr mfvdcontrol;

  hr=MFGetService(rndr, MR_VIDEO_RENDER_SERVICE, IID_IMFVideoDisplayControl, (void **)&mfvdcontrol);
  if(FAILED(hr))
   rndr.Release();
 }

 if(FAILED(hr))
 {
  bUseEVR=false;
  hr=rndr.CoCreateInstance(CLSID_VideoRenderer); ASSERT(SUCCEEDED(hr));
 }
 hr=m_gbuilder->AddFilter(rndr, L"Video renderer"); ASSERT(SUCCEEDED(hr));

 //Sample grabber
 hr=sgrabber.CoCreateInstance(CLSID_SampleGrabber); ASSERT(SUCCEEDED(hr));
 hr=sgrabber.QueryInterface(&sgf); ASSERT(SUCCEEDED(hr));
 hr=sgrabber->SetCallback(this, 0); ASSERT(SUCCEEDED(hr));
 hr=m_gbuilder->AddFilter(sgf, L"Sample grabber"); ASSERT(SUCCEEDED(hr));
 pSAMT=(AM_MEDIA_TYPE *)CoTaskMemAlloc(sizeof(AM_MEDIA_TYPE)); memset(pSAMT, 0, sizeof(AM_MEDIA_TYPE));
 pSAMT->majortype=MEDIATYPE_Video;
 pSAMT->subtype=MEDIASUBTYPE_RGB24;
 hr=sgrabber->SetMediaType(pSAMT); ASSERT(SUCCEEDED(hr));
 CoTaskMemFree(pSAMT);
 m_nDurCnt=0; m_his.clear();

 //Render streams
 if(!m_bIsTest)
 {
  hr=m_cgbuilder2->RenderStream(&PIN_CATEGORY_CAPTURE, NULL, m_vid, NULL, asf); ASSERT(SUCCEEDED(hr));
  hr=m_cgbuilder2->RenderStream(&PIN_CATEGORY_PREVIEW, NULL, m_vid, sgf, rndr);

  hr=m_cgbuilder2->RenderStream(NULL, NULL, m_aud, NULL, asf); ASSERT(SUCCEEDED(hr));
 }
 else
  hr=m_cgbuilder2->RenderStream(&PIN_CATEGORY_CAPTURE, NULL, m_vid, sgf, rndr); ASSERT(SUCCEEDED(hr));
 hr=sgrabber->GetConnectedMediaType(&m_GAMT); ASSERT(SUCCEEDED(hr));

 //Set video renderer
 if(bUseEVR)
 {
  CComPtr mfvdcontrol;
  CRect r;

  GetDlgItem(IDC_PREVIEW)->GetWindowRect(&r); r.MoveToXY(0, 0);
  hr=MFGetService(rndr, MR_VIDEO_RENDER_SERVICE, IID_IMFVideoDisplayControl, (void **)&mfvdcontrol); ASSERT(SUCCEEDED(hr));
  hr=mfvdcontrol->SetVideoWindow(*GetDlgItem(IDC_PREVIEW)); ASSERT(SUCCEEDED(hr));
  hr=mfvdcontrol->SetAspectRatioMode(MFVideoARMode_None); ASSERT(SUCCEEDED(hr));
  hr=mfvdcontrol->SetVideoPosition(NULL, &r); ASSERT(SUCCEEDED(hr));
 }
 else
 {
  CComPtr vwindow;
  CRect r;

  GetDlgItem(IDC_PREVIEW)->GetWindowRect(&r);
  hr=rndr.QueryInterface(&vwindow); ASSERT(SUCCEEDED(hr));
  hr=vwindow->put_Left(0); ASSERT(SUCCEEDED(hr));
  hr=vwindow->put_Top(0); ASSERT(SUCCEEDED(hr));
  hr=vwindow->put_Width(r.Width()); ASSERT(SUCCEEDED(hr));
  hr=vwindow->put_Height(r.Height()); ASSERT(SUCCEEDED(hr));
  vwindow->put_WindowStyle(0);
  hr=vwindow->put_Owner((OAHWND)(HWND)*GetDlgItem(IDC_PREVIEW)); ASSERT(SUCCEEDED(hr));
 }

 return true;
}

void CSecRecDlg::ClearGraph(void)
{
 CComPtr efilters;
 CComPtr bfilter;
 HRESULT hr=E_FAIL;

 if(!m_gbuilder)
  return;

 hr=m_mcontrol->Stop(); ASSERT(SUCCEEDED(hr));

 DeleteMediaType(&m_GAMT);

 hr=m_gbuilder->EnumFilters(&efilters); ASSERT(SUCCEEDED(hr));
 while(efilters->Next(1, &bfilter, NULL)==S_OK)
 {
  hr=m_gbuilder->RemoveFilter(bfilter); ASSERT(SUCCEEDED(hr));
  bfilter.Release();
  hr=efilters->Reset(); ASSERT(SUCCEEDED(hr));
 }
}

void CSecRecDlg::UninitGraph(void)
{
 if(!m_gbuilder)
  return;

 ClearGraph();
 RemoveFromROT();

 m_mcontrol.Release();
 m_cgbuilder2.Release();
 m_gbuilder.Release();
}

沒有留言: