というわけで実装その1です。前回からの続きです。
今回の実装は全体で使用する補助コードおよびコンストラクタ、デストラクタ、IUnknownの実装、読み込み、解放の処理を行います。
それ以外は次回に。
まずは全体で使用する補助コードから
//ロードされたメディア種別を列挙する typedef struct _mediacontenttype{ const TCHAR *ext; const WCHAR *type; } MEDIACONTENTTYPE; //ステータスコード #define MFSSTATUS_NONE 0x00000000 #define MFSSTATUS_LOAD 0x00000001 #define MFSSTATUS_PLAY 0x00000002 #define MFSSTATUS_PAUSE 0x00000004 #define MFSSTATUS_LOOP 0x00000008 #define MFSSTATUS_HASAUDIOLINE 0x00000100 #define MFSSTATUS_HASVIDEOLINE 0x00000200 //失敗時のGoto処理 #define FAILED_BREAK(hr,x) { (hr) = (x); if(FAILED(hr)){ break; } }
特にこの中でよく使うのがFAILED_BREAKというマクロです。早い話が処理が失敗したときにbreakを行います。
これはC言語やC++ではgotoを基本的に使わない(制御構造で対応する)のが普通ですが、COMのように「戻り値は基本的にHRESULT値であり、失敗したときは処理を継続しない方がよい」と決まっている場合はこういうマクロを持ってくるといいです。
本当ならばそれぞれの処理をモジュールとして分離する方がよいのですが、面倒だとこんなことをします。
使い方としては、ループがなく、breakが使える制御構造(do~while(0))を出してその中でgotoもどきとして使用します。
コンストラクタ、デストラクタ
CMFSession::CMFSession(HWND hWnd) : CUnknownBase(NULL), m_hWnd(hWnd), m_nStatusCode(MFSSTATUS_NONE), m_mtMediaLength(0), m_fAudioVolume(1.0f), m_fAudioBalance(0.0f), m_lpMediaSession(NULL), m_lpByteStream(NULL), m_lpMediaSource(NULL), m_lpPresentationClock(NULL), m_lpVideoDisplay(NULL), m_lpAudioVolume(NULL) { NonDelegatingAddRef(); } //デストラクタ CMFSession::~CMFSession() { ReleaseMovie(); }
コンストラクタではただ単に変数を初期化しているだけです。オーディオのボリュームは0.0(消音)~1.0(最大)、バランスは-1.0(左)~1.0(右)としています。
ロードが別のポイントにあるので一応この段階でオブジェクトを有効にするために内部参照カウントを有効にしています。
デストラクタは解放関数を呼び出しているだけですね。
IUnknownの機能の実装
//インターフェイスの要求 STDMETHODIMP CMFSession::NonDelegatingQueryInterface(REFIID riid,void **ppv) { if(IsEqualIID(riid,IID_IMFAsyncCallback)) return GetInterface(static_cast<IMFAsyncCallback *>(this),ppv); else return CUnknownBase::NonDelegatingQueryInterface(riid,ppv); }
管理クラスにはIMFAsyncCallbackを実装しているのでそれをQueryInterfaceで返す必要があります。
そのほかは上位に任せてしまっています。
読み込み動作
//メディアを読み込む HRESULT CMFSession::LoadMovie(const TCHAR *lpFileName) { IMFByteStream *lpByteStream = NULL; IMFSourceResolver *lpSourceResolver = NULL; IUnknown *lpSource = NULL; IMFTopology *lpTopology = NULL; IMFPresentationDescriptor *lpPresentDesc = NULL; IMFClock *lpClock = NULL; #ifdef _UNICODE const WCHAR *wszFileName; #else WCHAR wszFileName[4096]; #endif MF_OBJECT_TYPE objtype; DWORD count; HRESULT hr; //すでに読み込まれているときは解放を行う if(InnerTestStatus(MFSSTATUS_LOAD)){ ReleaseMovie(); } do{ //各オブジェクトの初期化 m_nStatusCode = MFSSTATUS_NONE; //ファイル名をTCHARからWCHARへと変換する #ifdef _UNICODE wszFileName = lpFileName; #else MultiByteToWideChar(CP_ACP,0,lpFileName,-1,wszFileName,sizeof(wszFileName) / sizeof(WCHAR)); #endif //MediaSessionの作成 FAILED_BREAK(hr,MFCreateMediaSession(NULL,&m_lpMediaSession)); //イベント取得を自分自身に割り当て FAILED_BREAK(hr,m_lpMediaSession->BeginGetEvent(static_cast<IMFAsyncCallback *>(this),NULL)); //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)); //SourceResolverの作成 FAILED_BREAK(hr,MFCreateSourceResolver(&lpSourceResolver)); //IMFMediaSourceの作成 objtype = MF_OBJECT_INVALID; FAILED_BREAK(hr,lpSourceResolver->CreateObjectFromByteStream(m_lpByteStream,NULL,MF_RESOLUTION_MEDIASOURCE,NULL,&objtype,&lpSource)); FAILED_BREAK(hr,lpSource->QueryInterface(IID_PPV_ARGS(&m_lpMediaSource))); //IMFTopologyの作成 FAILED_BREAK(hr,MFCreateTopology(&lpTopology)); //MediaSourceからPresentationDescriptionを取得する FAILED_BREAK(hr,m_lpMediaSource->CreatePresentationDescriptor(&lpPresentDesc)); //Descriptionの数を取得 FAILED_BREAK(hr,lpPresentDesc->GetStreamDescriptorCount(&count)); //取得された各Streamに対してトポロジーの状態を割り当てる for(DWORD i = 0;i < count;i++){ IMFStreamDescriptor *lpStreamDesc = NULL; IMFTopologyNode *lpSourceNode = NULL,*lpOutputNode = NULL; BOOL bIsSelected = FALSE; do{ //StreamDescriptionを取得 FAILED_BREAK(hr,lpPresentDesc->GetStreamDescriptorByIndex(i,&bIsSelected,&lpStreamDesc)); //対象のストリームが使われないときは作業を行わない if(!bIsSelected) break; //対象のストリームに対するNodeを作成 FAILED_BREAK(hr,CreateSourceNode(lpPresentDesc,lpStreamDesc,&lpSourceNode)); FAILED_BREAK(hr,CreateOutputNode(lpStreamDesc,&lpOutputNode)); //各ノードをトポロジーに登録 FAILED_BREAK(hr,lpTopology->AddNode(lpSourceNode)); FAILED_BREAK(hr,lpTopology->AddNode(lpOutputNode)); //ノードを接続 FAILED_BREAK(hr,lpSourceNode->ConnectOutput(0,lpOutputNode,0)); //対象のストリームの処理完了 hr = S_OK; } while(0); if(lpOutputNode != NULL){ lpOutputNode->Release(); } if(lpSourceNode != NULL){ lpSourceNode->Release(); } if(lpStreamDesc != NULL){ lpStreamDesc->Release(); } if(FAILED(hr)) break; } //トポロジーの処理に失敗したときは失敗する if(FAILED(hr)) break; //トポロジーをセッションに設定 FAILED_BREAK(hr,m_lpMediaSession->SetTopology(0,lpTopology)); //クロックを取得 FAILED_BREAK(hr,m_lpMediaSession->GetClock(&lpClock)); FAILED_BREAK(hr,lpClock->QueryInterface(IID_PPV_ARGS(&m_lpPresentationClock))); //メディア長を取得 FAILED_BREAK(hr,lpPresentDesc->GetUINT64(MF_PD_DURATION,&m_mtMediaLength)); //セッション作成完了 m_fAudioVolume = 1.0f; m_fAudioBalance = 0.0f; m_nStatusCode |= MFSSTATUS_LOAD; hr = S_OK; } while(0); //使用したインターフェイスを解放 if(lpClock != NULL){ lpClock->Release(); } if(lpPresentDesc != NULL){ lpPresentDesc->Release(); } if(lpTopology != NULL){ lpTopology->Release(); } if(lpSource != NULL){ lpSource->Release(); } if(lpSourceResolver != NULL){ lpSourceResolver->Release(); } if(lpByteStream != NULL){ lpByteStream->Release(); } return hr; }
一応手順としては
- MediaSessionを作成、初期化する
- ファイルに関連づけられたByteStreamの作成
- ByteStreamを利用するSource部の作成
- Sourceから出力される各ストリームの状態を取得
- 接続状態を管理するTopologyの作成
- Topology内にSourceから出力されたストリームを受け入れるOutputを作成、接続
- Topologyの状態をMediaSessionに割り当ててメディアの再生状態を確定
- メディアの必要な情報を取得
- 内部変数を初期化
を行います。もうこれに関してはDirectShowと同じように「こういう流れである」としてください。
相変わらず面倒なのは「どれが何を持っているのかがわかりづらい」というところですか。
あと、この読み込み処理内ではビデオ部の管理を行うIMFVideoDisplayControlやオーディオ部の音量管理を行うIMFAudioStreamVolumeは取得できません。
なぜかというと、この段階ではあくまでTopologyの状態を「設定するように命令した」だけで、ここから非同期でMediaSession内で再生ができるように準備作業を行います。
このため、実は「ロードした直後にビデオのサイズを取得する、といったことができない」ということだったりします。
これらをどこで取得するかはこの後やります。
もうひとつ。Microsoftのサンプルコードを見たことがある人なら思うことですが、ファイル名を指定して読み込むだけならIMFByteStreamを使う必要はありません。
この場合はByteStreamの初期化~IMFSourceResolverへの読み込みの部分をCreateObjectFromURL関数により一発でできます。
ただ、ByteStreamを使うことで自前のアーカイブ内からデータを読み取るときのコードと共通化しやすいためこの形式を使っています。
ByteStreamを使うときには基本的に後で実装を紹介しますが、SetByteStreamContentTypeのような処理が必要になりますので注意です。
(これを用意しないときはCreateObjectFromByteStreamの第二引数にファイル名を渡さないと処理が失敗します)
最後に、以降のコードでも出てきますが、COMでQueryInterfaceを使うときにサポートマクロがあります。
今回の場合はIID_PPV_ARGSというもので、取得したいインターフェイスを直接宣言したポインタを渡すとIIDを内部参照してくれる、というものです。
実はDirectShowでも使えたのですが、前に記事を書いたときには知らなかったのですよね・・・。
ただしDirectXGraphicsやDirectSoundなどでは使用できないこともあるので注意です。
どうでもいいですが、非UNICODEでこのコードを実行するとファイル名の文字列が・・・と突っ込みたくなりますが、その辺は置いておきましょう。
デストラクタ
//メディアを解放する BOOL CMFSession::ReleaseMovie(void) { HRESULT hr; if(m_lpMediaSession != NULL){ if(m_lpVideoDisplay != NULL){ m_lpVideoDisplay->Release(); m_lpVideoDisplay = NULL; } if(m_lpAudioVolume != NULL){ m_lpAudioVolume->Release(); m_lpAudioVolume = NULL; } hr = m_lpMediaSession->Close(); if(FAILED(hr)) return FALSE; } if(m_lpMediaSource != NULL){ m_lpMediaSource->Shutdown(); } if(m_lpMediaSession != NULL){ m_lpMediaSession->Shutdown(); } if(m_lpPresentationClock != NULL){ m_lpPresentationClock->Release(); m_lpPresentationClock = NULL; } if(m_lpMediaSource != NULL){ m_lpMediaSource->Release(); m_lpMediaSource = NULL; } if(m_lpByteStream != NULL){ m_lpByteStream->Release(); m_lpByteStream = NULL; } if(m_lpMediaSession != NULL){ m_lpMediaSession->Release(); m_lpMediaSession = NULL; } m_nStatusCode = MFSSTATUS_NONE; return TRUE; }
単にインターフェイスを解放しているだけです。
IMFVideoDisplayControlおよびIMFAudioStreamVolumeはIMFMediaSessionが初期化できていないと取得できないのでこんな書き方になっています。
面倒なのはClose=>Shutdown=>Releaseの手順を踏まなければならないことでしょうか。
解放前に再生していたときはCloseを行った段階で強制的に停止しますので気にせずに。
次は動画再生を外から制御するコードを見る
こちらは意外と簡単な実装で済みます。単にprotected部の関数に処理を委譲しているだけだったりします。