ホーム < ゲームつくろー! < DirectX技術編 < 音声付き動画をフルスクリーンで再生する


その5 音声付き動画をフルスクリーンで再生する


 前章までは簡易的なプログラムにはじまり、DirectShowの概要、GraphEditによるDirectShowの動作テストを説明してきました。いよいよこの章からDirectShowを用いたプログラムを実施していきます。DirectShowをゲームに適用する機会は、ゲーム全体の規模から見るとそれほど多くはありませんが、動画再生を行うにあたっては話は別で、最強の威力を発揮します。もちろん、Windows APIでも動画再生は可能ですが、MPEGやその他のフォーマットに対応させるには大変な苦労を強いられるでしょう。その点DirectShowはフィルタがフォーマット差を解消してくれるので安心してプログラムを組むことが出来ます。

 この章では、誰もがきっと望むだろうと思われる「フルスクリーンで音声付き動画を再生する」プログラムを作ります。ついでに、DirectShowのプログラムの基礎をみっちりやってしまいましょう。



@ COMの基本は初期化から

 DirectShowはPlatform SDKに移行し、完全にCOM扱いになってしまいました。よって、他のDirectXのコンポーネントよりも純粋にCOMな分、プログラムはちょっとだけめんどくさくなっています。ただ「COMとは?」という根本話はここではしません。今回の音声付き動画をフルスクリーンで再生する方法は、実はコンソールアプリケーションでも実現できますが、サンプルとの兼ね合いがありまして、Windowsプログラムで作ることにします。

 COMを使い始めるときには、COMを初期化しなければなりません。これは1スレッドについて1度行う必要があります。つまりもし3つのスレッドそれぞれでCOMを使用するのであれば、この初期化を3ヵ所そえぞれで行います。COMの初期化自体はとても簡単で、CoInitializeEx関数を用います。まず、objbase.hというヘッダーファイルをインクルードします。ここにCoInitializeEx関数が存在します。次に、

#define _WIN32_DCOM

というマクロ定数を定義します。これが無いとコンパイラが正しく通りません。この設定後、

CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);

この1行を機械的に呼び出せば初期化が行われてCOMを利用できるようになります。今のうちに言っておくと、COMを使用し終わったら終了宣言をしなければなりません。これは、

CoUninitialize();

を呼び出します。これを呼び出した後は、そのスレッドにおいてもうCOMは使用できません。



A フィルタグラフマネージャの取得

 DirectShowのプログラムは実質ここから始まります。まずはフィルタグラフを管理するフィルタグラフマネージャを作成します。取得するインターフェイスはIGraphBuilderです。このインターフェイスはWindows自体が提供するためCoCreateInstance関数というグローバルな関数を用いて生成します。

IGraphBuilder *pGB = NULL;
HRESULT hr = CoCreateInstance( CLSID_FilterGraph, NULL, CLSCTX_INPROC_SERVER, IID_IGraphBuilder, (void**)&pGB);

 CoCreateInstance関数はCOMとして登録されているインターフェイスを生成して返してくれます。
第1引数にインターフェイスを含むクラスIDを指定します。IGraphBuilderを取得するときに指定するIDはCLSID_FilterGraphと決まっているので、無条件で上のように与えます。
第2引数はCOMオブジェクトの内包状態を指定するフラグなのですが、殆どのCOMはNULLでOK。
第3引数は生成するCOMの実装ソースがあるDLLの所在を表します。IGraphBuilderについてはDLLがローカルにありますのでインプロセスであるCLSCTX_INPROC_SERVERを指定します。
第4引数に欲しいインターフェイスの固有IDを指定します。IGraphBuilderインターフェイスを表すIDはIID_IGraphBuilderです。
関数が成功すると第5引数にIGraphBuilderインターフェイスへのポインタが返ります。

この処理はもう決まりきっていますので、変更のしようがありません。「IGraphBuilderの取得の仕方はこうだ!」と決めてしまってよいでしょう。

 IGraphBuilderの役目はフィルタグラフにかかわるインターフェイスの提供、およびフィルタグラフの管理です。



B VMR9フィルタの作成

 レンダリングを担当するVMR9フィルタを生成してフィルタグラフに追加します。この生成にもCoCreateInstance関数が用いられます。

IBaseFilter *pVMR9 = NULL;
CoCreateInstance( CLSID_VideoMixingRenderer9, NULL, CLSCTX_INPROC_SERVER, IID_IBaseFilter, (void**)&pVMR9);

 関数が成功するとpVMR9にVMR9フィルタへのポインタが返ります。それをグラフフィルタに追加するにはIGraphBuilder::AddFilter関数を用います。

pGB->AddFilter( pVMR9, L"VMR9" );

 第1引数にフィルタへのポインタ、第2引数にその名前をワイド文字で指定します。



C VMR9をウィンドウレスモードにする

 今の状態でVMR9はウィンドウモードになっています。これをウィンドウレスモードにしなければフルスクリーン描画はできません。しかし、実はVMR9のメンバ関数にこれを設定する関数はありません。この設定は、VMR9が提供するIVMRFilterConfigインターフェイスを通して行います。この辺りが実にCOMっぽい感じです。VMR9からインターフェイスを取得するので、QueryInterface関数を使いましょう。尚、言い忘れていましたが、VMR9を扱うにはVmr9.hをインクルードする必要があります。ご注意下さい。

IVMRFilterConfig *pVMRCfg = NULL;
pVMR9->QueryInterface( IID_IVMRFilterConfig9, (void**)&pVMRCfg );

 IVMRFilterConfigはVMRを制御する様々な関数を提供してくれます。ウィンドウモードを切り替える関数はIVMRFilterConfig::SetRenderingMode関数です。

pVMRCfg->SetRenderingMode( VMRMode_Windowless );

VMRMode_Windowlessというフラグを与えることで、ウィンドウレスモードに切り替わります。今回はもうこのIVMRFilterConfigインターフェイスをもう使わないので、ここでさっさと解放です。

pVMRCfg->Release();



D 描画ウィンドウの指定

 次に描画対象となるウィンドウを指定します。「え?フルスクリーンでしょ?」と思われるかもしれませんが、よく考えれば指定が必要なのがわかります。まず、フルスクリーン描画とは「フレームが無くて画面いっぱいに広がったウィンドウへの描画」であり、そこにはやっぱりウィンドウがあります(WindowsというOSはウィンドウハンドルの無いものに描画できません)。今回使用するVMRは「あるウィンドウ内に動画を合成してレンダリングするフィルタ」と捉えるのが正しく、描画のためのウィンドウが必要になります。ウィンドウレスモードとは「あるウィンドウ内に境界線の無い動画を重ねるモード」という意味なんです。これらのことから描画対象となるウィンドウが必ず必要になります。指定したウィンドウを画面いっぱいに広げれば、それはもうフルスクリーンモードです。
 今回の場合、Direct3Dアプリケーションを初期化する時に渡される親ウィンドウのウィンドウハンドルを指定します。Direct3Dは初期化時にウィンドウモードかフルスクリーンモードかを選択できます。ですから、初期化時にフルスクリーンモードで使用すれば、動画も一緒にフルスクリーンになるというわけです。

 ウィンドウの設定はVMRのウィンドウ関連の設定インターフェイスであるIVMRWindowlessControlインターフェイスをVMRから取得し、そのSetVideoClippingWindow関数にウィンドウハンドルを指定します。これも、とてもCOMっぽい指定の仕方ですね。

IVMRWindowlessControl *pVMRWndCont = NULL;
pVMR9->QueryInterface( IID_IVMRWindowlessControl9, (void**)&pVMRWndCont );
pVMRWndCont->SetVideoClippingWindow( hWnd );

pVMRWndCont->Release();

 これで、VMR9の描画先のウィンドウは決まりました。しかし、まだ「どういうサイズで描画するのか」を決めていません。ところが、このサイズはVMR9をフィルタグラフ内で接続しなければ設定できない決まりになっています。よってまず、フィルタグラフを完成させてしまいましょう。



E ソースフィルタの作成と登録

 フィルタグラフを完成させるために、動画ファイルを読み込んでくれるSource Filterを作成し、フィルタグラフに追加します。
 Source FilterはIGraphBuilder::AddSourceFilter関数で簡単に作成でき、同時にフィルタグラフに追加できます。

WCHAR wFileName[] = L"OP.AVI";
IBaseFilter *pSource = NULL;
pGB->AddSourceFilter(wFileName, wFileName, &pSource);

 AddSourceFilter関数の第1引数と第2引数が同じになっていますが、意味は違います。第1引数はソースファイルの名前ですが、第2引数は「フィルタの名前」です。フィルタにはユーザ固有の名前を付けることが出来ます。ここではファイル名と別にする理由が無いので同じ引数を与えているだけです。別にしてもかまいません。指定のファイルがパスの通っているディレクトリ内にあって、それが何らかのメディアファイルであり、それを扱えるフィルタがあった場合、pSourceにそのフィルタへのポインタが返ります。この時、フィルタグラフへの登録はもう終わってしまっています。よって、IGraphBuilder::AddFilter関数を呼ぶ必要はありません。

 ファイル名に関して重要な注意があります。第1および第2引数の型は「LPCWSTR」になっています。これはワイド文字(例えばunicode)というやつです。私たちが以前まで普通に使っていたのはマルチバイト文字でして、これは「LPCSTR」です。マルチバイト文字とワイド文字は互換性がありません。ですから、ワイド文字がコンパイラオプションに設定されていない環境では、文字をプログラマが明示的にワイド文字に変換する必要があります。ただリテラル(ダブルクォーテーションで囲まれた文字)で定義する場合は「L接頭子」を付けると常にワイド文字として扱われます。

 レンダラとソースフィルタをフィルタグラフに登録し終えましたので、次にこれらのフィルタを繋ぎます。この時に楽をする方法があります。



F ICaptureGraphBuilder2インターフェイスの取得

 フィルタの接続は意外と面倒なものなのですが、これに関してヘルパーインターフェイスが用意されています。「ICaptureGraphBuilder2」というインターフェイスは、フィルタの接続をある程度自動化してくれまして、使い勝手がとても良いものです。ここではこれを使うことにしましょう。このインターフェイスの取得にもCoCreateInstance関数を用います。

ICaptureGraphBuilder2 *pCGB2 = NULL;
HRESULT hr = CoCreateInstance( CLSID_CaptureGraphBuilder2, NULL, CLSCTX_INPROC_SERVER, IID_ICaptureGraphBuilder2, (void**)&pCGB2);

 取得したICaptureGraphBuilder2インターフェイスは初期化が必要なのですが、これはとても簡単でして先に取得したIGraphBuilderインターフェイスをICaptureGraphBuilder2::SetFiltergraph関数に渡すだけです。

hr = pCGB2->SetFiltergraph( pGB );

 これでICaptureGraphBuilder2を使用する準備が整いました。次に、フィルタを繋ぎます。



G フィルタの接続

 ICaptureGraphBuilder2インターフェイスを用いる場合、フィルタの接続は驚くほど簡単です。まず、ソースフィルタをVMR9に接続するには、以下のようにします。

pCGB2->RenderStream(0, 0, pSource, 0, pVMR9);

これだけで接続完了です。背後で「Intelligent Connect」が大活躍しているので、プログラマはとっても楽が出来ているわけです。RenderStream関数はソースフィルタをレンダラフィルタに自動的に繋いでくれる関数で、次のように定義されています。

HRESULT RenderStream(
         const GUID   *pCategory,
         const GUID   *pType,
         IUnknown     *pSource,
         IBaseFilter    *pIntermediate,
         IBaseFilter    *pSink
);

 まず、3番目の引数pSourceが大切なので先に説明します。これはIGraphBuilderインターフェイス内に登録された繋ぎたい出力フィルタを指定します。今回の場合これはソースフィルタになっていますが、他のフィルタでもかまいません。ここは必ず指定する必要があります。
 pCategoryは接続時に入出力するピンの種類を指定するフラグです。多機能なフィルタの場合、出力フィルタに色々な意味があり、Intelligent Connectに時間がかかる場合があります。そういう時にピンの種類をフラグで明確にすることですばやく接続が行われます。今回のようにビデオの単純なレンダリングなど、接続元と先が明らかである場合はNULL指定によりIntelligent Connectに接続を任せることも出来ます。
 pTypepSourceの出力ピンの種類を指定する「メジャータイプ」と呼ばれるGUIDへのポインタを指定します。「メジャータイプ」というのはその名の通り、世の中でよく知られているピンの種類で、例えばMEDIATYPE_Audioだとオーディオ、MEDIATYPE_MidiだとMIDI、MEDIATYPE_Videoだとビデオを表します。これ以外の指定はマニアックになりますので詳しくはマニュアル「メジャータイプ」を参照してください。NULL指定をすると、すべてIntelligent Connectに任せます。
 pInterMediateには接続の際に中間フィルタとして接続して欲しいフィルタへのポインタを渡します。例えば、ビデオをファイルへ出力する前に圧縮をかけて欲しい時などに、任意の圧縮フィルタへのポインタをここに指定すると、Intelligent Connectがちゃんとそれを仲介してくれます。もちろんNULLが指定できます。
 pSinkが接続先の入力フィルタということになります。これはレンダラに限らず、ミキシングフィルタ(合成を担当するフィルタ)などももちろん指定できます。面白いのが、ここにNULLを指定できると言う点です。入力先を未指定にした場合、この関数はデフォルトの入力フィルタへ接続を試みます。例えば今回の指定で「pVMR9」を指定しなければ、多分デフォルトのビデオレンダラが指定されます。これはオーディオの場合も同じです。

 ビデオの接続は先の指定で終わりましたので、今度はオーディオの接続をしましょう(これを省くと無音の動画になります)。オーディオのフィルタは今回1つも作成登録していませんでしたが、これはこのRenderStream関数がデフォルトをちゃんと指定してくれるためです。オーディオの接続は次のように行います。

pCGB2->RenderStream(0, &MEDIATYPE_Audio, pSource, 0, 0);

 ピンの種類を「オーディオ」に指定し、入力フィルタを未指定にしています。これでデフォルトのオーディオフィルタ(多分Default DirectSoundフィルタ)に接続してくれます。

 これで、フィルタの生成登録から、ピンの接続まで終わりました。つまり、フィルタグラフの完成です!



H レンダリングサイズの指定

 フィルタグラフが完成した後に、VMR9ではレンダリング先のウィンドウに対して動画の出力サイズを設定する必要があります。これはお作法がありますので、その通りにプログラムします。

// 描画領域の設定(接続後でないとエラーになる)
LONG W,H;
RECT SrcR, DestR;
hr = pVMRWndCont->GetNativeVideoSize(&W, &H, NULL, NULL);
SetRect(&SrcR, 0, 0, W, H);
GetClientRect(hWnd, &DestR);
hr = pVMRWndCont->SetVideoPosition(&SrcR, &DestR);
pVMRWndCont->Release();           // ウィンドウレスコントロールはもう必要ない

 レンダリングサイズを指定するメンバ関数は、Dで取得したIVMRWindowlessControlインターフェイスが持っています。まず、GetNativeVideoSize関数で接続した動画のサイズを取得します(第1および第2引数)。これが「ソースサイズ」となりまして、上のプログラムではSetRect関数で値を保持しています。次に貼り付け先の矩形サイズを取得します。今回はウィンドウいっぱいいっぱいに広げますから、ウィンドウのクライアントサイズそのものになります。よって、GetClientrect関数(Windows API)でそのサイズをDestRに格納するようにします。ソースのサイズと貼り付け先の矩形サイズが決まりましたので、IVMRWindowlessControl::SetVideoPosition関数で貼り付け元と先のサイズを指定します。これで、画面全体に動画が描画されるようになります。この段階でIVMRWindowlessControlインターフェイスはお役目御免なので、リリースしてしまいましょう。



I フィルタグラフの実行

 フィルタグラフの接続が全て終了し、表示サイズを正しく指定すれば、いつでも動画を再生することができます。再生を担当するインターフェイスは、IGraphBuilderから生成できるIMediaControlインターフェイスです。

IMediaControl *pMediaCont = NULL;
pGB->QueryInterface( IID_IMediaControl, (void**)&pMediaCont );

 この状態で、

pMediaCont->Run();

とすると、指定のウィンドウ内で動画の再生が始まります。再生中は毎フレームごとに現在の状態を取得します。ゲームではそれほど制御精度を要求されないので、簡単なIMediaControl::GetState関数を使うことにします。

 IMediaControl::GetState関数は現在のフィルタグラフが再生中なのか、停止中なのか、一時停止中なのかを取得することが出来ます。

HRESULT GetState(
      LONG             msTimeout,
      OAFilterState   *pfs
);

 msTimeOutは状態を取得する時間を指定します。ここにINFINITEを指定すると無限時間待つことになります。ただ、待機中(状態を取得出来ない状態)は呼び出し元の処理が一切進みませんので、INFINITEは指定してはいけません
 pfsには現在の状態を格納するOAFilterState変数へのポインタを渡します。この変数にはFilter_State列挙型のいずれかが返ります。

typedef enum _FilterState
{
      State_Stopped,   // 停止中
      State_Paused,    // 一時停止中
      State_Running   // 再生中
} FILTER_STATE;



J 終了処理

 動画再生が終わり、もう使用しないフィルタグラフは適切に終了させる必要があります。
 まず、現在描画中の動画をIMediaControl::Stop関数で止めます(すでに止まっていても呼び出しは可能です)。これにより、各フィルタに停止命令が飛び、全ての動作がストップします。
 次に、生成した各インターフェイスを解放します。これまでに存在しているインターフェイスは、

  ・ pMediaCont (IMediaControl)
  ・ pVMR9 (IBaseFilter)
  ・ pCGB2 (ICaptureGraphBuilder2)
  ・ pGB (IGraphBuilder)

の4つです。これを順次解放していきます。解放順番はきっと厳密ではないのだろうと思うのですが、一応作成順と逆に解放しておきます。

pMediaCont->Release();
pVMR9->Release();
pCGB2->Release();
pGB->Release();

これで、綺麗さっぱり動画処理部分が無くなります。最後に、

CoUninitialize();

を忘れずに。



H クラス化に向けて

 以上で「指定のウィンドウいっぱいに動画を描画する」事ができます。ただ、今回の本当の目的は「フルスクリーンで描画する」でした。しかし、ここまで読み進めていただけるとおわかりだと思うのですが、フルスクリーンはウィンドウを画面いっぱいに広げたに過ぎず、「動画を再生する」という部分と分離しているわけです。Direct3Dにはフルスクリーンモードがちゃんと用意されていまして、それを用いれば動画は自動的にフルスクリーンになるわけです。

 よって、この部分のクラス化というのは、「指定のウィンドウいっぱいに動画を描画する」という機能を持ったクラスを作るということになります。これは上記の初期化及びフィルタグラフの接続をちゃんと行えば非常に簡単に作成できます。DirectShowの素晴らしいところは、再生する動画の種類を限定しなくても良いという所にあります。AVIだろうがMPEGだろうが、そのパソコンのWindowsが扱える動画であれば、ソースフィルタが自動的にそれを判定し、Intelligent Connectが専用の変換フィルタを選択してレンダラと接続してくれます。よって、クラスに渡すのは描画先のウィンドウハンドルと、再生したい動画ファイル名くらいなものです。そう考えると、クラス化は非常に簡単でしょう。

 ただ、VMR9が使えない環境である場合などは(今はめったにありませんが)、代替手段を講じる必要があります。これは、VMR7を用いるか、旧式のFull Screen Rendererを用いるかという選択になるでしょう。VMR7はWindows XP環境でしか使えないので厳しいのですが、Full Screen Rendererは相当に古いビデオカードでも利用可能です。ただ、パフォーマンスやレンダリング精度は落ちます。この辺をどう実装するかが、安定した動画を提供するクラス作りの肝となるでしょう。