Microsoft DirectShow フィルタ グラフからデータを取得する方法
Eric Rudolph
Microsoft Windows Digital Media Division
December 2001
要約: このドキュメントでは、独自のカスタム サンプル グラバ フィルタを使って、Microsoft DirectShow のメディア ストリームからデータを取得する方法について説明します。出発点としては、Microsoft DirectX 8.1 SDK に収録されている GrabberSample フィルタ サンプルを使用します。
はじめに
多くの開発者が Microsoft® DirectShow® に関して発する質問は、「DirectShow からデータを取り出してアプリケーションに入れるにはどうすればいいのか?」というものです。DirectShow はプラグイン アーキテクチャを使用しているため、この問題を解決する方法はいくつか存在します。単純なものから挙げていけば、以下の選択肢があります。
- Multimedia Streaming API を使用する。これらは単純な、同期的なソリューションです。
- データをキャプチャする単純な DirectShow フィルタを作成する。そのデータをアプリケーション内で処理します。
- すべての処理を行う DirectShow フィルタを作成する。このシナリオでは、複雑な処理の大部分がフィルタ内にカプセル化され、アプリケーションはほとんど作業を行いません。
このホワイト ペーパーでは第 2 のアプローチについて説明します。第 3 のアプローチは、他のアプリケーションからフィルタを利用できるようになるので、3 つの中では最も汎用性が高くなりますが、それと同時に最も難しいものでもあります。
注
Multimedia Streaming API の詳細については、DirectShow SDK ドキュメントを参照してください。
このドキュメントには以下のトピックが含まれています。
- 知っておく必要がある DirectShow の基本事項
- サンプル グラバ フィルタの作成
- サンプル アプリケーション コード
- 接続時間の短縮
- フィルタにバッファへの格納を行わせる方法
- 形式の変更への対応
- DirectShow サンプル グラバの制限
知っておく必要がある DirectShow の基本事項
まず、DirectShow とその動作について簡単に要約しておきましょう。DirectShow は、アプリケーションがフィルタと呼ばれる一連の相互に接続されたオブジェクトを通してデータをストリーミングできるようにするストリーミング アーキテクチャです。DirectShow フィルタの集まりはフィルタ グラフと呼ばれます。
DirectShow フィルタは、ソース フィルタ・変換フィルタ・レンダラの主に 3 つのカテゴリに分類されます。ソース フィルタはデータを作成し、これを次のフィルタに送り込みます。変換フィルタはデータを受け取って転送します。ときには複数のスレッド上で処理を行います。レンダラはデータの受け取りのみを行います。
すべての DirectShow フィルタは、ピンと呼ばれる接続ポイントを少なくとも 1 つは持っています。フィルタはピンを介して他のフィルタに接続します。メディア データは、ピン接続を通って、フィルタ間で移動します。
グラフの状態
フィルタ グラフは、停止・ポーズ・実行中・遷移の 4 つの状態を取ります。遷移状態では、グラフは 1 つの状態から別の状態に変化しつつありますが、DirectShow のマルチスレッド性のために、まだその変化を完了していません。
ほとんどのフィルタでは、ポーズと実行中の状態は同じものです。ソース フィルタは新しいデータを生成し、変換フィルタは処理のために新しいデータを受け付けます。この規則の例外は、ライブ キャプチャ フィルタとレンダラ フィルタです。ライブ キャプチャ フィルタは実行中にのみデータを送信し、ポーズ中にはデータを送信しません。レンダラ フィルタはポーズ中はデータのレンダリングを停止し、新しいデータを受け付けません。
停止したフィルタは、データの処理や新しいデータの受け付けは行いません。ワーカー スレッドをシャットダウンし、使用中の他のリソースをすべて解放します。
フィルタは、フィルタ グラフが 1 つの状態から別の状態に変化するときに、定義済みのプロトコルに従わなくてはなりません。詳細については、DirectShow SDK ドキュメントのトピック「フィルタ開発者が使用するデータ フロー」を参照してください。
マルチスレッディング
DirectShow を使うためには、マルチスレッド プログラミングに関してある程度の知識が必要です。単純な DirectShow アプリケーションでは、アプリケーション スレッドからは独立したスレッド上でデータがグラフを移動するということを知っているだけで十分です。しかし、フィルタを作成する場合には、スレッド・クリティカル セクション・イベントなどの概念を扱う必要があります。これらの問題を無視すると、フィルタは不正な動作をしたり、さらに悪い場合にはアプリケーション内でデッドロックを生じさせたりする可能性があります。これらの問題を理解しておけば、フィルタの作成ははるかに簡単になります。
**注 **
Multimedia Streaming API は、多くのマルチスレッディング問題から開発者を隔離してくれます。これは、この API を使う利点の1つです。
以下に、DirectShow フィルタにおけるスレッディングに関する一般的なガイドラインを示します。
- **ソース フィルタ
**ほとんどのソース フィルタは、フィルタ上の個々の出力ピンごとに独立したスレッドを作成します。スレッドはループに入り、そこでバッファにデータを格納し、次のフィルタにデータを配信します。 - **変換フィルタ
**ほとんどの変換フィルタはスレッドを作成しません。これらは、上流のフィルタが使用しているのと同じスレッドを使ってデータを処理します。一部の変換フィルタは、個々の出力ピンごとに独立したスレッドを作成します。これは、特に必要がない場合以外には勧められません。たとえば、インターリーブされたデータを別々のストリームに分割するフィルタは、通常は独立したスレッドを作成し、一方のストリームの処理を待っている間に、もう一方のストリームがブロックされないようにします。 - **レンダラ フィルタ
**一般に、レンダラ フィルタはスレッドを作成しません。
フィルタは、ポーズまたは実行を行うときに必要なスレッドを作成し、停止したときにスレッドをクローズします。
ピン接続のネゴシエーション
2 つのフィルタが接続されるとき、ピンはどのようなタイプの接続を確立するのかを決めるネゴシエーションを行います。具体的な内容は、それに関与するフィルタに依存しますが、一般にピンは以下の事柄を決定しなくてはなりません。
- 出力するデータ タイプ(オーディオやビデオなど)と、データの形式。
- 使用するバッファのサイズ、作成するバッファの数、および必要なメモリ アラインメント。
- どちらのフィルタがバッファを割り当てるか。
このホワイト ペーパーでは、これらの問題のいくつかについて解説しています。詳細については、DirectShow のドキュメントを参照してください。
サンプル グラバ フィルタの作成
データを DirectShow フィルタ グラフから取得するには、カスタムの「サンプル グラバ」フィルタを作成するのが簡単です。監視したいデータのストリームにフィルタを接続し、フィルタ グラフを実行します。データがフィルタを通る過程で、アプリケーションはデータを自由に操作することができます。
サンプル グラバ フィルタの使用例としては、以下のようなものが考えられます。
- ファイル全体をデコードしてメモリ バッファに格納する。
- ビデオ ファイルからポスター フレームを取得する。
- ライブなビデオ ストリームから静止画像をキャプチャする。
- ビデオ ファイルをデコードして、Microsoft DirectDraw® バッファに格納する。
DirectShow 8.0 SDK にはサンプル グラバ フィルタが含まれていましたが、ソース コードは提供していませんでした。DirectShow 8.1 SDK には、改訂バージョンのサンプル グラバ フィルタのソース コードが、GrabberSample Filter Sample という名前の SDK サンプルとして含まれています。
はじめの一歩
最初に行う選択は、変換フィルタとレンダラのどちらを作成するかということです。変換フィルタは他の下流のフィルタに接続することができるため、データのレンダリング、ファイルへの書き込み、およびその他の操作を実行することができます。ただし、変換フィルタは下流に接続を必要とするため、正しく実装しようとすると、複雑な作業が必要になります。レンダラ フィルタは 1 つの入力接続しか必要としません。
このホワイト ペーパーでは変換フィルタの作成方法を説明しますが、アイデアの多くはレンダラ フィルタにも適用できます。
このホワイト ペーパーで取り上げるフィルタは「トランス イン プレース」フィルタです。つまり、受け取ったデータを新しいバッファにコピーするのではなく、受信バッファの中で直接にデータを変更します。このフィルタは DirectShow の基底クラス ライブラリを使用します。
トランス イン プレース フィルタを作成するには、以下のステップを実行します。
- CTransInPlaceFilter クラスから派生した新しいクラスを作成します。
- オプションとして、フィルタを自己登録を行う本物の COM オブジェクトにすることができます。このためには、CLSID 定義を含んだ IDL ファイルまたはヘッダー ファイル、DLL 関数をエクスポートする DEF ファイル、およびフィルタを作成するための静的クラス メソッドが必要となります。詳細については、DirectShow SDK ドキュメントのトピック「DLL の作成方法」と「フィルタの登録」を参照してください。
- CTransInPlaceFilter の 2 つの純粋仮想メソッド、Transform メソッドとCheckInputType メソッドをオーバーライドします。
CTransInPlaceFilter クラスは、ピン接続とバッファのネゴシエーション・必要に応じたピンの再接続・入力ピンから出力ピンへのデータの移動・マルチスレッドのサポートなどのさまざまなタスクを自動的に処理します。基底クラスの C++ コードを読むと、DirectShow フィルタの詳細を学ぶことができます。より複雑な処理を行いたい場合には、CTransInPlaceFilter の他のメソッドもオーバーライドしなくてはならないことがあります。
CheckInputType メソッドのオーバーライド
フィルタの CheckInputType メソッドは、どのメディア タイプを受け付け、どのメディア タイプを拒否するかを決定します。ピン接続プロセスの際に、上流のピンはさまざまなメディア タイプを提案します。フィルタは任意のメディア タイプを受け付けまたは拒否することができます。DirectShow は、フィルタ グラフを作成するときに、レジストリにリストされているフィルタを自動的に探し、接続を確立しようとします。たとえば、フィルタが未圧縮のビデオのみを受け付けており、アプリケーションがこれをAVIファイル ソースに接続しようとしたとき、DirectShow は適切なビデオ デコンプレッサを挿入します。
形式タイプ
フィルタがサブタイプ MEDIASUBTYPE_RGB24 の MEDIATYPE_Video のみを受け付ける場合、必ずしも FORMAT_VideoInfo の形式タイプには接続しません。Format_VideoInfo2 や FORMAT_DvInfo を含む他のビデオ形式タイプがいくつか存在します。開発者は、フィルタがどの形式を処理するのかを決定し、それに応じて各種の形式タイプを受け付けまたは拒否しなくてはなりません。
形式ブロックとインバートされた DIB
未圧縮のビデオ タイプの場合、上流フィルタは左上隅が原点のデバイス独立ビットマップ (DIB) を送ることがあります。これは、形式ブロックの BITMAPINFOHEADER 構造体の biHeight メンバを使って、接続時に指定します。このため、フィルタが特定の DIB の向き (左下隅が原点か左上隅が原点か) を必要とする場合には、biHeight メンバをチェックし、フィルタが処理しないタイプはすべて拒否するようにしてください。
多くのデコンプレッサはどちらの向きでもデコードを行うことができ、両方のタイプを提案します。向きをチェックせずにメディア タイプを受け付けると、ピンはデコンプレッサが先に提案した向きを使用します。
アプリケーションからのメディア タイプの設定
サンプル グラバ フィルタでは、フィルタがどのメディア タイプを受け付けるのかを、アプリケーションから制御できるようにするべきでしょう。このアプローチでは、アプリケーションは以下のステップを実行します。
- アプリケーションは、フィルタのカスタム メソッドを呼び出して、希望のデータ タイプを指定します。これは具体的な形式であってもよく、受け付け可能な形式の範囲を指定する一般的な記述であってもかまいません (たとえば、任意のサイズの 24 ビット RGB ビデオなど)。
- アプリケーションはサンプル グラバをグラフ内の他のフィルタに接続します。ピンのネゴシエーションの際に、CheckInput メソッドは提案されたメディア タイプを、アプリケーションがステップ 1 で指定したタイプとマッチングしようと試みます。
- アプリケーションは別のカスタム メソッドを呼び出して、接続に使用する実際のメディア タイプを取得します。
たとえば、アプリケーションがステップ 1 で 24 ビット RGB を指定したとします。ステップ 2 では、ピンは 320 × 240 ピクセルなどの具体的なビデオ サイズを使って接続します。ステップ 3 で、アプリケーションはメディア タイプを取得してビデオ サイズを決定します。この情報がなければ、アプリケーションは受信したデータを解釈することができません。
フィルタには、この 2 つのメソッドを含んでいるカスタム COM インターフェイスを定義する必要があります。DirectShow サンプル グラバ フィルタは ISampleGrabber インターフェイスを使用しています。独自のフィルタを作成するときには、これを参考にしてください。
Transform メソッドのオーバーライド
CTransInPlaceFilter コンストラクタ メソッドのパラメータの 1 つは、フィルタが、自分が受信したデータを変更するかどうかを指定するフラグです。値 false
を渡した場合には、データをいかなる形でも変更してはなりません。それ以外の場合は、Transform メソッドの中でデータを自由に変更することができます。
Transform メソッドは、メディア サンプルの IMediaSample インターフェイスへのポインタを受け取ります。このメソッドは CTransInPlaceFilter::Receive メソッドから呼び出されます。Transform メソッドが返ったら、Receive メソッドは出力ピンの CBaseOutputPin::Deliver を呼び出して、サンプルを配信します。
Transform メソッドが S_FALSE を返した場合、基底クラスは品質管理の変更を通知します。ただし、このケースでは、Receive メソッドは (S_FALSE ではなく) S_OK を返し、上流フィルタは送り続けます。Transform メソッドがエラー コードを返した場合、基底クラスはフィルタ グラフに対してストリーミング エラーを通知し、フィルタ グラフは停止します。真のストリーミング エラーが起こったのではない限り、エラー コードを返すべきではありません。単にストリームを停止したい場合には、Receive メソッドをオーバーライドして、Receive から S_FALSE を返すようにしてください。
マルチスレッディングの処理
アプリケーションは、フィルタにデータを配信したスレッドとは別のスレッド上でつねに実行されます。アプリケーション内で同期的にデータを取得するときには、このマルチスレッディングを考慮に入れる必要があります。以下に、いくつかの一般的なシナリオでのヒントを示します。
ファイル全体のデコード
圧縮済みファイル全体をデコードして、個々の未圧縮データのブロックを順番に取得したい場合には、スレッディングのことを気にする必要はないでしょう。アプリケーション内でグローバル バッファを作成し、そのバッファに書き込みを行うように Transform メソッドを作成します。別の方法として、サンプルを受信するたびに、Transform にコールバック メソッドを呼び出させることもできます。このコールバック メソッドの中で、グローバル バッファへの書き込みを行います。アプリケーション内で、コールバックをセットアップし、グラフを停止するまで実行すれば、作業は終わりです。
ファイルのセクションのデコード
このシナリオは、ファイル全体のデコードに似ていますが、アプリケーションは IMediaSeeking::SetPositions メソッドを使って開始位置と停止位置を設定する必要があります。別の方法として、ソース フィルタにデータ配信の停止を指示するために、Receive メソッドから S_FALSE を返すことも可能です。
ファイルのランダムなセクションのデコード
ファイルの一部をデコードした後に、別の位置にシークして再びデコードしたい場合のプロセスは、より複雑になります。フィルタ グラフをシークするか、1 つのグラフ状態から別のグラフ状態に変更したときには、アプリケーションはグラフ状態が安定するまで待たなくてはなりません。
グラフをシークした場合(IMediaSeeking または IMediaPosition を使用)、呼び出しはレンダラ フィルタから始まり、ソース フィルタに達するまで上流に向けて同期的に移動します。ソース フィルタはデータのプッシュを非同期的に停止し、下流に向けてフラッシュを送信し、新しい位置にシークした後に、データの送信を再開します。
単一のデータ フレームを取得するには、Receive メソッドをオーバーライドして S_FALSE を返すようにします。アプリケーションの中では、グラフをポーズさせ、目的の時刻までシークします。ソースはこれに応答してシークを行い、1 つのサンプルを下流に送信します。
アプリケーションにサンプルを非同期的ではなく同期的に処理させたい場合には、イベントを使用します。Transform メソッドでイベントを設定し、アプリケーション内でイベントを待ちます。たとえば、次のようなループが使えます。
while (終了するまで)
フィルタ グラフをシークする。
イベントが通知されるまで待つ。
この例では、データの処理を、Transform メソッドの中かコールバック メソッドの中だけで行うと仮定しています。データをアプリケーション ループの中で処理したい場合には、第 2 のイベントが必要となります。
while (終了するまで)
フィルタ グラフをシークする。
イベント 1 が通知されるまで待つ。
データを処理する。
イベント 2 を通知する。
フィルタの Transform メソッドは、次のように作成します。
Transform:
イベント 1 を通知する。
イベント 2 を待つ。
S_FALSE を返す。
第二のイベントがなければ、Transform メソッドは別のスレッド上で実行されているので、ただちに返ります。するとアプリケーションが古いデータを処理している間も、他のフィルタがサンプルに新しいデータを書き込めるようになります。
注
もう 1 つのオプションとして、Transform メソッドの中でサンプルの AddRef を呼び出した後に、アプリケーションからサンプルの Release を呼び出すという方法があります。サンプルの参照カウントを保持することで、これが「フリー」リストに返されるのを防ぎます。ただし、この方法では、下流のサンプルが変更されるのを防ぐことはできません。参照カウントと IMediaSample インターフェイスの詳細については、SDK ドキュメントの「メディア サンプル」と「アロケータ」を参照してください。
サンプル アプリケーション コード
次のコードは、サンプル グラバ フィルタを使用するコンソール アプリケーションです。
#include "stdafx.h"
#include <atlbase.h>
#include <streams.h>
#include // Nullレンダラのため
#include // GetOutPin、GetInPinのため
#include // GetOutPin、GetInPinのため
#include "SampleGrabber.h"
int test(int argc, char* argv[]);
int main(int argc, char* argv[])
{
CoInitialize( NULL );
int i = test( argc, argv );
CoUninitialize();
return i;
}
HANDLE gWaitEvent = NULL;
HRESULT Callback(IMediaSample* pSample, REFERENCE_TIME* StartTime,
REFERENCE_TIME* StopTime)
{
// 注: フィルタの GetConnectedMediaType を呼び出して形式を取得するまで、
// このサンプルに対しては何の処理もできない
DbgLog((LOG_TRACE, 0, "Callback with sample %lx for time %ld",
pSample, long(*StartTime / 10000)));
SetEvent(gWaitEvent);
return S_FALSE; // ソースに対し、サンプルの配信を停止するよう指示する
}
int test( int argc, char * argv[] )
{
// サンプルを取得したときに発行されるイベントを作成する
gWaitEvent = CreateEvent( NULL, FALSE, FALSE, NULL );
// サンプル グラバはレジストリ内にないので、"new"を使って作成する
HRESULT hr = S_OK;
CSampleGrabber *pGrab = new CSampleGrabber(NULL, &hr, FALSE);
pGrab->AddRef();
// フィルタのコールバック関数を設定する
pGrab->SetCallback(&Callback);
// 部分的に指定されたメディア タイプをセットアップする
CMediaType mt;
mt.SetType(&MEDIATYPE_Video);
mt.SetSubtype(&MEDIASUBTYPE_RGB24);
hr = pGrab->SetAcceptedMediaType(&mt);
// フィルタ グラフ マネージャを作成する
CComPtr pGraph;
hr = pGraph.CoCreateInstance( CLSID_FilterGraph );
// 他の有用なインターフェイスを照会する
CComQIPtr<IGraphBuilder, &IID;_IGraphBuilder> pBuilder(pGraph);
CComQIPtr<IMediaSeeking, &IID;_IMediaSeeking> pSeeking(pGraph);
CComQIPtr<IMediaControl, &IID;_IMediaControl> pControl(pGraph);
CComQIPtr<IMediaFilter, &IID;_IMediaFilter> pMediaFilter(pGraph);
CComQIPtr<IMediaEvent, &IID;_IMediaEvent> pEvent(pGraph);
// グラフにソース フィルタを追加する
CComPtr pSource;
hr = pBuilder->AddSourceFilter(L"C:\\test.avi", L"Source", &pSource);
// グラフにサンプル グラバを追加する
hr = pBuilder->AddFilter(pGrab, L"Grabber");
// 入出力ピンを発見し、接続する
IPin *pSourceOut = GetOutPin(pSource, 0);
IPin *pGrabIn = GetInPin(pGrab, 0);
hr = pBuilder->Connect(pSourceOut, pGrabIn);
// Null Renderer フィルタを作成し、これをグラフに追加する
CComPtr pNull;
hr = pNull.CoCreateInstance(CLSID_NullRenderer);
hr = pBuilder->AddFilter(pNull, L"Renderer");
// 他の入出力ピンを取得し、接続する
IPin *pGrabOut = GetOutPin(pGrab, 0);
IPin *pNullIn = GetInPin(pNull, 0);
hr = pBuilder->Connect(pGrabOut, pNullIn);
// グラフをデバッグ出力に表示する
DumpGraph(pGraph, 0);
// 注: グラフは作成されたが、形式はまだ不明である。
// 形式を知るためには、GetConnectedMediaType を呼び出す。
// この例では、単にデバッグ ウィンドウに適当な情報を書き出す。
REFERENCE_TIME Duration = 0;
hr = pSeeking->GetDuration(&Duration);
BOOL Paused = FALSE;
long t1 = timeGetTime();
for(int i = 0 ; i < 100 ; i++)
{
// グラフをシークする
REFERENCE_TIME Seek = Duration * i / 100;
hr = pSeeking->SetPositions(&Seek, AM_SEEKING_AbsolutePositioning,
NULL, AM_SEEKING_NoPositioning );
// グラフがまだポーズしていなければ、ポーズする
if( !Paused )
{
hr = pControl->Pause();
ASSERT(!FAILED(hr));
Paused = TRUE;
}
// ソースがサンプルを配信するのを待つ。コールバックは S_FALSE を返すので、
// ソースは一回のシークにつき 1 つのサンプルを送付する。
WaitForSingleObject(gWaitEvent, INFINITE);
}
long t2 = timeGetTime();
DbgLog((LOG_TRACE, 0, "Frames per second = %ld", i * 1000/(t2 - t1)));
pGrab->Release();
return 0;
}
接続時間の短縮
サンプル グラバをオーディオ タイプを受け付けるようにセットアップし、ファイル ソースを入出力ピンに接続すると、接続プロセスは機能しますが(ファイルにオーディオ ストリームが含まれていた場合)、これには長い時間がかかります。これは、DirectShow Intelligent Connect プロセスが、フィルタがどのメディア タイプを受け付けるかを推測することができないため、すべてを試みることが原因です。このプロセスはシステム上のすべてのビデオおよびオーディオ デコーダをロードし、それぞれをグラフ内のファイル ソースとフィルタの間に挿入しようと試みます。まず先にビデオ デコーダを試すので、オーディオ デコーダに達するまでにはしばらく時間がかかります。
CBasePin::GetMediaType メソッドの中で、フィルタの優先メディア タイプを指定することにより、この問題を解消できます。これにより、DirectShow 接続ロジックに、どの codec を試みるのかを示すヒントを与えることができます。
フィルタ コンストラクタ メソッドのオーバーライド
CTransInPlaceFilter クラスは、CTransInPlaceInputPin および CTransInPlaceOutputPin クラスを使用して、フィルタの入力ピンと出力ピンを自動的に作成します。GetMediaType メソッドをオーバーライドするためには、フィルタを変更する必要があります。まず、CTransInPlaceInputPin から派生させた CSampleGrabberInPin という名前の新しいクラスを定義します。次に、CSampleGrabber コンストラクタ メソッドの中で、CSampleGrabberInPin の新しいインスタンスを作成し、これをフィルタの m_pInput メンバ変数に代入します。
EnumMediaType メソッドのオーバーライド
上流フィルタは、サンプル グラバに接続するときに、サンプル グラバの入力ピンの IPin::EnumMediaTypes を呼び出します。通常、この時点では、出力ピンは依然として非接続状態にあります。その場合、CTransInPlaceInputPin クラス (CBasePin クラスの EnumMediaTypes をオーバーライド) はエラー コード VFW_E_NOT_CONNECTED を返します。その結果、GetMediaType は決して呼び出されません。これを回避するために、EnumMediaTypes をオーバーライドします。出力ピンが接続されていなければ、CBasePin メソッドのように列挙オブジェクトを作成します。それ以外の場合は、CTransInPlaceInputPin バージョンのメソッドを呼び出します。
GetMediaType メソッドのオーバーライド
GetMediaType メソッドで、メディア タイプ パラメータのメジャー タイプのみにデータを格納します。それ以外のものにデータを格納すると、一部のサードパーティ codec はクラッシュします。
ヘッダー ファイルに次のコードを追加します。
class CSampleGrabberInPin : public CTransInPlaceInputPin
{
public:
CSampleGrabberInPin(CTransInPlaceFilter *pFilter, HRESULT *pHr)
: CTransInPlaceInputPin(NAME("SGInputPin"), pFilter, pHr, L"Input")
{
}
HRESULT GetMediaType( int iPosition, CMediaType *pMediaType );
STDMETHODIMP EnumMediaTypes( IEnumMediaTypes **ppEnum );
};
ソース ファイルに次のコードを追加します。
CSampleGrabber::CSampleGrabber(...)
/* 省略 */
{
m_pInput = (CTransInPlaceInputPin*)new CSampleGrabberInPin(this, phr);
if(!m_pInput)
{
*phr = E_OUTOFMEMORY;
}
}
HRESULT CSampleGrabberInPin::GetMediaType(int iPosition,
CMediaType *pMediaType)
{
if (iPosition < 0) {
return E_INVALIDARG;
}
if (iPosition > 0) {
return VFW_S_NO_MORE_ITEMS;
}
*pMediaType = CMediaType();
pMediaType->SetType( ((CSampleGrabber*)m_pFilter)->m_mtAccept.Type() );
return S_OK;
}
STDMETHODIMP CSampleGrabberInPin::EnumMediaTypes(IEnumMediaTypes **ppEnum)
{
CheckPointer(ppEnum,E_POINTER);
ValidateReadWritePtr(ppEnum,sizeof(IEnumMediaTypes *));
// 出力ピンが接続されていなければ、アプリケーションが
// 設定したメディア タイプを提供する
if( !((CSampleGrabber*)m_pTIPFilter)->OutputPin()->IsConnected() )
{
// 新しい参照カウントを行う列挙子を作成する
*ppEnum = new CEnumMediaTypes(this, NULL);
return (*ppEnum) ? NOERROR : E_OUTOFMEMORY;
}
// 出力ピンが接続されている場合には、完全なメディア タイプを提供する
return ((CSampleGrabber*)m_pTIPFilter)->
OutputPin()->GetConnected()->EnumMediaTypes(ppEnum);
}
フィルタにバッファへの配信を行わせる方法
一部のケースでは、サンプル グラバに対し、アプリケーションにが選択したバッファにサンプルを送付することができます。この動作を理解するためには、DirectShow で使用されているアロケータ メカニズムを理解し、また Intelligent Connect の動作に関してもある程度の知識を得ておく必要があります。
以下に、開発者が行わなくてはならない作業の要約を示します。
- CMemAllocator クラスから派生した、CSampleGrabberAllocator という名前の新しいクラスを定義します。
- GetAllocatorRequirements・Alloc・ReallyFree メソッドをオーバーライドして、アプリケーションのメモリ バッファを指すメモリ アロケータを提供させます。
- 入力ピンの NotifyAllocator メソッドをオーバーライドし、カスタム アロケータ以外のすべてのアロケータを拒否します。
- GetAllocator メソッドをオーバーライドして、カスタム アロケータを返すようにします。
- フィルタ内に、アプリケーションが読み取り専用モードを指定したかどうかを判断する protected メソッドを用意します。
- フィルタ内に、アプリケーションが配信バッファを指定するために使用する public メソッドを用意します。
アロケータ
2 つのピンが接続するとき、これらのピンはサンプルを下流に渡すためのメモリ バッファ トランスポートに関して合意しなくてはなりません。これはアロケータと呼ばれます。接続されたピンの個々のペアは 1 つのアロケータを使用します。変換フィルタが、サンプルを入力ピンから出力ピンにコピーするとき、変換フィルタは 2 つの異なるアロケータの間でコピーを行っていることになります。一方、フィルタがイン プレース変換を実行するときには、両方のピンで同じアロケータを使用しています。すべてのピン接続が同じアロケータを使用していれば、サンプルがメモリ コピーなしにソース フィルタからレンダラまで移動することも不可能ではありません。
アロケータは以下のプロパティを持ちます。
- **Prefix
**バッファの先頭に割り当てが必要なスペア バイトの数。 - **Alignment
**バッファのアラインメント。 - **Buffer count
**アロケータが作成するバッファの数。これにより、上流ピンは自分のスレッド上で複数のメモリ バッファに対して配信を行うことができ、下流ピンは入力ピンをブロックすることなくバッファをホールドすることができます。ここで示すカスタム アロケータでは、バッファ カウントは 1 でなくてはなりません。 - **Size
**各バッファの最大サイズ。
トランス イン プレース フィルタでは、出力ピンはほぼ必ず入力ピンと同じアロケータを使用するので、出力ピンのために追加のコードを用意する必要はありません。
ヘッダー ファイルに次のコードを追加します。
//----------------------------------------------------------------------
// カスタム アロケータ クラス。
// このオブジェクトはバッファ位置を参照する CMediaSamples を割り当てる。
//----------------------------------------------------------------------
class CSampleGrabberAllocator : public CMemAllocator
{
protected:
CSampleGrabberInPin *m_pPin; // このオブジェクトを作成したピン。
CSampleGrabberAllocator(CSampleGrabberInPin *pParent, HRESULT *phr)
: CMemAllocator(NAME("SampleGrabberAllocator"), NULL, phr),
m_pPin(pParent)
{
};
~CSampleGrabberAllocator()
{
// m_pBuffer をクリアする。これは割り当てられたバッファではなく、
// デフォルト デストラクタはこれを解放しようと試みる。
m_pBuffer = NULL;
}
// 上流ピンに対し、必要なプロパティを通知する。
HRESULT GetAllocatorRequirements( ALLOCATOR_PROPERTIES *pProps );
HRESULT Alloc();
void ReallyFree();
};
class CSampleGrabberInPin : public CTransInPlaceInputPin
{
CSampleGrabberAllocator *m_pPrivateAllocator;
ALLOCATOR_PROPERTIES m_allocprops;
BYTE *m_pBuffer;
protected:
HRESULT SetDeliveryBuffer(ALLOCATOR_PROPERTIES props, BYTE *pBuffer);
public:
// ユーザーが指定したもの以外のアロケータを拒否する(設定されていた場合)
STDMETHODIMP NotifyAllocator(IMemAllocator *pAllocator,
BOOL bReadOnly);
// 必要ならば、特殊なアロケータを返す。
STDMETHODIMP GetAllocator( IMemAllocator **ppAllocator );
};
class CSampleGrabber : public CTransInPlaceFilter
{
HRESULT SetDeliveryBuffer(ALLOCATOR_PROPERTIES props, BYTE *pBuffer);
};
ソース ファイルに次のコードを追加します。
//------------------------------------------------------------------------
// SetDeliveryBuffer: 入力ピンに対し、使用するアロケータ バッファを通知する。
// 入力ピンの SetDeliveryBuffer メソッドのコメントを参照。
//------------------------------------------------------------------------
HRESULT CSampleGrabber::SetDeliveryBuffer(ALLOCATOR_PROPERTIES props,
BYTE *pBuffer )
{
// ピンが接続されている間は配信バッファを変更しない。
if(InputPin()->IsConnected() || OutputPin()->IsConnected())
{
return E_INVALIDARG;
}
return ((CSampleGrabberInPin*)m_pInput)->
SetDeliveryBuffer(props, pBuffer);
}
STDMETHODIMP CSampleGrabberInPin::NotifyAllocator(
IMemAllocator *pAllocator, BOOL bReadOnly )
{
if (m_pPrivateAllocator)
{
if (pAllocator != m_pPrivateAllocator)
{
return E_FAIL;
}
else
{
// 上流フィルタが読み取り専用バッファを望んでいるが、
// 我々がこれを望んでいない場合は、実行を失敗させる。
// 上流フィルタが読み取り専用バッファを要求しなかったが、
// われわれがこれを望んでいる場合には、問題はない。
if (bReadOnly & !SampleGrabber()->IsReadOnly())
{
return E_FAIL;
}
}
}
return CTransInPlaceInputPin::NotifyAllocator(pAllocator, bReadOnly);
}
STDMETHODIMP CSampleGrabberInPin::GetAllocator(
IMemAllocator **ppAllocator)
{
if( m_pPrivateAllocator )
{
*ppAllocator = m_pPrivateAllocator;
m_pPrivateAllocator->AddRef();
return NOERROR;
}
else
{
return CTransInPlaceInputPin::GetAllocator( ppAllocator );
}
}
HRESULT CSampleGrabberInPin::SetDeliveryBuffer(ALLOCATOR_PROPERTIES props,
BYTE *pBuffer )
{
// 複数のバッファを許容しない。
if (props.cBuffers != 1)
{
return E_INVALIDARG;
}
if (!pBuffer)
{
return E_POINTER;
}
m_allocprops = props;
m_pBuffer = pBuffer;
HRESULT hr = S_OK;
m_pPrivateAllocator = new CSampleGrabberAllocator(this, &hr);
if (!m_pPrivateAllocator)
{
return E_OUTOFMEMORY;
}
m_pPrivateAllocator->AddRef();
return hr;
}
//------------------------------------------------------------------------
// GetAllocatorRequirements: アロケータのプロパティを取得する。
//------------------------------------------------------------------------
HRESULT CSampleGrabberAllocator::GetAllocatorRequirements(
ALLOCATOR_PROPERTIES *pProps)
{
*pProps = m_pPin->m_allocprops;
return NOERROR;
}
//------------------------------------------------------------------------
// Alloc: メモリの割り当ては行わず、単にアプリケーションが指定した
// バッファを使用する。
//------------------------------------------------------------------------
HRESULT CSampleGrabberAllocator::Alloc()
{
// このコードの大部分は CMemAllocator::Alloc から直接に得られたもの。
CAutoLock lck(this);
// SetProperties が呼び出されたことをチェックする。
HRESULT hr = CBaseAllocator::Alloc();
if (FAILED(hr)) {
return hr;
}
// 条件が変更されていない場合は、再割り当てを行わない。
if (hr == S_FALSE) {
ASSERT(m_pBuffer);
return NOERROR;
}
ASSERT(hr == S_OK);
// 古いリソースを解放する。
if (m_pBuffer) {
ReallyFree();
}
// アラインメントされたサイズを計算する。
LONG lAlignedSize = m_lSize + m_lPrefix;
if (m_lAlignment > 1) {
LONG lRemainder = lAlignedSize % m_lAlignment;
if (lRemainder != 0) {
lAlignedSize += (m_lAlignment - lRemainder);
}
}
ASSERT(lAlignedSize % m_lAlignment == 0);
// メモリは割り当てない。アプリケーションが指定したバッファを
// 使用する。
m_pBuffer = m_pPin->m_pBuffer;
if (m_pBuffer == NULL) {
return E_OUTOFMEMORY;
}
LPBYTE pNext = m_pBuffer;
CMediaSample *pSample;
ASSERT(m_lAllocated == 0);
// 新しいサンプルを作成する。個々のサンプルについて m_lSize バイトを、さらに
// サンプル 1 つにつきプレフィックスとして m_lPrefix バイトを割り当てている。
// GetPointer が m_lSize バイトへのポインタを返すように、ポインタを
// プレフィックスの後のメモリに設定する。
for (; m_lAllocated < m_lCount; m_lAllocated++, pNext += lAlignedSize)
{
pSample = new CMediaSample(NAME("Sample Grabber media sample"),
this, &hr;, pNext + m_lPrefix, m_lSize);
ASSERT(SUCCEEDED(hr));
if (pSample == NULL) {
return E_OUTOFMEMORY;
}
m_lFree.Add(pSample); // 実行に失敗することはない。
}
m_bChanged = FALSE;
return S_OK;
}
//------------------------------------------------------------------------
// ReallyFree: メモリの解放は行わない。アプリケーションによって
// 割り当てられているため。
//------------------------------------------------------------------------
void CSampleGrabberAllocator::ReallyFree()
{
// このコードの大部分はCMemAllocator::ReallyFreeから直接に得られたもの。
ASSERT(m_lAllocated == m_lFree.GetCount());
CMediaSample *pSample;
for (;;) {
pSample = m_lFree.RemoveHead();
if (pSample != NULL) {
delete pSample;
} else {
break;
}
}
m_lAllocated = 0;
// メモリの解放は行わず、アプリケーションに任せる。
}
このカスタム アロケータを使用するには、前に示したサンプル アプリケーションに次のコードを追加します。
// 部分的に指定されたメディア タイプをセットアップする。
CMediaType mt;
#if 1
mt.SetType(&MEDIATYPE_Video );
mt.SetSubtype(&MEDIASUBTYPE_RGB24);
#if 1
ALLOCATOR_PROPERTIES props;
props.cBuffers = 1;
props.cbBuffer = 320*240*3;
props.cbAlign = 1;
props.cbPrefix = 0;
BYTE *pBuffer = new BYTE[320*240*3];
pGrab->SetDeliveryBuffer(props, pBuffer);
memset(pBuffer, 0, 320*240*3);
#endif
#else
mt.SetType(&MEDIATYPE_Audio);
#endif
ASSERT(hr == NOERROR);
pGrab->SetAcceptedMediaType(&mt);
形式変更への対応
DirectShow では、グラフの実行中に、ピンの再接続なしにストリームの形式が変更されることがあります。上流フィルタが、ソース メディアが形式を切り替えたために形式の変更を要求することもありますし、下流フィルタが効率を高めるために形式の変更を要求することもあります。たとえば、Video Renderer フィルタはつねに GDI との互換性がある RGB タイプで接続します。しかし、ストリーミングが開始されると、DirectDraw のために YUV タイプへの切り替えを試みます。
形式の変更を要求するために、フィルタは以下の操作を行います。
- 上流または下流の隣接するフィルタに対して IPinConnection::DynamicQueryAccept または IPin::QueryAccept を呼び出し、新しいメディア タイプを指定します。
- 他のピンが S_OK を返した場合、フィルタは IMediaSample::SetMediaType を呼び出して、新しいメディア タイプを次のサンプルに適用します。
CTransInPlaceFilter クラスの中で、QueryAccept の呼び出しは最終的には CheckInputType の呼び出しを引き起こします (これは、要求が下流フィルタから来た場合にも起こります。詳細については、ソース コードを参照してください)。サンプル グラバでは、この実装のために予期しない動作が生じることがあります。たとえば、サンプル グラバを幅広いメディア タイプ (例: 任意のビデオ タイプ)を受け付けるように構成したとします。下流フィルタが、RGB タイプから YUV タイプのように形式の変更を要求した場合、サンプル グラバは新しいタイプを受け付け、次に受信するサンプルは期待した形式とは異なる形式になります。
形式の変更に対応するための方法としては、以下のものがあります。
- フィルタは新しい形式を拒否する。
- フィルタは Transform メソッドの中で新しい形式をチェックし、アプリケーションに対して、形式が変更されたことを通知する。
- アプリケーションはコールバック メソッドの中で新しい形式をチェックする。
形式の変更をチェックするには、個々のサンプルに対して IMediaSample::GetMediaType を呼び出します。通常、メディア タイプは NULL です。形式の変更が行われた後の最初のサンプルは新しいメディア タイプを持ち、それ以降のサンプルは再び NULL のタイプを持ちます。
パフォーマンス上の注意事項
形式の変更を拒否すると、パフォーマンスに影響が及ぶことがあります。たとえば、ある開発者は、コールバック関数が何もしていないにもかかわらず、サンプル グラバのパフォーマンスが低下していることに気づきました。問題の原因は、その開発者がサンプル グラバを RGB タイプのみを受け付けるように構成していたため、Video Renderer が YUV タイプに切り替えることができなかったことにありました。グラフからサンプル グラバを削除すると、Video Renderer は YUV タイプを受け付けるデコーダに直接に接続します。YUV タイプのレンダリングは多くのビデオ カードで高速に行われるので、サンプル グラバを外すことでパフォーマンスを改善することができます。
一方、アプリケーションが自分が受け取ったサンプルをレンダリングしない場合には、この点は問題にならない可能性があります。
DirectShow サンプル グラバの制限
DirectShow に付属しているサンプル グラバにはいくつかの制限があります。これらの制限を理解しておけば、GrabberSample Filter のソース コードをアプリケーションのニーズに合わせて変更できます。
ワンショット モード
ワンショット モードでは、サンプル グラバは、このホワイト ペーパーで前に説明したように、サンプルを受け取ったときに S_FALSE を返します。S_FALSE の戻り値は、上流フィルタに対してデータの送信を停止するように通知します。このメカニズムにはいくつかの欠点があります。
- 上流フィルタはデータの送信を停止しますが、Filter Graph Manager に EC_COMPLETE イベントを送信することはしません。このため、フィルタがサンプルを受け取ったことがアプリケーションには通知されません(ただし、アプリケーションがコールバックを設定している場合には、それが呼び出されるのを待つことができます)。
- 上流フィルタが出力キューにサンプルを配信するためにワーカー スレッドを使用している場合には、ワーカー スレッドは動作しつづけます。
これよりも優れたアプローチとしては、アプリケーションがコールバック メソッドの中でS_FALSEを返し、フィルタがその値を Return メソッドの中で戻り値として使用するという方法があります。このようにフィルタを設計することで、ワンショット モードを新たに設計する必要がなくなります。
ビデオ形式
ビデオ タイプについては、サンプル グラバは VIDEOINFOHEADER 形式を必要とします。VIDEOINFOHEADER2 や DVINFO などの他の形式タイプを必要とするフィルタに接続することはできません。このため、MPEG-2 や DV ビデオ、またはフィールド ベースの (インターレース) ビデオとの互換性はありません。
バッファード モード
サンプル グラバのバッファード モードは、あまり有用ではありません。アプリケーションは、サンプルをバッファにコピーしたいのであれば、コールバックの中で行うことができます。
形式の変更
アプリケーションは、サンプル グラバに対してパーシャル メディア タイプか完全なメディア タイプを指定することができますが、「MEDIASUBTYPE_RGB24 または MEDIASUBTYPE_UYVY」のように 2 つの異なるメディア タイプを指定することはできません。この実装のために、サンプル グラバの形式変更への対応が制限されています。
ページのトップへ