カテゴリー別アーカイブ: MediaFoundation

MediaFoundationで自前描画に挑戦してみた

だから日本語の資料が少なすぎる上にDirectShowにおけるbaseclassesに相当するサポートクラス群がないものだから動きが全く分からないのなんの。これじゃDirectShowの発展系としてMediaFoundationで実装する人って出ないのではないでしょうか?と思えるほどの状態です。それでも挑んでみるのが私なのでやってみるだけやってみました。

 

DirectShowにおける「CBaseVideoRendererを使ったレンダリング」に近いことができればとりあえずOK

これが目標です。というのも、MovieLayerPlayerで描画を行うためにはウィンドウ経由の描画に頼らないで描画する必要があるからです。そのため、いったんプログラム側で管理しているサーフェイスなりテクスチャなりに一度転送してそこから・・・というシーケンスになります。

今回はこれを実現しそうなサンプルとして「EVRPresenter」を選択してこれを発展させる形で実装に挑んでみました。この選択が吉と出るか凶と出るか、時間の無駄と出るか・・・。

 

EVRPresenterとは

通常、MediaFoundationでウィンドウへの描画には「Enhanced Video Renderer」を使うことになります。これはWindowsVista以降で使用可能となるレンダラで、DirectShowのVideoMixingRenderer系を拡張したレンダラになっています。また、WindowsVista以降であればDirectShow側でもフィルタとして登録されていますのでこれを使って描画することができます。使い勝手自体はVideoMixingRendererよりたぶんいいとは思いますが、WindowsXP以前では使用できませんので(今となっては余り意味がありませんが)注意です。

で、このEnhanced Video Rendererの「プレゼント処理」を乗っ取る形で自前実装を行っているサンプルがEVRPresenterになります。その前の色変換処理などはすべてレンダラに任せて最終結果だけを得られる形になっています。ただし、元々Enhanced VideoRendererがVideoMixingRendererの考え方を踏襲して作られたような格好をしているので動きそのものはVideoMixingRenderer9とかなり似ていますし、Direct3D9(かそれ以上)が必要となるのでそれがない時は基本的に使えません。この制約が最終的に効いてきます。

 

で、実装してみた

基本的にはサンプルを見ながら必要な動きを奪ってきて自分が持っている各インターフェイスに振り分ける作業をしています。実装に必要になるのは主に

  • サンプルプール(ビデオイメージを入れておくサーフェイスのバッファ処理)
  • スケジューラ(指定時間でサンプルを描画するように指示する)
  • プレゼンタ(EnhancedVideoRendererの描画結果を受け取る)

の3つとなります。最後のが大きすぎる(EVRに連結させるためにかなりの数のインターフェイスが必要になるため)のが厳しいところです。このあたりはMediaFoundationのインターフェイスを実装したい場合のライブラリを公開していないから、というのが大きいです。実際、DirectShowではサンプルプールやスケジューラやプレゼンタの大多数の機能はbaseclasses経由で公開されているのでCBaseVideoRendererのように簡単に使えるのが大きいです。

今回はあくまで「サーフェイスに描画するところまで」とするのが目的なのでプレゼント処理は行いません。これでもそれなりに動くようになるまではかなり苦労を強いられました。一部の機能を実現するために下位機能の呼び出しに制約がありますのでその辺を中心にちょこっと。

 

内部で使用するDirectXGraphicsはDirect3D9Ex以上でないと大変なことに

元からMediaFoundationはWindowsVista以上(つまりDesktopWindowManager下で動作すること)を前提としているのでDirectXGraphics側もDirect3D9Exの状態でないとかなり困る事態が発生しやすいです。たとえばEVRの出力サーフェイスはレンダーターゲットでなければならない(サンプルではスワップチェーンのサーフェイスを指していますが今回はサーフェイスでないと意味がない)ので再生中に何らかの都合でデバイスをリセットしたくてもDirect3D9のままではD3DPOOL_DEFAULTのサーフェイスを取られたままになるためリセットできません。これはちょっと痛すぎです。Direct3D9Exであればこの制約がないので突破できたり。VideoMixingRenderer9時代にも似たような現象がありましたがここにも現れるのか・・・というところです。

また、EVRのフィルタはIDirect3DDeviceManager9(もしくはIMFDXGIDeviceManager)を返さないと動作しないのでこのインターフェイスを作成しないとだめなのですが、この処理がまた渋いのなんの。Direct3DDeviceを複数またいで使用させる時に妙なロックが必要になったり、これのせいでMovieLayerPlayer側に変なデッドロックを作ってしまったような感じで。DirectXをビデオデコーダーのアクセラレーションに使うのは分かりますが、ちょっと厳しいのではないですか?と。

おかげで自分が持っているDirect3D9を使った描画ライブラリにDirect3D9Exも含めて実装するようにしました。

スケジューラの実装が不安定なような・・・

気がするんです。テストで作ったプログラムでは発生しないのですが、MovieLayerPlayerへの組み込みテストではかなりの確率(50%に近いくらい)で読み込み=>再生を行うとフレーム遅延が大量に発生する描画となってしまいます。しかもこのときはウィンドウサイズの変更を行うと遅延していたはずのフレームが一気に描画されて再生位置が追いつく、という現象が現れていまいち。ウィンドウサイズの変更でDirect3D9Exもリセットがかかるのでその辺との兼ね合いの可能性も大きいような気がしますが・・・。

この現象についてどうにかしないとさすがに公開レベルにはならない、ということで今のところ内部テストだけになっています。スケジューラだけをデバッグすればいいはずなのですが、その場合はバグっている様子がなく。やっかいですね。

そして各インターフェイス間の動きが分かりづらい

DirectShowのフィルタ実装の時もたぶん同じことを書いたり思ったりしています。何がどこでどういう順序で呼ばれるのかがわかりづらい。実装して動かしてみることで何となく動きはつかめてきましたし、インターフェイスが何のためにあるのかもようやく分かり始めてきたかな、というレベルです。このあたりも英語の資料しかないのがかなり効いています。まあいまさらWindowsのプログラムだけにこだわる意味なんてありませんからね、というところでしょうか。

ちなみにEVRPresenterを使ってMediaFoundationで直接再生するのも手順が分かりづらいです。DirectShowのように「AddFilterでフィルタを追加してRenderFile」とは行かないですしね・・・。このあたりも一応解説しておきましょう。EVRPresenterクラスをプログラム内で直接使うには以下のようにします。blog内の記事から管理クラス内部実装その3からビデオレンダラを作成しているところに処理を突っ込みます。

  1. EVRPresenterのインスタンスを作成
  2. MFCreateVideoRendererでEVRを含んだレンダラをIMFMediaSinkとして作成
  3. IMFVideoRendererをIMFMediaSinkから取り出してEVRPresenterを設定
  4. IMFMediaSinkからGetStreamSinkByIndexでIMFStreamSinkを取得
  5. IMFStreamSinkはIMFActivateと並んでIMFTopologyNodeで接続できるオブジェクトなのでこれをSetObjectで設定

となります。これ、どうやって接続させればよいか悩んだんですよね・・・。SetObjectで設定できるのはIMFActivateとIMFStreamSinkなのは分かっていましたがそれがどこから手に入るのかやっぱり分からない、というのは厳しすぎです。

ちなみにこのシーケンスのおかげでIMFMediaSinkがDirectShowのIBaseFilter、IMFStreamSinkがDirectShowのIPinに近い役目を持っていることが分かったので次に完全自前実装をやろうとした時にかなり役立つ情報となってくれるはずです。

初期化のタイミングがロード直後でないのが使いづらい

MediaFoundationの実装時にも示しましたが、ロードが完了した直後は再生できる状態ではありません。その後非同期で各接続を行うためで、これが非常にやっかいです。逆に言うとその処理が終わるまで正確な情報が分からない(たとえばビデオの画像サイズが不明)のでその情報が分かるまで待機しなければならない、とかMediaFoundationで各インターフェイスを作って接続をしようとしたらその段階でエラーとなり再生できない、などがあります。(実際、AdvancedCodecsを入れてある私の環境でmp4を再生させようとしたらMediaFoundation経由だと読み込んだ後接続できない状態に。入っていないなら普通に成功したのですが)

この辺も示したサンプルからかなり修正を加えてロード完了通知を送るようにしたりと工夫することになりました。

 

でも描画がバグったら使い物にならないだろう

結局「一定確率でフレームがどんどん遅延していく」現象の謎が未だ解けずお蔵入りしそうな気配となっています。おそらくスケジューラの状態がおかしいかEVRとプログラム本体側のDirect3DDevice9との兼ね合いが良くないのでしょうね。DirectShowの時も結局VideoMixingRenderer9を使う実装をしてみたものの不安定すぎてお蔵入りにしたで似ていると言えば似ている末路になりそうです。

だからといってCBaseVideoRenderer並の実装はさすがに面倒です。この実装のおかげでたぶん今であれば一週間がんばれば実装できるとは思いますが趣味レベルでこれをやるのはちょっと躊躇するところです。他の人が使うので仕事として依頼、くらいならやる価値は十分にあると思います。これができればWindowsのADVエンジンでDirectShowからMediaFoundationへの移行が十分視野に入ると思います。

ちなみにどうでもいいこととしてDirect3D9Exを使うと案外簡単にウィンドウサイズ変更が実装できるのでもしかするとWindowModePatchにウィンドウサイズの自由変更を実装するかもしれません。暇だったらですが。

 

MediaFoundationを使う 記事まとめ

というわけで、自分で書きためてきたものを10日前後毎日書いてみたわけですが・・・。

個人的にはおもしろい面とおもしろくなかった面がありました。

まずはまとめリンクから

  • 実装前の説明編
  • 管理クラス編
  • IMFByteStreamを使ったカスタムロード編
  • 番外編(IUnknownを実装した基本クラス)

これでとりあえず一通り

記事として書いたものを出してみました。自分としては「長かったな~」というところです。

ここまで書いたならblogではなく適当なページを作ってそこでやった方が見栄えはいいのですが、

  • プログラムコードの表記を整えるのにblogのプラグインを使った方が楽
  • ページを更新しても更新情報がWeb上に出回るまでのラグが大きすぎる

などという理由で却下です。

一応プログラムの解説っぽいページの構成になっているはずです。

連載中に起こった出来事に関してコメントがしづらいのが欠点か

iPhoneやらがNTT Docomoで発売されることが決定したり、高校生クイズがあったり・・・。

途中で雑記記事を入れるとリンクの連続性が破壊される(記事を開いたときに左右移動で次の項目に移らない)のが問題ですね。

雑記用にblogを作るほど雑記用の記事があるわけでもなし、さらに言うなら分けてもすぐにリンクがくるわけでもなし、ですね。

本来ならVPSを借りてそこでblog(WordPressあたり?)を複数個適当に展開してそれぞれにDNSを割り振ればまあそれはそれで・・・なのですが。

この連載を書いている最中に思わなくもなかった考えですね。

次はXAudioをやった後でUnityですかね

XAudioは個人的にもやっておきたい実装です。特にとあるADVシステムでDirectSoundとXAudioが選択できるようになっているので実装する価値ありとの判断です。

DirectSoundもだいぶ古いシステムとなっていますからね。動画周りをMediaFoundationで固めるなっら音周りはXAudioで固めるのが筋ですか。

こうやってWinXP時代の古いDirectX系の実装から離れて新設計のAPIを使っていくんですね。

古いから悪いわけではないですが、提供元からのサポートが切れるのが困る訳なので。

あとはUnityですか。iOSやAndroidなどのスマートフォン関係とWindows、Mac関連のデスクトップ関係で共通のゲームシステムを作るなら良さそうな感じです。

まあ、これに関してもたぶん適当に見てやれば私ならできるんでしょうが、面倒なので放置しておいたので・・・。

また。iOSやらAndroidやらもスマートフォン関係のアプリを開発するならMacOSが必須と思ったほうがいいのですが、手元にないのでどこかから手に入れてこないと問題のようです。

中古のMac miniあたりを探してきて前にあったiMacのごとくVNCとBluetoothキーボードで遠隔開発ができる状態をまた作らないとだめかな~と思います。

AppleのDeveloperへの登録やらXcodeやらをそろえるにさらに金はかかりますが仕事を受けられない&実装速度がわからないよりは、と思うと一種の自己投資と思うことにします。

問題はしばらく資格試験を連続で入れてしまったことでしょうか。

いかに半分以上ニートだからといっても勉強が必要です。こちらも仕事になるので必要ですが、資格を取っておくのもこの後重要だと思っています。

仕事そのものは資格試験があろうが無かろうが普通にこなしますが。

MediaFoundationを使う (8) IMFByteStreamを実装してみる 後編

前編からの続きです。

これでMediaFoundation系はとりあえず終了です。IMFByteStreamを実装してみてそれを使ってみます。

コンストラクタ、デストラクタ

//コンストラクタ
CMFByteStream::CMFByteStream(HRESULT *lphr,const TCHAR *lpFileName)
	: CUnknownBase(NULL), m_lpFileName(NULL), m_hFileStream(NULL), m_ullStreamPos(0), m_ullStreamSize(0), m_hAsyncThread(NULL), m_hThreadEvent(NULL), m_lpAttributes(NULL)
{
	HRESULT hr;
	::InitializeCriticalSection(&m_vCSStream);
	::InitializeCriticalSection(&m_vCSQueue);
	if(lpFileName != NULL){
		m_lpFileName = new TCHAR[lstrlen(lpFileName) + 1];
		_tcscpy(m_lpFileName,lpFileName);
	}
	hr = MFCreateAttributes(&m_lpAttributes,0);
	NonDelegatingAddRef();
	if(lphr != NULL){ *lphr = hr; }
}
//デストラクタ
CMFByteStream::~CMFByteStream()
{
	Close();
	if(m_lpAttributes != NULL){
		m_lpAttributes->Release();
		m_lpAttributes = NULL;
	}
	if(m_lpFileName != NULL){
		delete[] m_lpFileName;
		m_lpFileName = NULL;
	}
	::DeleteCriticalSection(&m_vCSQueue);
	::DeleteCriticalSection(&m_vCSStream);
}

初期化シーケンスは基本そのままです。コンストラクタで内部オブジェクトを順次初期化、デストラクタでデータを閉じた上内部オブジェクトの解放を行っています。

ちなみにIMFAttributesだけはヘルパ関数でオブジェクトを作成しています。第二引数は初期データサイズなのですが、0でも大丈夫のようです。

IUnknown系動作の実装

//インターフェイスの要求
STDMETHODIMP CMFByteStream::NonDelegatingQueryInterface(REFIID riid,void **ppv)
{
	if(IsEqualIID(riid,IID_IMFByteStream)) return GetInterface(static_cast<IMFByteStream *>(this),ppv);
	else if(IsEqualIID(riid,IID_IMFAttributes)) return GetInterface(m_lpAttributes,ppv);
	return CUnknownBase::NonDelegatingQueryInterface(riid,ppv);
}

IMFByteStreamを要求されたときは自分自身を渡します。

また、IMFAttributesを要求されたときには持っている内部オブジェクトを返しておきます。

これがないと使用時にいろいろと大変なことになります。内部では使わないんですけれどもね・・・。

ファイルオープン、クローズ

//ストリームを開く
HRESULT CMFByteStream::Open(void)
{
	unsigned int threadid;
	if(IsValidStream()) return E_ABORT;
	m_hFileStream = ::CreateFile(m_lpFileName,GENERIC_READ,FILE_SHARE_READ,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);
	if(m_hFileStream == INVALID_HANDLE_VALUE){ m_hFileStream = NULL; return E_FAIL; }
	if(!::GetFileSizeEx(m_hFileStream,(LARGE_INTEGER *)&m_ullStreamSize)) return E_FAIL;
	m_hThreadEvent = ::CreateEvent(NULL,FALSE,FALSE,NULL);
	if(m_hThreadEvent == NULL) return E_FAIL;
	m_hAsyncThread = (HANDLE)_beginthreadex(NULL,0,&CMFByteStream::AsyncThread,this,0,&threadid);
	if(m_hAsyncThread == NULL){ ::CloseHandle(m_hThreadEvent); m_hThreadEvent = NULL; return E_FAIL; }
	return S_OK;
}
//ストリームを閉じる
STDMETHODIMP CMFByteStream::Close(void)
{
	dequeAsyncItem::iterator it,itend;
	{
		CLockQueue lock(this);
		for(it = m_dequeWorkItem.begin(),itend = m_dequeWorkItem.end();it != itend;++it){ it->lpResult->Release(); }
		for(it = m_dequeDoneItem.begin(),itend = m_dequeDoneItem.end();it != itend;++it){ it->lpResult->Release(); }
		m_dequeWorkItem.clear(); m_dequeDoneItem.clear();
	}
	if(IsValidThread()){
		::SetEvent(m_hThreadEvent);
		::WaitForSingleObject(m_hAsyncThread,INFINITE);
		::CloseHandle(m_hThreadEvent);
		::CloseHandle(m_hAsyncThread);
		m_hThreadEvent = NULL; m_hAsyncThread = NULL;
	}
	if(IsValidStream()){
		CLockStream lock(this);
		::CloseHandle(m_hFileStream);
		m_ullStreamPos = m_ullStreamSize = 0;
		m_hFileStream = NULL;
	}
	return S_OK;
}

ファイルの非同期処理の形を見たまま書いています。スレッドの作成手順も見たまま。

閉じる方も手順は見たままです。スレッドを止める手順も見たまま。

非同期アイテムキューの片付けだけが面倒なくらいですか。

属性値の取得、設定

//ストリームの能力を取得する
STDMETHODIMP CMFByteStream::GetCapabilities(DWORD *pdwCapabilities)
{
	CheckPointer(pdwCapabilities,E_POINTER);
	if(!IsValidStream()) return E_HANDLE;
	*pdwCapabilities = MFBYTESTREAM_IS_READABLE | MFBYTESTREAM_IS_SEEKABLE;
	return S_OK;
}
//ストリームのサイズを取得する
STDMETHODIMP CMFByteStream::GetLength(QWORD *pqwLength)
{
	CheckPointer(pqwLength,E_POINTER);
	*pqwLength = m_ullStreamSize;
	return S_OK;
}
//ストリームのサイズを設定する
STDMETHODIMP CMFByteStream::SetLength(QWORD qwLength)
{
	return E_NOTIMPL;
}
//ストリームの位置を取得する
STDMETHODIMP CMFByteStream::GetCurrentPosition(QWORD *pqwPosition)
{
	CheckPointer(pqwPosition,E_POINTER);
	*pqwPosition = m_ullStreamPos;
	return S_OK;
}
//ストリームの位置を設定する
STDMETHODIMP CMFByteStream::SetCurrentPosition(QWORD qwPosition)
{
	if(qwPosition > m_ullStreamSize) return E_INVALIDARG;
	return Seek(msoBegin,qwPosition,0,NULL);
}
//ストリームの終端かどうか
STDMETHODIMP CMFByteStream::IsEndOfStream(BOOL *pfEndOfStream)
{
	CheckPointer(pfEndOfStream,E_POINTER);
	*pfEndOfStream = m_ullStreamPos >= m_ullStreamSize ? TRUE : FALSE;
	return S_OK;
}

データ位置取得時にはロックを置いていませんが、置いても意味はないです。

この辺は単に内部の状態を返すだけなので持っている値を返して完了です。唯一SetLengthは書き込み時しか動作する必要がないでE_NOTIMPLを返しています。

書き込み処理

//同期書き込みを行う
STDMETHODIMP CMFByteStream::Write(const BYTE *pb,ULONG cb,ULONG *pcbWritten)
{
	return E_NOTIMPL;
}
//非同期書き込みを開始する
STDMETHODIMP CMFByteStream::BeginWrite(const BYTE *pb,ULONG cb,IMFAsyncCallback *pCallback,IUnknown *punkState)
{
	return E_NOTIMPL;
}
//非同期書き込みを終了する
STDMETHODIMP CMFByteStream::EndWrite(IMFAsyncResult *pResult,ULONG *pcbWritten)
{
	return E_NOTIMPL;
}
//書き込みバッファのフラッシュを行う
STDMETHODIMP CMFByteStream::Flush(void)
{
	return E_NOTIMPL;
}

実装する必要がないのでE_NOTIMPLで終わりです。IStreamの実装と考え方は同じです。

データカーソル位置の変更

//ストリーム位置を移動する
STDMETHODIMP CMFByteStream::Seek(MFBYTESTREAM_SEEK_ORIGIN SeekOrigin,LONGLONG llSeekOffset,DWORD dwSeekFlags,QWORD *pqwCurrentPosition)
{
	int64_t llNewPos;
	CLockStream lock(this);
	switch(SeekOrigin){
	case msoBegin: llNewPos = llSeekOffset; break;
	case msoCurrent: llNewPos = (int64_t)m_ullStreamPos + llSeekOffset; break;
	default: return E_INVALIDARG;
	}
	if(llNewPos < 0){ llNewPos = 0; } else if((uint64_t)llNewPos > m_ullStreamSize){ llNewPos = m_ullStreamSize; }
	m_ullStreamPos = llNewPos;
	if(pqwCurrentPosition != NULL){ *pqwCurrentPosition = m_ullStreamPos; }
	return S_OK;
}

今回はさすがにロックが必要ですね。ほとんど見たまんまの実装です。

dwSeekFlagは今回の場合見る必要はないので無視しています。

読み込み処理

//同期読み取りを行う
STDMETHODIMP CMFByteStream::Read(BYTE *pb,ULONG cb,ULONG *pcbRead)
{
	CheckPointer(pb,E_POINTER); CheckPointer(pcbRead,E_POINTER);
	return InnerRead(pb,cb,pcbRead,m_ullStreamPos);
}
//非同期読み取りを開始する
STDMETHODIMP CMFByteStream::BeginRead(BYTE *pb,ULONG cb,IMFAsyncCallback *pCallback,IUnknown *punkState)
{
	ASYNCITEM item; HRESULT hr;
	CheckPointer(pb,E_POINTER); CheckPointer(pCallback,E_POINTER);
	hr = MFCreateAsyncResult(NULL,pCallback,punkState,&item.lpResult);
	if(FAILED(hr)) return hr;
	item.pbBuffer = pb; item.cbSize = cb; item.cbRead = 0; item.nReadPos = m_ullStreamPos;
	CLockQueue lock(this);
	m_dequeWorkItem.push_back(item);
	::SetEvent(m_hThreadEvent);
	return S_OK;
}
//非同期読み取りを終了する
STDMETHODIMP CMFByteStream::EndRead(IMFAsyncResult *pResult,ULONG *pcbRead)
{
	CheckPointer(pResult,E_POINTER); CheckPointer(pcbRead,E_POINTER);
	dequeAsyncItem::iterator it,itend; HRESULT hr;
	CLockQueue lock(this);
	for(it = m_dequeDoneItem.begin(),itend = m_dequeDoneItem.end(),hr = E_FAIL;it != itend;++it){
		if(it->lpResult == pResult){
			*pcbRead = it->cbRead;
			m_dequeDoneItem.erase(it);
			pResult->Release(); hr = S_OK;
			break;
		}
	}
	return hr;
}
//内部読み込み処理
HRESULT CMFByteStream::InnerRead(BYTE *pb,ULONG cb,ULONG *pcbRead,uint64_t nReadPos)
{
	if(!IsValidStream()) return E_HANDLE;
	CLockStream lock(this);
	if(!::SetFilePointerEx(m_hFileStream,*((LARGE_INTEGER *)&nReadPos),NULL,FILE_BEGIN)){ return E_FAIL; }
	if(!::ReadFile(m_hFileStream,pb,cb,pcbRead,NULL)) return E_FAIL;
	m_ullStreamPos = nReadPos + *pcbRead;
	return S_OK;
}

同期読み取りは内部読み取りにそのまま渡しているだけです。

非同期読み取りの場合は、読み取りの開始側はIMFAsyncResultをヘルパ関数により生成してそれを読み取り要求アイテムとともに非同期処理スレッドに回しています。

終了側は終了したアイテム内から同じIMFAsyncResultを持つアイテムを探索して読み取られたバイト数を戻しています。

ちなみにdequeではなくlistにしたわけはこのEndReadの呼び出しが終了した順に呼び出されるかが不定のためです。

まあ、システムが実装しているMediaFoundationからの読み取りなら終了した順になるのですが・・・。

非同期処理スレッド

//非同期処理スレッド
unsigned int __stdcall CMFByteStream::AsyncThread(void *lpContext)
{
	ASYNCITEM item; CMFByteStream *lpThis; HRESULT hr; DWORD dw;
	
	lpThis = (CMFByteStream *)lpContext;
	while(1){
		dw = WaitForSingleObject(lpThis->m_hThreadEvent,INFINITE);
		if(dw != WAIT_OBJECT_0 || lpThis->m_dequeWorkItem.empty()) break;
		while(!lpThis->m_dequeWorkItem.empty()){
			{
				CLockQueue lock(lpThis);
				if(lpThis->m_dequeWorkItem.empty()) break;
				item = lpThis->m_dequeWorkItem.front();
				lpThis->m_dequeWorkItem.pop_front();
			}
			hr = lpThis->InnerRead(item.pbBuffer,item.cbSize,&item.cbRead,item.nReadPos);
			if(FAILED(hr)){ item.lpResult->Release(); break; }
			{
				CLockQueue lock(lpThis);
				lpThis->m_dequeDoneItem.push_back(item);
			}
			
			hr = MFInvokeCallback(item.lpResult);
		}
	}
	return 0;
}

非同期処理そのものは普通です。イベントが入ったときに未処理アイテムから状態を取得して読み取ります。

で、読み取り処理が完了すると、ヘルパ関数であるMFInvokeCallbackを使ってIMFAsyncCallback内のInvokeを呼び出します。

通常はInvokeを呼び出すときはIMFAsyncResultをMFCreateAsyncResultで作成してMFInvokeCallbackで呼び出す必要があります。

ちなみにヘルパだから単に呼び出しているだけかと思えばさにあらず。

このヘルパ関数はMediaFoundationが持っているWorkQueueにInvokeの発行を非同期で依頼する、という動作をとります。

これによりInvokeは実装側スレッドでもなくMediaFoundationのコアスレッドでもなく、非同期処理専用のスレッドから呼ばれるため安全な非同期となる、という形になります。

使用してみる

MediaSessionの初期化でByteStreamを使用しているコードがありましたが、これを修正します。

修正前は

//ByteStreamの作成
FAILED_BREAK(hr,MFCreateFile(MF_ACCESSMODE_READ,MF_OPENMODE_FAIL_IF_NOT_EXIST,MF_FILEFLAGS_NONE,wszFileName,&m_lpByteStream));
FAILED_BREAK(hr,SetByteStreamContentType(lpFileName,NULL));

としていましたが、これを

//ByteStreamの作成
CByteStream *lpByteStream = new CMFByteStream(&hr,lpFileName);
if(FAILED(hr)) break;
hr = lpByteStream->Open();
if(FAILED(hr)){ lpByteStream->Release(); break; }
hr = lpByteStream->QueryInterface(IID_PPV_ARGS(&m_lpByteStream)); lpByteStream->Release();
if(FAILED(hr)) break;
FAILED_BREAK(hr,SetByteStreamContentType(lpFileName,NULL));

とすることでこちらで実装したIMFByteStreamが使用できるようになります。

なお、IMFAttributesをQueryInterfaceで渡す実装を忘れるとSetByteStreamContentTypeが失敗します。

こういう見落としがたま~にあるので気をつけましょう。

というわけで、長々と書いてきたMediaFoundationネタもいったん終了

これでADVゲームでもまあ使えるかな~というところでしょうか。

なお、見てわかるとおりSource部分だけはMediaFoundationの方が実装しやすいです。

DirectShowだとSourceFilterの実装となるのでフィルタの実装、ピンの実装、IAsyncReaderの実装とコード量はかなり多くなります。

(DirectShowBaseClassesからCBaseFilterを引っ張ってくるなどすればまだましではあるが)

ただ残りの部分の実装は難しいです。こちらは暇があるときにでもまた紹介できるかな・・・?

MediaFoundationを使う (7) IMFByteStreamを実装してみる 前編

まだこの話題を続けます。なんかここまで書くとどこかの雑誌などでちゃんとした連載記事を書きたくなるのは気のせいでしょうか・・・。

で、やりやすい話題として持ってきました。IMFByteStreamを実装してみます。

そもそもIMFByteStreamとは

まず実装対象となっているIMFByteStreamについて確認しておきましょう。

IMFByteStreamとは、MediaFoundationでバイト単位のデータを扱うときに使用するインターフェイスです。

一応読み込み、書き込みの両方をサポートしており、非同期読み取り、同期読み取りの両方をサポートしています。

DirectShowのIAsyncReaderに書き込みのサポートを追加したもの、ともいえます。

ちょっと非同期処理が甘いような気がする

IAsyncReaderに比べて非同期の扱いがかなり甘いと思います。

というのも、非同期読み取りの開始を行うときに開始バイト位置を指定しません。

位置は(読み取り開始の前に位置決めがない場合)前回の処理が完了した時点から、となっています。

本当の非同期ならどのタイミングで読まれるかは不明なのですが、これに関しては「処理は非同期処理を発行した順番に行われる」が守られていることを前提としているようです。

ちなみに、MediaFoundationの非同期処理はどのインターフェイスでもだいたいこんな流れになっています。

  1. 対象のインターフェイスに非同期処理を依頼する(Begin~)。このとき、IMFAsyncCallbackを渡して処理完了後にコールバックしてもらう
  2. 非同期処理をインターフェイス内で行う。この処理は依頼された順序を守って行うこと。(順序を変更してはならない)
  3. 非同期処理が完了した後、依頼時に渡されたIMFAsyncCallbackにあるInvokeを呼び出して終了したことを通知する。このとき、非同期処理を行った側はIMFAsyncResultを内部で生成してそれをInvokeで渡す必要がある。ただし、Invokeはほとんどの場合MediaFoundationが持つ非同期処理キュー上から発行する必要がある
  4. IMFAsyncCallback側は非同期処理が完了したことを検知するとその結果を保存する。その後、非同期処理完了を確認したことを依頼先に通知する(End~)
  5. 非同期処理側は終了確認通知時(End~呼び出し時)に終了時に必要な情報を引き渡す。これで処理が完全に完了する

というわけで定義

今回の実装ではWinAPIの各処理(CreateFile,ReadFile,SetFilePointerEx,CloseHandle)を使った実装を行います。

自前のアーカイブなどからデータを読み取るときにはそれぞれの部分を置き換えるように実装すると良いと思います。

#ifndef __mfbytestream_h__
#define __mfbytestream_h__
class CMFByteStream : public CUnknownBase, public IMFByteStream{
public:
	CMFByteStream(HRESULT *lphr,const TCHAR *lpFileName); //コンストラクタ
	virtual ~CMFByteStream(); //デストラクタ
	//IUnknownの機能を実装する
	DECLARE_IUNKNOWN;
	STDMETHODIMP NonDelegatingQueryInterface(REFIID riid,void **ppv); //インターフェイスの要求
	//IMFByteStreamの実装
	STDMETHODIMP GetCapabilities(DWORD *pdwCapabilities); //ストリームの能力を取得する
	STDMETHODIMP GetLength(QWORD *pqwLength); //ストリームのサイズを取得する
	STDMETHODIMP SetLength(QWORD qwLength); //ストリームのサイズを設定する
	STDMETHODIMP GetCurrentPosition(QWORD *pqwPosition); //ストリームの位置を取得する
	STDMETHODIMP SetCurrentPosition(QWORD qwPosition); //ストリームの位置を設定する
	STDMETHODIMP IsEndOfStream(BOOL *pfEndOfStream); //ストリームの終端かどうか
	STDMETHODIMP Read(BYTE *pb,ULONG cb,ULONG *pcbRead); //同期読み取りを行う
	STDMETHODIMP BeginRead(BYTE *pb,ULONG cb,IMFAsyncCallback *pCallback,IUnknown *punkState); //非同期読み取りを開始する
	STDMETHODIMP EndRead(IMFAsyncResult *pResult,ULONG *pcbRead); //非同期読み取りを終了する
	STDMETHODIMP Write(const BYTE *pb,ULONG cb,ULONG *pcbWritten); //同期書き込みを行う
	STDMETHODIMP BeginWrite(const BYTE *pb,ULONG cb,IMFAsyncCallback *pCallback,IUnknown *punkState); //非同期書き込みを開始する
	STDMETHODIMP EndWrite(IMFAsyncResult *pResult,ULONG *pcbWritten); //非同期書き込みを終了する
	STDMETHODIMP Seek(MFBYTESTREAM_SEEK_ORIGIN SeekOrigin,LONGLONG llSeekOffset,DWORD dwSeekFlags,QWORD *pqwCurrentPosition); //ストリーム位置を移動する
	STDMETHODIMP Flush(void); //書き込みバッファのフラッシュを行う
	STDMETHODIMP Close(void); //ストリームを閉じる
	HRESULT Open(void); //ストリームを開く
protected:
	//内部ロックオブジェクト
	class CLockStream{
	public:
		CLockStream(CMFByteStream *obj) : m_obj(obj){ ::EnterCriticalSection(&obj->m_vCSStream); }
		~CLockStream() { ::LeaveCriticalSection(&m_obj->m_vCSStream); }
	private:
		CMFByteStream *m_obj;
	};
	class CLockQueue{
	public:
		CLockQueue(CMFByteStream *obj) : m_obj(obj){ ::EnterCriticalSection(&obj->m_vCSQueue); }
		~CLockQueue() { ::LeaveCriticalSection(&m_obj->m_vCSQueue); }
	private:
		CMFByteStream *m_obj;
	};
	//非同期処理アイテムの定義
	typedef struct _AsyncItem{
		IMFAsyncResult *lpResult;
		BYTE *pbBuffer;
		ULONG cbSize,cbRead;
		uint64_t nReadPos;
	} ASYNCITEM;
	typedef std::list<ASYNCITEM> dequeAsyncItem;
	HRESULT InnerRead(BYTE *pb,ULONG cb,ULONG *pcbRead,uint64_t nReadPos); //内部読み込み処理
	static unsigned int __stdcall AsyncThread(void *lpContext); //非同期処理スレッド
	inline bool IsValidStream(void) const { return m_hFileStream != NULL; } //ストリームが有効かどうか
	inline bool IsValidThread(void) const { return m_hAsyncThread != NULL; } //スレッドが有効かどうか
	
private:
	TCHAR *m_lpFileName; //ファイル名
	HANDLE m_hFileStream; //ファイルハンドル
	uint64_t m_ullStreamPos; //ストリーム位置
	uint64_t m_ullStreamSize; //ストリームサイズ
	CRITICAL_SECTION m_vCSStream; //ストリーム同期ハンドル
	HANDLE m_hAsyncThread; //非同期処理スレッド
	HANDLE m_hThreadEvent; //スレッドイベント
	CRITICAL_SECTION m_vCSQueue; //キューアイテム同期ハンドル
	dequeAsyncItem m_dequeWorkItem; //未処理アイテムキュー
	dequeAsyncItem m_dequeDoneItem; //処理済みアイテムキュー
	IMFAttributes *m_lpAttributes; //属性値管理
};
#endif //__mfbytestream_h__

ちなみに非同期アイテムを管理しているキューはdequeではなくlistを使っています。

実装を始めたときには実はdequeだったのですが、これだととある時に処理速度が問題になる可能性に至りlistを使っています。

動作テストをしたときにはdequeでも大丈夫だろう、という結論はあるのですがそのままにしてあります。

後、書き込み系については実装を行いません。前にIStreamの実装でWrite部を実装しなかったように今回も実装する必要はありません。

書き込み部の実装が必要になるとしたらせいぜいメディアキャプチャを使うときくらいだと思います。

実装部はちょっと大きめ

いろいろと実装するので説明が大変です。IStreamの時とほぼ同じですが同期処理を行う必要があるので書くことが多いです。

MediaFoundationを使う (6) MediaFoundation管理クラス実装その4

前回からの続きです。管理クラスの実装はこれで最後です。記事そのものはもう少し続きますが。

今回は使ってみるコードも出しておきます。この実装は非常に面倒、というよりMediaFoundationがどういう動作をしているかある程度知らないと対応するコードが書きづらいです。

IMFAsyncCallbackの実装

MediaFoundationが非同期動作時に処理が完了したときに呼び出す処理を実装します。

つまりMediaFoundationに処理を依頼したときに処理結果を受け取る為のものです。

//内部パラメータを取得する
STDMETHODIMP CMFSession::GetParameters(DWORD *pdwFlags,DWORD *pdwQueue)
{
	return E_NOTIMPL;
}
//非同期処理が行われたときのコールバック
STDMETHODIMP CMFSession::Invoke(IMFAsyncResult *pAsyncResult)
{
	IMFMediaEvent *lpMediaEvent; MediaEventType type; HRESULT hr;
	do{
		lpMediaEvent = NULL;
		//イベントキューからイベントを取得
		FAILED_BREAK(hr,m_lpMediaSession->EndGetEvent(pAsyncResult,&lpMediaEvent));
		FAILED_BREAK(hr,lpMediaEvent->GetType(&type));
		//セッション終了でないときは再度イベント取得を有効にする
		if(type != MESessionClosed){ m_lpMediaSession->BeginGetEvent(static_cast<IMFAsyncCallback *>(this),NULL); }
		//セッションが有効であるときはイベントをウィンドウプロシージャから発行してもらう
		if(m_lpMediaSession != NULL){
			::PostMessage(m_hWnd,WM_MFSNOTIFY,(WPARAM)this,(LPARAM)lpMediaEvent);
			lpMediaEvent = NULL;
		}
		hr = S_OK;
	} while(0);
	if(lpMediaEvent != NULL){ lpMediaEvent->Release(); }
	return hr;
}

GetParameterについては今回は実装する必要がありません。一応WorkQueueに関する処理が必要なときに実装しますが、使いません。

InvokeはMediaFoundationが処理が完了したときのコールバック処理を行います。

が、ここで具体的に処理を行うべきではありません。

なぜかというと、このInvokeを呼び出しているのは実装側のスレッドでもなくMediaFoundationのスレッドでも厳密にはありません。

これを呼び出すのはMediaFoundationが非同期コールバックを呼び出すため「だけ」に作ったスレッドから呼び出されています。

(正確には非同期コールバックなど非同期処理をMediaFoundationが行うために用意された作業スレッド上から)

そのため、その中で処理を行うと

  • 処理時間が長引くとMediaFoundationの非同期処理に影響を与える
  • MediaFoundationの処理と競合する恐れがある

ことから、一度実装者側のスレッドへ動作を移すべき、と言うことになります。(理由は想定ですが、たぶん正しいでしょう)

そのため、コールバック時に渡されたイベント情報を有効にしたまま一度ウィンドウプロシージャに投げてしまうのがいいわけです。

こうすると、ウィンドウプロシージャは実装側のスレッド(おそらくメインスレッド)で動きますのでMediaFoundationとしては文句なしとなります。

ウィンドウプロシージャにはwParamに自身のアドレス、lParamにIMFMediaEventのアドレスを渡しておきます。

なお、セッション終了だけは例外で、これはIMFMediaSessionの解放前に行われるためイベント処理は行うべきではないわけです。

再描画処理

//再描画を行う
BOOL CMFSession::Repaint(void)
{
	if(m_lpVideoDisplay == NULL) return FALSE;
	m_lpVideoDisplay->RepaintVideo();
	return TRUE;
}

一応WM_PAINTが発行したときにこれを呼び出す「べき」とされています。

実際にはウィンドウのサイズと同一のビデオを持つメディアであれば呼び出さなくてもそこまで影響はなかったですが、「べき」ですので。

本来のイベント処理

//イベント処理発行
BOOL CMFSession::HandleEvent(LPARAM lParam)
{
	IUnknown *lpUnknown; IMFMediaEvent *lpMediaEvent = NULL; MF_TOPOSTATUS topostatus; MediaEventType type; HRESULT hrStatus,hr;
	
	if(lParam == NULL) return FALSE;
	do{
		lpUnknown = (IUnknown *)lParam;
		FAILED_BREAK(hr,lpUnknown->QueryInterface(IID_PPV_ARGS(&lpMediaEvent)));
		FAILED_BREAK(hr,lpMediaEvent->GetType(&type));
		FAILED_BREAK(hr,lpMediaEvent->GetStatus(&hrStatus));
		if(FAILED(hrStatus)){ hr = hrStatus; break; }
		switch(type){
		case MESessionTopologyStatus:
			FAILED_BREAK(hr,lpMediaEvent->GetUINT32(MF_EVENT_TOPOLOGY_STATUS,(UINT32 *)&topostatus));
			switch(topostatus){
			case MF_TOPOSTATUS_READY:
				{
					IMFGetService *lpGetService = NULL;
					
					do{
						FAILED_BREAK(hr,m_lpMediaSession->QueryInterface(IID_PPV_ARGS(&lpGetService)));
						if(InnerTestStatus(MFSSTATUS_HASVIDEOLINE)){ FAILED_BREAK(hr,lpGetService->GetService(MR_VIDEO_RENDER_SERVICE,IID_PPV_ARGS(&m_lpVideoDisplay))); }
						if(InnerTestStatus(MFSSTATUS_HASAUDIOLINE)){ FAILED_BREAK(hr,lpGetService->GetService(MR_STREAM_VOLUME_SERVICE,IID_PPV_ARGS(&m_lpAudioVolume))); }
					} while(0);
					if(lpGetService != NULL){ lpGetService->Release(); }
				} break;
			} break;
		case MEEndOfPresentation:
			if(InnerTestStatus(MFSSTATUS_LOOP)){
				PROPVARIANT varStart;
				varStart.vt = VT_I8;
				varStart.hVal.QuadPart = 0;
				m_lpMediaSession->Start(NULL,&varStart);
			}
			else{ m_nStatusCode &= ~(MFSSTATUS_PLAY | MFSSTATUS_PAUSE); }
			hr = S_OK;
			break;
		default: hr = S_OK;
		}
	} while(0);
	if(lpMediaEvent != NULL){ lpMediaEvent->Release(); }
	if(lpUnknown != NULL){ lpUnknown->Release(); }
	return SUCCEEDED(hr);
}

HandleEventはウィンドウプロシージャにWM_MFSNOTIFYが投げられたときに呼ばれることを前提に実装します。

IMFMediaEventを取得するまでにちょっと遠回りをしていますが、IMFMediaEventに直接キャストをしても問題はあまりないです。

ここでは具体的に2つのイベントに関して処理をしていて、それがMESessionTopologyStatusとMEEndOfPresentationです。

MESessionTopologyStatusはセッションに設定されているTopologyの状態が変化したときに呼び出されるイベントです。

このとき、Topologyの準備ができた(メディアの再生準備が完全に完了した)ときにMediaSessionから取得できるメディア出力に関する各サービスが取得できるようになります。

このタイミングで初めてIMFVideoDisplayControlやIMFAudioStreamVolumeが取得できるのでとっておきましょう。

MEEndOfPresentationはそのまんまでプレゼンテーションが終了した、つまりメディアが終端まで再生したというイベントです。

ループ再生時には再度先頭かあら再生すればいいですし、ループしないときは再生状態を打ち切ってしまえばいいわけです。

このように直接MediaSessionの状態を変更する恐れがあるのでInvoke内ではなくウィンドウプロシージャから呼び出してもらった方がいい、というわけですね。

このほかにも様々なイベントがありますので必要に応じてここで処理を実装すればいいと思います。

で、簡単に使用するコードを

CMFSession *g_lpSession = NULL;
LRESULT CALLBACK WndProc(HWND hWnd,UINT uMsg,WPARAM wParam,LPARAM lParam);
int WINAPI _tWinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,LPTSTR lpCmdLine,int nShowCmd)
{
	HWND hMainWnd; HRESULT hr;
・・
	MFStartup(MF_VERSION);
	g_lpSession = new CMFSession(hMainWnd);
	hr = g_lpSession->LoadMovie(TEXT("op.wmv"));
	hr = g_lpSession->PlayMovie(FALSE);
・・
	//g_lpSession->Release;
	g_lpSession->ReleaseMovie();
	delete g_lpSession;
	g_lpSession = NULL;
	MFShutdown();
・・
}
LRESULT CALLBACK WndProc(HWND hWnd,UINT uMsg,WPARAM wParam,LPARAM lParam)
{
	switch(uMsg){
	case WM_MFSNOTIFY:
		if(wParam != NULL){ ((CMFSession *)wParam)->HandleEvent(lParam); }
		return 0;
	case WM_PAINT:
	{
		PAINTSTRUCT ps; HDC hdc;
		hdc = BeginPaint(hWnd,&ps);
		・・・
		EndPaint(hWnd,&ps);
		if(g_lpSession != NULL){ g_lpSession->Repaint(); }
	} break;
	・・・
	}
	return DefWindowProc(hWnd,uMsg,wParam,lParam);
}

作成時はnewで作ります。解放するときはReleaseMovie->deleteもしくはReleaseを使います。

また、ウィンドウプロシージャにWM_MFSNOTIFYが送られてきたときは対象のオブジェクトのHandleEventを呼び出します。

再生はnewの時に与えたウィンドウハンドル上でビデオが再生されます。

とりあえず今までのコードをつなげれば動くはず

つなげるのも一苦労ですが。コード部をがんばってコピペすればMediaFoundationを使えます。

ADVゲームで使うにはまだまだ苦労が必要です。一応残りはByteStreamを自前で実装して自前のアーカイブファイルから読み出せるようにしてみます。

ここまで実装すればまあADVゲームの実装でDirectShowの代わりにはなるでしょうか。