ちなみに初心者向けにやっているものではないので初心者的なことを知りたい人は別のページに行ってくださいませ。ここでは変に深いところ(とくにゲームに関連する技術)を重点的にやっています。
今回はループ再生を含む再生制御です。動画をループで再生することはあまり多くないですが、背景画を動画で扱ってループさせるならあり得るでしょうし。
今回の開始点は前回のプログラムで、
//再生位置の変更を行うタイムフォーマットをメディアタイム(100nsを1とする単位系)にする hr = m_lpMediaSeeking->SetTimeFormat(&TIME_FORMAT_MEDIA_TIME); //メディアを再生する hr = m_lpMediaControl->Run(); //ここでいろいろな動作をする ...(a) //再生を停止する hr = m_lpMediaControl->Stop();
の…(a)に当たる部分を示していきたいと思います。
DirectShowの時間管理
まず、DirectShowで時間管理を行うときに出てくる変な時間のがいくつかあります。
データ型 | 定義元型 | 説明 |
---|---|---|
REFERENCE_TIME | __int64 | 基準時間型(100nsを1とする単位系)での時間を表す |
LONGLONG | __int64 | 対象のタイムフォーマットでの時間を表す |
REFTIME | double | 1秒を1.0とする単位系での時間を表す |
全部64bit型です。この辺がDirectShowの時間の細かさを示す数値でもあります。
ま、とりあえず全体の時間を取得してみましょう。時間を取得するときはIMediaSeekingにあるGetDurationを使って
LONGLONG llDuration; hr = m_lpMediaSeeking->GetDuration(&llDuration);
で時間が取得できます。が、この時間は「現在設定している時間単位系でこのメディアがどのくらいの長さがあるか」ですので注意です。
このサンプルで…(a)の場所にコードが来たときにはすでにSetTimeFormatが成功しているはずですので、その場合はメディアタイムで取得できることになります。
つまり、llDurationはこのメディアの時間を100nsを1とする値で取得できることになります。この場合に限りREFERENCE_TIMEと同じ意味である、といえるわけです。
ちなみに、このままだとちょっと扱いづらい(1ms単位への変換など)と思うときはヘルパクラスであるCRefTimeを使うと簡単になります。
これで時間を取得できればあとはIMediaSeekingのSetPositionsを使ってシーク処理を行うことができるので万々歳ですね。再生終了時に先頭に戻して再度再生、と。
再生終了検知
お次は、どうやって再生が終了したことを検知するかですが、これはIMediaEventExを使って処理します。まず、IMediaEventにイベントが発生したときにイベントプロシージャに通知するように仕掛けを行います。
#define WM_DIRECTSHOWMESSAGE (WM_APP + 1) m_lpMediaEventEx->SetNotifyWindow((OAHWND)hWnd,WM_DIRECTSHOWMESSAGE,(LPARAM)NULL);
をおこなうと、グラフ(DirectShowで再生しているオブジェクト)に何かイベントが発生すると、hWndのウィンドウにメッセージが送られます。後はそれを見て処理を行えばOKです。
LRESULT CALLBACK WndProc(HWND hWnd,UINT uMsg,WPARAM wParam,LPARAM lParam) { switch(uMsg){ ... case WM_DIRECTSHOWMESSAGE: HandleEvent(); break; ... } return DefWindowProc(hWnd,uMsg,wParam,lParam); }
おきまりのルーチンですね。
あとは、IMediaEventExからイベントを取得してそれが再生完了であったときに再度再生を行えばOKです。再生完了かどうかを判定するコードは
BOOL bIsComplete; long lEventCode; LONG_PTR lEvParam1,lEvParam2; bIsComplete = FALSE; do{ //イベントを取得 hr = m_lpMediaEventEx->GetEvent(&lEventCode,&lEvParam1,&lEvParam2,0); if(hr == S_OK){ //再生終了であったときフラグを立てる if(lEventCode == EC_COMPLETE) bIsComplete = TRUE; //イベントを削除 m_lpMediaEventEx->FreeEventParams(lEventCode,lEvParam1,lEvParam2); } } while(hr == S_OK); //再生終了のとき if(bIsComplete){ ...(b) }
do~whileがあるのは、再生停止以外のイベントがイベントキューの中にあるときにそれを無視するための構文です。実際にはそのイベントも意味があることがあるのですが、
今回は単純なループ再生を目的とするので無視しましょう。また、説明によってはGetEventの第二引数と第三引数はlong *と記述してあるものもあると思いますが、
これはLONG_PTR *が正しいです。ポインタが与えられることもあるイベントなので64bit拡張が必要です。
停止、そしてループ
で、残った…(b)ですが、この処理は簡単だと思います。単純に先頭に戻して再度再生させるだけです。
が、シーク処理を行う前に一度グラフを停止してしまう方が安全です。というよりはグラフは自動的にシーク処理をする前にグラフを一度ポーズ状態に「勝手にする」ので、無駄な作業を行わないように
完全に停止する命令を挟んだ方がいいと思います。というわけで、この部分のコードです。
//レンダラを完全に停止させる m_lpMediaControl->Stop(); //スレッドとしてグラフを完全に停止させるためタイムスライスをゆずる //Sleep(0); //先頭に移動 LONGLONG llAbsoluteTime = 0; hr = m_lpMediaSeeking->SetPositions(&llAbsoluteTime,AM_SEEKING_AbsolutePositioning,NULL,AM_SEEKING_NoPositioning); //再度再生 hr = m_lpMediaControl->Run();
Sleep命令はあったほうがいいですが、必ずしもいるかどうかは私もよくわかりません。
で、これで再度再生命令を行うと再生終了時にまたイベントが発生して・・・を繰り返すことでループ再生を行うことができます。
というわけでループ再生まで簡単にやってみました。ちなみに、やろうと思えばこのやり方を使うことでDirectSoundを使わずに
DirectShowだけでゲームのサウンドを再生といった荒技もできるのでやってみたい人はそういう検討もあってもいいかもです。
次はどれがいいでしょうか?
+アーカイブからのデータ読み出しのサポート
+サーフェイスへのレンダリング処理
+COMオブジェクトの自前実装例
・・・たぶん最後を先にやらないと以降そういうことが多くなるのでわかりにくくはなるのですが、前者2つは最後の項目を知らなくても一定量できるので
先に知った方がやりやすいかもしれないですね。