DirectShowを使った動画再生とフレームビットマップ取得方法

AKIYAMA Kouhei

2002年05月05日

目次

  1. 概要
  2. 材料
  3. 最も簡単な再生方法とその問題点
  4. サンプルグラバフィルタを使ったフレーム取得
  5. ISampleGrabberのコールバックを使う。
  6. Rendererを作る
  7. InputPinやAllocatorを細工する
  8. 最後に
  9. 参考文献

1 概要

この記事では私がDirectShowを学び、動画再生のテストプログラムを作った過程を大雑把に紹介する。

単にDirectShowで動画を再生すると言っても様々な方法がある。画面全体でメディアプレイヤーのごとく表示すればいいだけなのか、それとも様々なエフェクトや合成処理をしたいのか、要求によってやり方は変わってくるし難易度も変わってくる。

ここではゲーム等で必要となるであろう、毎フレームの画像をビットマップとして取り出し、CreateDIBSectionを通して自分でウィンドウにBitBltする方法をお伝えしたい。

2 材料

今回使用した材料は以下の通りである。

DirectXはもう少し古いバージョンでも問題ないかもしれない。Visual Studio .NETはまだ買っていないので知らない。

3 最も簡単な再生方法とその問題点

DirectShowを学ぶ上でまず見なければならないのは当然DirectX SDKに付属するヘルプドキュメントだ。DirectShowの章を見るとDirectShowの概要から実例、リファレンスと関連する情報が網羅されている。

まず手始めに「ファーストステップガイド」の「ファイルの再生法」を見てみよう。その一番下のサンプルコードを見て欲しい。これは最もシンプルな動画再生のコードである。是非ビルドして動作を確認して欲しい。正直これには驚いた。こんな短いコードで全てできるのである。ウィンドウの作成すらお任せなのだ。

次の「ビデオウィンドウの設定」では自分でウィンドウを作成して、そのクライアント領域に動画再生用のウィンドウをアタッチする方法も書かれている。これもとてもシンプルな物だ。

さて、こんなに簡単なDirectShowなのだが、今紹介した方法ではゲームで使うには不満がある。例えば以下のようなことをしたいときだ。

これらを実現するためにはフレームごとの画像データを取りだして、自分で適切に合成処理をして表示すればよい(ひょっとしたらDirectGraphicsと併用するという手もあるかもしれないがここでは古い2Dシステムを想定している)。上で紹介した方法はDirectShowが自動的にウィンドウを作ってそのウィンドウ内で再生を行うものだ。これは手軽で便利な方法だが、ゲーム内で使うにはいささか柔軟性に欠ける方法である。そこでフレーム毎の画像を取り出して自分で処理したいということになるのだが、DirectShowはその方法もちゃんと提供してくれている。

4 サンプルグラバフィルタを使ったフレーム取得

フレームのビットマップ画像を獲得するヘルプドキュメントに書かれている方法はサンプルグラバフィルタを使う方法だ。何かいい方法はないかなとヘルプドキュメントを眺めると「チュートリアル」の「メディアサンプルの入手」が目に入る。

DirectShowはフィルタの組み合わせで動作する。フィルタにはソースフィルタ、変換フィルタ、レンダラーフィルタがある。例えばソースフィルタがaviファイルを読み込み、変換フィルタが適切に表示できる状態へ変換し、レンダラーフィルタがウインドウに描画するといった具合である。このようにフィルタ間をデータが流れることによって最終的にメディアの再生が可能になる。

そして大事なのはDirectShowではそれらフィルタを個別に付けかえられ、さらに自分で新しいフィルタを作ることも可能な点だ。これによって任意の変換を付け加えたり、標準では対応していない形式のデータに対応したり、描画をカスタマイズできる。

ここではフレームのビットマップ画像を獲得するために、サンプルグラバフィルタを用いる。サンプルグラバフィルタはDirectShowであらかじめ用意されている変換フィルタだ。このフィルタは送られてきたデータをそのままバッファリングしてアプリケーションから参照できるようにする。サンプルグラバフィルタをソースフィルタとレンダラーフィルタの間に挟めばその間を流れるフレーム画像を獲得できるのだ。

「ビデオウィンドウの設定」のサンプルコードを「Microsoft DirectShow フィルタ グラフからデータを取得する方法」を見ながら改造して、画像を獲得して表示するようにしたプログラムをdirectshow2.cppに示す。このコードはテストと言うこともあってDIBSectionを解放をしていなかったり、書き方がいい加減だったりするがご容赦いただきたい。簡単に解説すると、以下のような流れになっている。

  1. フィルタグラフマネージャの作成
  2. フィルタグラフの構築
    1. ソースフィルタの追加
    2. サンプルグラバフィルタの追加
    3. Nullレンダラーフィルタの追加
    4. フィルタ間のピン接続
  3. DIBSection作成(無圧縮RGB24bit、ムービーと同サイズ)
  4. フィルタグラフの実行
  5. タイマー起動
  6. 一定周期で描画処理
    1. サンプルグラバが獲得した画像をDIBSectionにコピー
    2. ウィンドウをInvalidateRectして再描画を促す
    3. WM_PAINTでDIBSectionをウィンドウへBitBlt

詳しくはヘルプドキュメントを読めば分かると思う。ヘルプドキュメントに書いてあるだけでは詳細がよく分からない場合、サンプルのソースコード内を検索しよう。具体的な使い方やメソッドの典型的な実装が分かる。

ビルドの際注意する点が一つある。streams.hとそれに必要なライブラリがサンプルのディレクトリ内にあるということだ。したがってビルドするにはSDK内のdxsdk\samples\Multimedia\DirectShow\BaseClassesをINCLUDEディレクトリに追加し、baseclasses.dswをビルドして得られるstrmbase.libとstrmbasd.libをリンクできるようにしておかなければならない。baseclasses.dspではマルチスレッドライブラリを用いてビルドされるようになっているので、アプリケーション側もマルチスレッドライブラリを使用するようにしないとリンカーが警告を出す。

5 ISampleGrabberのコールバックを使う。

directshow2.cppはサンプルグラバフィルタによってフレーム画像を獲得できること確認するための物なので、いささか効率が悪いことをしている。その1つはフレームの更新にタイマーを使っている点だ。

効率よく処理するためにはフレームの更新タイミングに合わせて再描画するようにしなくてはならないだろう。「メディアサンプルの入手」の「コールバックメソッドの使用」の部分を見るとその方法が分かる。

6 Rendererを作る

ISampleGrabberでOKかと思っていたのだが、テストしているうちに以下のような問題が分かってきた。

DirectShowにおいて遅いマシンでも適切にフレームをスキップするような機能については「品質コントロールの管理」に説明がある。これによると品質コントロールはCBaseVideoRendererがやっているらしい。NullRendererはこれを継承していないのだろうか。そこで、CBaseVideoRendererを継承したRendererを自分で用意することにする。同時にデータの獲得処理もRendererでやってしまえば速度も向上させられるのではないかと考えた。

レンダラーフィルタを自作するサンプルがSDK内にある。samples\Multimedia\Directshow\Filters\SampVidがそれだ。

ヘルプドキュメントやサンプルを見て作成したのが以下である。

CBaseVideoRendererを継承して適切なメソッドをオーバーライドすれば良いだけなのでさほど難しくはない。CLSID(GUID)が必要だったのでVC6付属のguidgen.exeを使って生成した。COMにあまり詳しくない私は多少戸惑った。

7 InputPinやAllocatorを細工する

まだ遅い。もっと速くできないものだろうか。速度に影響するものは沢山あるが、何とかできそうで大きいものとしてはコピーの回数が上げられる。フィルタグラフの中をサンプル(フレーム画像が)流れていくわけだが、その際のコピーの回数を減らせられればかなりの効率アップが期待できる。

現状のコピー回数を調べてみると以下のようになる。

  1. ソースフィルタがファイルから読んで展開した画像を格納するのが1回
  2. レンダリングフィルタがDIBSectionにコピーするのが1回
  3. WM_PAINTがDIBSectionからウィンドウDCへコピーするのが1回

ショートカットできる場所は無いだろうか。レンダリングフィルタがいきなりウィンドウDCへコピーできるとは思えない。DirectDrawを使えば別だろうが。となるとソースフィルタがいきなりDIBSectionに書き込んでくれればコピーを1回減らせることになる。そんなことが可能なのだろうか。それがDirectShowでは可能なのだ。そもそもそれを行っているのがSDKサンプルのSampleVidなのだから。

フィルタ間のデータのやりとりでシステムメモリを使う場合、メモリアロケータを自前で用意することによって使用するメモリを自由に決められる。それにはレンダラーフィルタの入力ピンも自前で用意する必要がある。自前の入力ピンが自前のメモリアロケータを提供するようにしておけば、ソースフィルタの出力ピンはレンダラーフィルタとのデータのやりとりに自前のメモリアロケータが用意したメモリを使うようになる(かもしれない)。かもしれないと書いたのは必ずしもレンダラーフィルタの入力ピンが提供するメモリアロケータが使われるとは限らないからだ。従って自前のメモリアロケータが使われなかったときの事も考慮してプログラムする必要がある。

以下のコードは自前の入力ピント自前のメモリアロケータを備えたレンダラーフィルタの簡単な例だ。ここではDIBSectionに直接書き込ませるのではなく、多めのバッファを提供するメモリアロケータを用意した。レンダラーに渡されたサンプルオブジェクトをそのまま保持してしまおうという作戦だ。

8 最後に

DirectShowの柔軟な設計のおかげで、各部をお好みにカスタマイズして目的を達成できた。

ここで示したコードはあくまで動作を検証するためのテスト用である。このような低品質のコードを実際に使用してはならない。ここで得られた成果を元に再利用可能なライブラリを設計すべきだ。

9 参考文献

  1. DirectX 8.0 SDK ヘルプ(日本語)
  2. DirectX 8.1 SDK ヘルプ(英語)
  3. Microsoft DirectShow フィルタ グラフからデータを取得する方法, Eric Rudolph, 2001.

上へ

Last modified: Mon May 06 18:19:47 2002