公開。報告を受けてまだ直せていないバグもあるのだけど、そちらは根が深いので、直せたところまでというバージョンで公開。
むぅ、壊れたデータへの耐性が低いよな〜。0.7.0 ではもう少し考えて (エンコーダは壊れたデータを吐き出すし、メモリではデータ化けが発生するし、CPU はオーバークロックのせいで計算結果を間違えるし、HDD ではデータ破壊が起きる前提で) コードを書くことにしよう。
一般的な DirectShow 環境ではファイルソース、スプリッター (パーサー)、デコーダ、レンダラーという 4 種類のフィルタ (モジュール - DLL) が組みあわされて動作します。フィルタの組み合わせ方のことを Microsoft はフィルタグラフと呼んでいて、フィルタグラフは GraphEdit (DirectShow の開発環境に入っているアプリケーション) で確認することができます。実際に MPG ファイルを再生しているときのフィルタグラフを表示してみると次のようになります。
メディアプレイヤーで再生できるフォーマットを増やしたいなどといった DirectShow に対する機能追加は、新しいフィルタを作成して、DirectShow 環境に登録することで実現されます。わかりやすい例を挙げると、ソフトウェア DVD プレイヤーをインストールした場合、標準では用意されていない MPEG-2 ビデオのデコーダフィルタや AC3 オーディオのデコーダフィルタなどが PC に追加登録され、そして DVD の再生が可能になるという形になっています。
しかし、一般に Microsoft 以外が作成したフィルタの品質というのはあまり優れたものではありません。Microsoft が作成した標準のフィルタでも、スマートティーやサンプルグラバといった通常のファイル再生では利用されないフィルタの品質は、優れているといいがたいものばかりです。(標準フィルタを試してみる・問題が出る・同等機能のよりまともな独自フィルタを作るというループを繰り返していると、標準フィルタの存在は、ある意味壮大な罠なのではないかと思ってしまうほどです)
私はこの混沌が発生したのは、Microsoft が DirectShow という一種フレームワークを用意しておきながら、ドキュメントによる説明を十分に行っていないために、各フィルタ開発者があるべき仕様というものを推測しながら開発しているのが原因ではないかと考えました。そこで、私が理解している範囲での DirectShow フレームワークの仕組みを解説してみようと思います。これからしばらくの間、お付き合いをお願いします。
なんで唐突にこんなことを書き始めたのかというと、現在どっぷりとはまり込んでいる某お仕事で、あまりにも質の悪いフィルタに出くわした挙句、ソース出してもらって私が直すという作業をする羽目になったから。いーかげん嫌になった。こんな不毛な作業で時間消費したくない。
前回と同じ、MPG ファイルを標準フィルタのみで再生する場合のフィルタグラフを使って説明を続けます。ファイルソース、スプリッター (パーサー)、デコーダ、レンダラーという 4 種類のフィルタが存在すると書きましたが、それぞれフィルタグラフ上では次の名前で表示されています。
表示名 | 種別 |
Azurewind.mpg | ファイルソース |
MPEG-I Stream Splitter | スプリッター(パーサ) |
MPEG Video Decoder | デコーダ |
MPEG Audio Decoder | デコーダ |
Video Renderer | レンダラー |
Default DirectSound Device | レンダラー |
フィルタは、フィルタグラフ上で矢印によって接続されていますが、この矢印は再生中のデータの流れる方向を示しています。また矢印の根元側を上流側、先端側を下流側と呼びます。
上流側から各フィルタの機能を説明していきます。ファイルソースフィルタは下流側から要求されたデータをファイルから読み込んで、引き渡すことだけを担当しています。
スプリッターフィルタは、ファイルソースフィルタに位置を指定してデータを要求し、渡されたデータをビデオ・オーディオに分離 (スプリット) して各下流側フィルタに順次渡すことを担当しています。また、下流側フィルタに渡すデータに対して、タイムスタンプを付加することも担当しています。
デコーダフィルタでは、上流側フィルタから渡された圧縮データを YUV/RGB ビデオや、PCM オーディオといったレンダラーが出力可能なデータに変換して、下流側フィルタに渡すことを担当しています。
レンダラーフィルタでは、上流側フィルタから渡されたデータの表示 (映像の場合)・出力 (音声の場合) を担当しています。このとき、スプリッターフィルタによって付加されたタイムスタンプを元に、映像と音声の同期補正が行われます。同期補正の具体的な方法としては、オーディオレンダラー (フィルタに時計のアイコンを持つ) が基準クロックを提供し、ビデオレンダラーは基準クロックに対して、このタイムスタンプのデータを描画するタイミングが来たら教えてね、と依頼する形となります。
このように、DirectShow では各フィルタの役割分担 (責任範囲) が明確になっているので、DirectShow に機能追加を行いたいという場合は、一部のフィルタの代わりを用意するだけで済みます。
たとえば、ファイルからの再生だけではなく、HTTP からのダウンロード再生をサポートしたいという場合はファイルソースフィルタの代替 (標準でその機能を提供するフィルタが存在します) を用意すれば済みますし、MKV や OGM といった新しい多重化フォーマットをサポートしたい場合はスプリッターフィルタを用意すれば済みます。H.264 や AAC SBR といった新しい圧縮フォーマットをサポートしたい場合はデコーダフィルタだけを用意すれば済みます。A/V 同期などという面倒なことは標準のフィルタにまかせてしまって、アプリケーション開発者はいちいち実装する必要はなくなります。
ただし、これは正常に動作する、なるべく周囲に迷惑をかけないように考慮して作られた礼儀正しいフィルタのみで構成されている、蝶々が飛ぶお花畑な午後の茶会環境でのみ成立する夢のような話です。クソ高い優先度を主張し、そのくせまともに動作しないはしたないフィルタがひとつ紛れ込んだだけで、あっという間にまともな再生環境は失われてしまいます。
こういった不幸な事態を避けるためには、Microsoft がそれなりの権威をふるって、DirectShow フィルタの従うべき作法をドキュメント化しておくべきだと思うのですが、DirectX 9 になっても未だにドキュメントでは断片的な情報しか記載されていません。そのため、フィルタ開発者は、標準フィルタの動作やサンプルを元に脳内仕様をでっちあげる、脳内仕様を元にフィルタを作る、標準フィルタだけを相手に動作確認しながら脳内仕様を複雑化させていくというステップを踏むことになるわけです。今回私が記述する内容は、私の中に構築された DirectShow の脳内仕様です。当然ながら推測ベースなので誤りを含みます。それでも、何も無いよりはましな世界になると信じて、これを記述しています。
ファイル再生用のフィルタグラフは、メディアタイプとメリット (優先順位) を元に自動的に構築されます。フィルタの情報として、入力ピンでサポートするメディアタイプ、出力ピンでサポートするメディアタイプ、そしてメリットがレジストリに登録 (手動で登録/削除する場合は regsvr32.exe を使う) されており、フィルタグラフ構築時には、上流側フィルタの出力ピンのメディアタイプをサポートする、メリットの高いフィルタが自動的に選択されて接続されていきます。
フィルタのメリットには代表値として次の 4 つの値が存在します。
名称 | 数値 | メモ |
MERIT_PREFERRED | 0x800000 | 標準のレンダラ以外では利用すべきではない |
MERIT_NORMAL | 0x600000 | 通常の優先順位 (自動接続で使われる) |
MERIT_UNLIKELY | 0x400000 | 低めの優先順位 (自動接続で使われる) |
MERIT_DO_NOT_USE | 0x200000 | 自動的には利用されない |
これらの代表値以外にも、MERIT_NORMAL よりも高く、MERIT_PREFERRED よりも低い微妙なメリットを設定することができます。フィルタグラフの自動構築は MERIT_DO_NOT_USE を超えるメリットを持つフィルタ間での、純粋にメリットの大小の比較にもとづいて実行されます。
MERIT_PREFERRED のメモ欄に「標準のレンダラ以外では利用すべきではない」と書きましたが、これは MERIT_PREFERRED 以上と、未満の間ではグラフ構築の際のフィルタ決定方法に違いが存在するためです。MERIT_PREFERRED 未満のフィルタでは、メディアタイプの完全一致がメリット値よりも優先しますが、MERIT_PREFERRED 以上のフィルタではメディアタイプの部分一致が成立した時点で自動接続の試行が行われてしまいます。
MERIT_PREFERRED 以上のメリットを指定していても、サポートしていないデータが渡ってきた場合に正しく接続を拒絶してくれれば問題ないのですが、そういう何も考えていないメリットを指定するフィルタに限って、例外を発生させたり、無限ループに落ちたりしがちなのが困った所です。(そんなフィルタいくらなんでも存在しないだろうと思うかもしれませんが、世の中は広いのです)
MERIT_DO_NO_USE 以下のメリットは、自動的に利用されたくないフィルタに対して指定するメリット値です。このメリットを指定する場合ですが、アプリケーションにフィルタを組み込んでしまい、レジストリには登録しない [1/29 邪悪のススメ] とどちらが有利か一度検討して欲しいところです。
MERIT_UNLIKELY から MERIT_NORMAL までの優先順位に関しては、既存フィルタが存在して、それの代わりに利用するために高いメリットを指定する場合は仕方がないと思いますが、それ以外ではメディアタイプの完全一致を利用することでメリットに頼らない接続を実現した方が、多くの人が仕合せになれるのではないかと思います。
フィルタ間の接続は「ピン」と呼ばれるインタフェース [IPin] を介して行われます。標準の MPEG Video Decoder フィルタは入力と出力で 1 つずつ、合計二つのピンを持ちます。このうち、入力側には MPEG-I Stream Splitter のビデオ出力ピンが接続され、出力側にはビデオレンダラーの入力ピンが接続されます。
接続中のピンは、接続相手とメディアタイプとサンプルバッファを共有します。メディアタイプを共有するのは、下流側が、自分が受け取っているのはどのようなデータなのかを知る目安のためです。サンプルバッファを共有するのは、出力ピン (上流側) から入力ピン (下流側) へデータを渡す場合、ピン間で共有しているサンプルバッファを経由する以外に方法が無いためです。
標準的なピン間の接続手順を次にリスト化します。
以上が標準的なピン間の接続手順です。あくまでも「標準的な」接続手順です。というのは、接続手順を見ると判るようにピン間の接続はほぼ上流側フィルタの出力ピンが主導権・決定権を握っています。そして、出力ピンはフィルタ作者が実装するものですから、上記の手順を無視して好き勝手な順番で処理を行うことができます。
Microsoft が提供している基底クラスライブラリを利用した場合ならば、ほぼ上記手順どおりの接続が行われるのですが、それでも出力ピン側は入力ピン側が通知したサンプルバッファに対する要望を無視することができたりします。例えば、標準フィルタである DV デコーダフィルタは、入力ピンがバッファを 4 枚確保してくれと要望を出しても 1 枚しかバッファを確保してくれないわがままなやつだったりします。(ティーフィルタで分けて下流につなごうという時に問題を起こしてくれました)
またファイルソースフィルタと接続するフィルタの入力ピンや、ビデオレンダラーと接続するフィルタの出力ピンでは少し違った接続手順が必要になります。
ビデオレンダラーと接続するフィルタでは、オーバーレイを利用する関係から通常のピン接続とは異なるメディアタイプネゴシエーションが行われます。まず前提知識として、オーバーレイを利用する場合ビデオレンダラーとビデオデコーダフィルタの間のピン接続で共有されるサンプルバッファは、グラフィックカード上に搭載されている VRAM のアドレスが直接渡されてくる形となります。また、オーバーレイは常に利用可能という訳ではなく、再生中にオーバーレイを OFF にして、通常の GDI 出力に切り替えなければいけなくなることもありえます。
このうち、GDI とオーバーレイの切り替え検出等の処理はビデオレンダラー側が対応してくれるのですが、切り替わった後でサンプルバッファに書き込むデータの形式をオーバーレイ用のフォーマットから、GDI 用のフォーマットに変更する等の処理はビデオデコーダ側で行う必要があります。
ビデオデコーダとビデオレンダラーの間での大まかなメディアタイプネゴシエーションの流れを次にリスト化します。
メディアタイプが再生途中で変更される場合、サンプルバッファから取得した出力用のバッファには新しいメディアタイプが設定されていて、IMediaSample::GetMediaType で以降利用するバッファフォーマットが取得できます。
フォーマットが変更されていない場合、出力用のサンプルに対して IMediaSample::GetMediaType() を実行しても S_FALSE が返ります。この場合、以前と同じメディアタイプを利用することができます。
気の滅入る話ですが、ビデオレンダラーと直接接続できて、YUV/RGB オーバーレイも使えて、大抵の環境で問題がおきないようなフィルタを作ろうという場合、次のリストすべてを満たすようにフィルタを作る必要があります。
オーバーレイ用に確保されたサンプルバッファでは、グラフィックカード上の VRAM にフレームバッファが確保されるため、ラインピッチに一定の制限が課されることが普通です。GPU - VRAM 間のインタフェースには 256 bit や 128 bit 等のビット幅が利用されていますが、フレームバッファの行先頭がこのバス幅と一致していると速度面で有利になるために、このような仕組みが採用されています。
例えば、ATI や nVidia, Intel などの最近のチップでは 720x480 サイズの画像の為に用意されるバッファは 768x480 というサイズの幅が 64 の倍数に拡張されたものになりますし、Matrox のチップでは、ドライバの設定に応じて 720x480 のままで確保される場合と 1024x480 で確保される場合の 2 パターンが存在します。
ビデオデコーダがこのバッファに書き込む場合は、次の図に示すように元画像を出力先フレームバッファの左側に詰めて、右側のパディング部分には何も書き込まないようにします。
パディング部分の幅は前回記述したメディアタイプネゴシエーションの中で、ビデオレンダラー側から通知されます。フォーマット切り替えが行われる際に通知されるメディアタイプに含まれる BITMAPINFOHEADER の中で、オリジナルの幅にパディング部分の幅が加算された biWidth が設定されているので、ビデオデコーダ側はそれを見てフレームバッファのラインピッチを知るという仕様になっています。
したがって、オーバーレイを利用したいビデオデコーダフィルタはオリジナルの画像よりも広い画像幅を持ったメディアタイプを受け入れて、その画像幅からフレームバッファの幅と必要なパディング量を算出するという実装する必要があります。
すでに記述したように、フレームバッファに課された制限は VGA チップやドライバの設定によって異なりますからビデオデコーダ側ではこれらの相違をできる限り吸収してあげる必要があります。世の中は広いので、ATI と nVidia と Intel で問題がおきなきゃそれでいいだろと言わんばかりに、64 の倍数にアライメントされたフレームバッファしかサポートしない雄雄しい仕様のフィルタも存在するのですが……それは流石に頭が悪いという評価を受けても仕方がないのではないかと思います。
16 bit RGB (565/555) のメディアタイプを利用する場合 32/24 bit RGB とは異なり、マスク情報付の BITMAPINFOHEADER を利用する必要があります。次に、565 形式でのメディアタイプ設定のコードサンプルを載せます。
CMediaType mt; // 設定対象 VIDEOINFOHEADER2 *vih; // フォーマットブロック DWORD *mask; // ビットマスク用ポインタ // メディアタイプおよびサブタイプ設定 mt.SetType(&MEDIATYPE_Video); mt.SetSubtype(&MEDIASUBTYPE_RGB565); mt.SetFormatType(&FORMAT_VideoInfo2); // フォーマット用のバッファを割り当てる vih = (VIDEOINFOHEADER2 *)mt.AllocFormatBuffer(sizeof(VIDEOINFOHEADER2)+12); if(vih == NULL){ // エラー処理 } // フォーマットブロックの中身を設定 memset(vih, 0, sizeof(VIDEOINFOHEADER2)+12); // BITMAPINFOHEADER 部分 vih->bmiHeader.biSize = sizeof(BITMAPINFOHEADER); vih->bmiHeader.biWidth = 映像の幅; vih->bmiHeader.biHeight = 映像の高さ; vih->bmiHeader.biPlanes = 1; vih->bmiHeader.biBitCount = 16; vih->bmiHeader.biCompression = BI_BITFIELDS; vih->bmiHeader.biSizeImage = GetBitmapSize(&(vih->bmiHeader)); // 16 bit RGB 用のマスク部分 mask = (DWORD *)((&vih->bmiHeader)+1); mask[0] = 0x00F800; // 赤のマスク (MSB から 5 bit 分) mask[1] = 0x0007E0; // 緑のマスク (赤の次から 6 bit 分) mask[2] = 0x00001F; // 青のマスク (LSB 5 bit 分) // 他のアスペクト比情報やインタレースフラグ等は必要ならば設定する
555 形式の 16 bit RGB フォーマットを指定する場合、上のコードでの、サブタイプ指定部分とビットマスク指定部分を変更する必要があります。
BI_BITFIELDS を利用せずに、biCompression に BI_RGB を指定して biBitCount を 16 にしただけでは 555 形式の RGB として固定的に処理されてしまい、565 形式しかサポートしない ATI や nVidia 等の VGA 環境では、ディスプレイのプロパティで 16 bit に設定されている場合、ビデオレンダラーとの直接接続が行えずに間に余計なフィルタが挟まって、オーバーレイが利用できなくなってしまいます。
再生中のグラフは、スプリッター (パーサ) フィルタが駆動します。通常のフィルタは、入力ピンからサンプルを受け取った時点でそのサンプルを処理して下流のフィルタへ引き渡すだけでよかったのですが、スプリッターフィルタではフィルタ自身が上流のファイルソースフィルタにデータを要求して、切り分け、適切なタイムスタンプを付与し、下流のフィルタに引き渡さなければいけません。
再度、このフィルタグラフを例に使って説明します。ファイルソースフィルタと MPEG-I Stream Splitter の間のピン接続は IAsyncReader というインタフェースを利用した (下流側から見て) プル型の接続となっています。それ以外のピン接続はすべて IMemInputPin というインタフェースを利用した (上流側からみて) プッシュ型のピン接続です。
標準的なフィルタグラフでのメディア再生中の各フィルタの挙動を説明しておくと、次のようになります。
安定的な再生が行われている状態では、各ピン間に存在するバッファは全て満タンの状態になっていてスプリッターフィルタはバッファの空きを待ってはデータを積んでいくという状況になります。
スプリッターフィルタが複数のピンを持つ場合、スプリッターフィルタが 1 つの駆動スレッドしか持っていないと、再生がフリーズしてしまうことがあります。その具体的な条件を次に挙げます。
スプリッターフィルタの各出力ピンが十分なバッファを持てば回避できる問題ではあるのですが、十分なバッファというものは再生するファイルによって異なります。出力ピン毎にストリーム駆動用のスレッドを持たせてしまい、片方のストリームがブロックされている場合でも、もう片方のストリームは問題なく処理が進むようにスプリッターを作成するのが標準的な作法だと私は考えています。
もちろん世の中は広いので、スプリッターフィルタが単一のストリーミングスレッドしか持たないから、デコーダでブロックしちゃうと再生がフリーズしちゃうことがあるなぁ、デコーダフィルタがピン間で共有するバッファとは別に無制限 (メモリ限界まで) にデータを受信可能なバッファを持たせてブロックしないようにしちまえ、スプリッターフィルタは無制限にサンプルを送り出してるとメモリ使用量が爆発するから Sleep() と GetTickCount() でサンプル出力タイミングを調整だぁ、あれぇ A/V 同期に問題が発生した (リアルタイムクロックとサウンドカードでのクロック誤差があれば問題が発生する) なぁ、なんだかわからないけどとりあえずオーディオデコーダで適当にサンプルを破棄するようにしたら問題解決ぅワーイという頭の悪いフィルタセットを作って恥じない連中というのもいるのですが。……どうすればここまで間違った方向に突っ走れるのだろう……。
ファイルソースフィルタとスプリッターフィルタ間のピン接続は、IAsyncReader というインタフェースを利用する形になります。IAsyncReader はファイルソースフィルタの出力ピンが提供するデータ取得用のインタフェースで、基本的にすべてのソースフィルタ (の出力ピン) はこのインタフェースを提供しています。
ソースフィルタとスプリッターフィルタが接続する際には、通常のメディアタイプネゴシエーションとバッファネゴシエーションに加えて、IAsyncReader インタフェースが取得されたかどうかという判定が加わります。ピン接続の手順内で IAsyncReader をスプリッターフィルタが取得しない場合、接続は失敗します。(ソースフィルタによっては IAsyncReader が取得されない場合でも通常のピン接続と同様に接続を許容することがあります。この場合ストリームはソースフィルタが駆動します)
IAsyncReader インタフェースには、非同期読み込みのためのメソッド [Request() & WaitForNext()] と同期読み込みのためのメソッド [SyncRead()] が用意されています。IAsyncReader という名前から考えて非同期読み込みを使うのが通常の利用方法なのかもしれませんが、同期読み込み機能しか利用しない形でもスプリッターフィルタを作成することは可能です。むしろ、そちらの方が楽にフィルタを作成できるかもしれません。
実際、私が MP4 (正確には 3G2) ファイルのスプリッターフィルタを作った際には面倒だったので IAsyncReader の同期読み込み機能のみを利用してスプリッターフィルタを作ってしまいました。非同期読み込みを利用する場合だと無駄にサンプルバッファを経由しなければいけないとか、無駄にスレッドを増やす必要があるとか、無駄にアライメントを考慮しなければいけないとか、そのくせパフォーマンスの向上は 1% すら期待できないとか色々と理由があったものですから。