どうもアクセスログ(カウンタ)を見ているとDirectShowの関係でCBaseVideoRendererを使いたい、と言う人がいるようです。
たぶんサンプルがあればそちらを見た方がはやいのですが・・・。まあ興味本位で久しぶりにDirectShowネタと言うことで解説を。
土日に記事を書いていないのも手伝って前後編を土日に出すことにします。
CBaseVideoRendererを使ってビデオのレンダリングを行う
DirectShowのフィルタの中でもレンダリングフィルタなので動作は簡単です。
まあ、継承元であるCBaseRendererの動きをある程度知らないと実装の仕方がわからない、と言うパターンはあると思います。
おそらく、簡単な実装例があればどう動くかわかると思います。
かなり簡易で書いている+コンパイル確認をほとんどしていないので間違えているパターンがあるかもしれません。
クラス定義
実装するクラスをCSurfaceRendererとして説明してみます。実際に描画はできませんが動作サンプルなので・・・。
//サーフェイスレンダラ class CSurfaceRenderer : public CBaseVideoRenderer{ public: CSurfaceRenderer(IUnknown *lpOwner,HRESULT *lphr); //コンストラクタ virtual ~CSurfaceRenderer(); //デストラクタ //CBaseVideoRendererから継承する関数 virtual HRESULT CheckMediaType(const CMediaType *lpMediaFormat); virtual HRESULT SetMediaType(const CMediaType *lpMediaFormat); virtual HRESULT DoRenderSample(IMediaSample *lpMediaSample); virtual void OnReceiveFirstSample(IMediaSample *lpMediaSample); private: int m_nWidth,m_nHeight; //ビデオサイズ size_t m_nLineSize; //ビデオのラインサイズ };
CBaseVideoRendererまでの継承で実装していない関数は2つあり、それが
virtual HRESULT CheckMediaType(const CMediaType *lpMediaFormat); virtual HRESULT DoRenderSample(IMediaSample *lpMediaSample);
です。これは必ず実装が必要です。
あとは最低限の動作を行うには基本的にはもう2つ必要で、それが
virtual HRESULT SetMediaType(const CMediaType *lpMediaFormat); virtual void OnReceiveFirstSample(IMediaSample *lpMediaSample);
です。CBaseVideoRendererを継承して実装が必要な物はだいたいこの4つになります。
それを踏まえると、基本的なフィルタクラスの宣言はこのような形になります。
コンストラクタ・デストラクタ
//コンストラクタ CSurfaceRenderer::CSurfaceRenderer(IUnknown *lpOwner,HRESULT *lphr) : CBaseVideoRenderer(CLSID_SurfaceRenderer,NAME("SurfaceRenderer"),lpOwner,lphr), m_nWidth(0), m_nHeight(0), m_nLineSize(0) { } //デストラクタ CSurfaceRenderer::~CSurfaceRenderer() { }
中で出てくるCLSID_SurfaceRendererは自分でGUIDを作成して定義した物を与えます。NAMEマクロは言わずもがな。
コンストラクタ中の引数の意味については後編で実際に使ってみる例で説明します。デストラクタではこれを拡張したときに解放するコードを追加するためにあります。
CheckMediaTypeの実装
HRESULT CSurfaceRenderer::CheckMediaType(const CMediaType *lpMediaFormat) { CheckPointer(lpMediaFormat,E_POINTER); //videoでもRGB準拠のフォーマットでないときはエラー if(!IsEqualGUID(*lpMediaFormat->Type(),MEDIATYPE_Video)) return VFW_E_INVALIDMEDIATYPE; //videoが描画できない画像フォーマット(描画可能なフォーマットはXRGB8888のみと仮定)はエラー if(!IsEqualGUID(*lpMediaFormat->Subtype(),MEDIASUBTYPE_RGB32)) return VFW_E_INVALIDSUBTYPE; //videoinfoを示していないときは引数エラー if(!IsEqualGUID(*lpMediaFormat->FormatType(),FORMAT_VideoInfo)) return VFW_E_NO_TYPES; //対象のメディアフォーマットは使用できる return S_OK; }
メディアタイプを指定されるのでそれが描画可能かどうかを判定するルーチンです。メジャータイプ、サブタイプ、フォーマットそれぞれで判定を行います。
CBaseVideoRendererの場合はビデオ(フレーム)を受け取るので、サブタイプはそれに準拠した物を受け取り可とするのが妥当です。
自前描画でそれ以外を受け取り可にする場合はここに条件文を追加すると良いです。
ちなみに、ビデオをデコードされたフレームがYV12など適応しない場合でも、インテリジェンス接続を使っているときはGraphBuilderが自動的に中間フィルタを挟んで色変換を行おうとするので、
面倒なときはRGBのみでも問題ありません。描画元が通常メディア(MPEG、WMVなど)なら問題ないはずです。
また、描画元がアルファを含んでいることを知っている場合はMEDIASUBTYPE_ARGB32などアルファを含むフォーマットを受け入れる、というのもありです。
描画元がアルファを含んでいて(MEDIASUBTYPE_ARGB32を出力するなど)このレンダラフィルタがアルファを受け入れられない(MEDIASUBTYPE_RGB32を受け入れる)場合も中間フィルタによる色変換が発声します。
なお、YV12=>RGB24系への変換(WebMなど)はWindows標準のフィルタではできませんのであしからず。拙作の「DirectShow Extend Filter Library」などを使わないと・・・
この関数はGraphBuilderが接続をトライするときに何度でも呼び出されます。
なので、回数に依存した項目は記述できないと思った方がいいです。この関数がS_OKを返すと次の関数が呼ばれて接続が確定します。
エラーコードを返す場合とS_FALSEを返す場合で時と場合によっては動作が違うことがあるのでそれだけは気をつけて。
SetMediaTypeの実装
HRESULT CSurfaceRenderer::SetMediaType(const CMediaType *lpMediaFormat) { const VIDEOINFOHEADER *lpVideoInfoHeader; const BITMAPINFOHEADER *lpBitmapInfoHeader; CheckPointer(lpMediaFormat,E_POINTER); lpVideoInfoHeader = (VIDEOINFOHEADER *)lpMediaFormat->Format(); lpBitmapInfoHeader = &(lpVideoInfoHeader->bmiHeader); //ビデオの必要な情報を設定する m_nWidth = lpBitmapInfoHeader->biWidth; m_nHeight = lpBitmapInfoHeader->biHeight; m_nLineSize = (((size_t)m_nWidth * (size_t)lpBitmapInfoHeader->biBitCount / 8) + 0x03) & ~0x03; //複数フォーマット対応時はlpMediaFormat->SubType()も確認すること return S_OK; }
VIDEOINFOHEADER構造体やBITMAPINFOHEADER構造体については適当なヘルプを参照してください。
必要なのは、
- この場合、lpMediaFormat->Format()はVIDEOINFOHEADERのポインタを返す(CheckMediaTypeでFormatのGUIDをFORMAT_VideoInfoとしているため。FORMAT_VideoInfo2ならVIDEOINFOHEADER2のポインタになる)
- ここで初めてビデオラインの幅・高さがわかるのでそれを保存しておくこと
- BITMAPINFOHEADERとして扱われる=プログラム内のビットマップとしての特性を持つ(biHeightが負でないときボトムアップで扱われる)ことに注意
ということです。ちなみに、これでS_OKを返さないと接続に失敗して再度CheckMediaTypeの行程に戻ります。
DoRenderSampleの実装
HRESULT CSurfaceRenderer::DoRenderSample(IMediaSample *lpMediaSample) { unsigned char *lpSrc,*lpDest; size_t nSrcPitch,nDestPitch; int i,nWidth,nHeight; long nLength; CheckPointer(lpMediaSample,E_POINTER); nWidth = m_nWidth; nHeight = m_nHeight; //サンプルの情報を取得 lpMediaSample->GetPointer(&lpSrc); nLength = lpMediaSample->GetActualDataLength(); nSrcPitch = m_nLineSize; //描画先の情報を取得 //lpDest = ・・・; nDestPitch = ・・・; lpDest = NULL; nDestPitch = 0; //これはサンプルコードなのでこの処理 //必要であれば描画先のロックを行う(この呼び出しは非同期なので) //ボトムアップの描画 if(nHeight >= 0){ for(lpSrc = lpSrc + nSrcPitch * (nHeight - 1),i = 0;i < nHeight;i++){ //描画処理 lpSrc = lpSrc - nSrcPitch; lpDest = lpDest + nDestPitch; } } //トップダウンの描画 else{ for(nHeight = -nHeight,i = 0;i < nHeight;i++){ //描画処理 lpSrc = lpSrc + nSrcPitch; lpDest = lpDest + nDestPitch; } } //必要であれば描画先ロックの解除 //描画完了 return S_OK; } [/cpp] <p>DoRenderSample関数はデコードが完了してそのサンプルを描画するタイミングになったときにFilterGraphから呼び出されます。</p> <p>なので、本体プログラムなどの動作とは無関係に呼び出されます。そのため、描画先オブジェクトとの同期処理が必要になると思われます。</p> <br> <p>メモリの取得は見たまんまのコードです。なお、この関数内でnLengthは使用されません。</p> <p>これはサウンドのサンプルを読み出すときには必須なのですが、フレームデータの場合は大きさが固定なので参照しても意味はないです。</p> <p>フレームの色配列を取得してそのまま書き写すなり何なりを描画処理として行えばOKです。</p> <br> <h2>OnReceiveFirstSampleの実装</h2> void CSurfaceRenderer::OnReceiveFirstSample(IMediaSample *lpMediaSample) { //DoRenderSampleによってサーフェイスを強制的に更新する DoRenderSample(lpMediaSample); }
OnReceiveFirstSample関数は、ポーズ処理などで最初のサンプルを受信する(この場合はポーズがかかったタイミングでビデオに描画して欲しい絵のデータが来る)ので、それを処理するためにあります。
だいたいのゲームでは、この場合とりあえずサンプルを描画して描画先を更新してしまうのがわかりやすいのでDoRenderSampleを呼び出して処理しています。
と言うわけで、簡易実装をやってみました。
後編はこれをIGraphBuilder経由で使用するときにどうするかを見てみます。