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の代わりにはなるでしょうか。

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

  1. tanaka

    はじめまして
    最近、Media Foundationの勉強を始めました。
    本記事を参考に実装しているのですが、
    下記の実装の部分でビルド時にエラーが発生し苦戦しています。

    if(InnerTestStatus(MFSSTATUS_HASVIDEOLINE)){ FAILED_BREAK(hr,lpGetService->GetService(MR_VIDEO_RENDER_SERVICE,IID_PPV_ARGS(&m_lpVideoDisplay))); }

    ■エラー
    MFSession.obj : error LNK2001: 外部シンボル “_MR_VIDEO_RENDER_SERVICE” は未解決です。
    C:\Test\System/MAIN.EXE : fatal error LNK1120: 外部参照 1 が未解決です。

    いろいろ調べてみたのですが、原因が分からず行き詰っております。
    何かお気づきの点があれば、ご助言頂きたいと思いコメントしました。
    よろしくお願いします。

    開発環境
    ———————————————-
    Windows 8.1 Pro 64bit
    Microsoft Visual Studio 2010(Ver. 10.0.40219.1 SP1)
    Microsoft Windows SDK for Visual Studio 2010(Ver. 7.0.30319)
    Directx9.0c SDK (Summer 2004)

    返信
  2. 音宮 志久 投稿作成者

    ご質問ありがとうございます。上記のエラーについて推測だけ書きます。

    上記のエラーですが、開発環境を見る限り考えられるのはstrmiids.libのバージョンの問題の可能性があります。
    というのも、VisualStudio2010付属のstrmiids.libであれば問題はないのですが、開発環境にDirectX9.0c SDKが見受けられ、その場合DirectX9.0c SDKにもstrmiids.libがあり、こちらのバージョンが古いため_MR_VIDEO_RENDER_SERVICEが定義されていないのです。
    そうすると、ビルドの設定でDirectX9.0c SDKを先に参照するようになっていた場合ですと_MR_VIDEO_RENDER_SERVICEが定義されていないstrmiids.libを参照しているのではないか、と思われます。

    開発テストを行うだけであれば一度DirectSDKへの参照を対象プロジェクトの場合のみ行わないようにする(プロジェクトのプロパティ=>構成プロパティ=>VC++ディレクトリでライブラリディレクトリからDirectXSDKのライブラリディレクトリを外す)とよいのではないでしょうか。

    ご参考になれば良いと思います。

    返信
    1. tanaka

      ご回答ありがとうございます。
      ご指摘の通り、DirectXSDKへの参照を行わないようにしたところ、エラーが発生しなくなりました。
      ありがとうございます。

      ちなみにですが、DirectXSDKと共存させたい場合はどのようにすればよいでしょうか?

      返信
      1. 音宮 志久 投稿作成者

        Summer2004を使っているとのことですのでDirectX9のプログラムを作っていると推察いたしますが、その場合は最新のDirectXSDK(といってもJune2010というものです)を入れることで解決できます。ただし、このとき旧のlibファイルが残る可能性があるのでSummer2004は完全に削除する必要があると思われます。
        実はDirectShow関係のファイルはある時期(いつだったかは記憶しておりませんが)からWindowsSDKに統合されているのでJune2010にはstrmiids.libが入っていません。そのため、今回のような問題が起こることはありません。

        この方法の注意点はd3dx9.libを連結している場合(つまりプログラム内でD3DX~という関数を使用している場合)にユーザー側にDirectXのランタイムをインストールするようにしなければならないことにあります。自分一人で開発しているときは問題ないのですが商用などでユーザーに配布するときはランタイムの必要性を書かないとユーザーが実行できなくなってしまいます。(June2010の場合はd3dx9_43.dllを使います)

        ご参考になれば良いと思います。
        (もしかするとライブラリの話については後で独立した記事に書き直すかもしれません)

  3. tanaka

    なるほど。最新版にしなければいけないのですね。
    ご助言ありがとうございます。
    試してみます。

    返信

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

この記事のトラックバック用URL