相変わらず前回に引き続いてです。いったん終わった段階でまとめを作ります。
で、実装その3です。今回は内部的な動きですのでMediaFoundationを使うときのインターフェイスの動きを見ることができます。
ちょっとしたトリックもありますので注意してください。
まずサポート関数の前にとあるデータの定義
static const MEDIACONTENTTYPE g_avMediaType[] = { { TEXT("mp4"), L"video/mp4" }, { TEXT("wmv"), L"video/x-ms-wmv" }, { TEXT("avi"), L"video/x-msvideo" }, { NULL, NULL } }; #ifdef _UNICODE #define _istlead(c) 0 #define _isttrail(c) 0 #else #define _istlead(c) _ismbblead(c) #define _isttrail(c) _ismbbtrail(c) #endif //_UNICODE
というわけでデータ定義です。これを次で使います。
おそらく左側と右側のデータの関連についてはわかると思いますが・・・。
UNICODEの為の互換処理設定も面倒ですね。
ByteStreamにContentTypeを設定する
//ByteStreamにContentTypeを設定する HRESULT CMFSession::SetByteStreamContentType(const TCHAR *lpFileName,const TCHAR *lpContentType) { IMFAttributes *lpAttributes = NULL; const MEDIACONTENTTYPE *lpMediaType; const TCHAR *lp,*lpLastPath,*lpLastExt; HRESULT hr; do{ CheckPointer(m_lpByteStream,E_POINTER); FAILED_BREAK(hr,m_lpByteStream->QueryInterface(IID_PPV_ARGS(&lpAttributes))); if(lpContentType != NULL){ #ifdef _UNICODE lpAttributes->SetString(MF_BYTESTREAM_CONTENT_TYPE,lpContentType); #else WCHAR wszContentType[64]; MultiByteToWideChar(CP_ACP,0,lpContentType,-1,wszContentType,sizeof(wszContentType) / sizeof(WCHAR)); lpAttributes->SetString(MF_BYTESTREAM_CONTENT_TYPE,wszContentType); #endif //_UNICODE hr = S_OK; break; } for(lp = lpLastPath = lpLastExt = lpFileName;*lp != TEXT('\0');lp++){ if(_istlead(*lp) && _isttrail(*(lp + 1))){ lp++; } else if(*lp == TEXT('\\') || *lp == TEXT('/')){ lpLastPath = lp; } else if(*lp == TEXT('.')){ lpLastExt = lp; } } if(lpLastPath >= lpLastExt){ hr = S_FALSE; break; } lpLastExt++; for(lpMediaType = g_avMediaType,hr = S_FALSE;lpMediaType->ext != NULL;lpMediaType++){ if(_tcsicmp(lpMediaType->ext,lpLastExt) == 0){ hr = lpAttributes->SetString(MF_BYTESTREAM_CONTENT_TYPE,lpMediaType->type); break; } } } while(0); if(lpAttributes != NULL){ lpAttributes->Release(); } return hr; }
ByteStreamにContentTypeを設定します。まずこの関数の動きですが、
- ByteStreamからIMFAttributes(各オブジェクトが保持している属性値管理)を取得する
- ContentTypeが指定されているときはそれをByteStreamに設定して終了
- ファイル名から拡張子部分を検索。見つからなければ設定しない
- 拡張子とContentTypeの対応リスト内から検索。見つかったときはそのContentTypeを設定する
となります。で、何でこんなものが必要かというとMediaFoundation管理クラス実装その1で見せたロード関数内の
FAILED_BREAK(hr,lpSourceResolver->CreateObjectFromByteStream(m_lpByteStream,NULL,MF_RESOLUTION_MEDIASOURCE,NULL,&objtype,&lpSource));
に問題があります。
このCreateObjectFromByteStreamで読み込みを行う場合、IMFSourceResolverにByteStreamが保持しているファイルの種類を伝えないと読み込めません。
このファイルの種類がContentTypeになります。判別手順としては
- 第二引数のpwszURLがNULLではないとき、これの拡張子を判別してContentTypeを取得
- 第二引数がNULLのとき、IMFByteStreamに設定されているContentType取得してこれをSourceのContentTypeとする
となります。面倒なのはこのContentTypeが何者か?という問題だったわけです。
で、何のことか探すのに非常に苦労しました。初めはDirectShowなどと同じようにGUIDかと思いきや全然違いましたし。
結論から言うと、MIMEタイプだったわけです。Attributeの設定で属性値型が「Wide-character string」からもしや・・・と思い。
というわけで、この属性値に対象のByteStreamがロードしているMIME値を引き渡せば成功します。
たとえばWindowsMediaVideo(.wmv)では「video/x-ms-wmv」を設定すればいいわけです。ひどいですね・・・。
おそらくカスタムされたスプリッタを使うときはそれに接続できるように適当なMIMEタイプが必要になるんでしょうね・・・。(テストしていないので想像)
TopologyNodeの作成
//ソース部のトポロジーノードを作成する HRESULT CMFSession::CreateSourceNode(IMFPresentationDescriptor *lpPresentDesc,IMFStreamDescriptor *lpStreamDesc,IMFTopologyNode **lplpNode) { IMFTopologyNode *lpNode = NULL; HRESULT hr; do{ //SourceStream用のノードを作成 FAILED_BREAK(hr,MFCreateTopologyNode(MF_TOPOLOGY_SOURCESTREAM_NODE,&lpNode)); //必要な属性を設定する FAILED_BREAK(hr,lpNode->SetUnknown(MF_TOPONODE_SOURCE,m_lpMediaSource)); FAILED_BREAK(hr,lpNode->SetUnknown(MF_TOPONODE_PRESENTATION_DESCRIPTOR,lpPresentDesc)); FAILED_BREAK(hr,lpNode->SetUnknown(MF_TOPONODE_STREAM_DESCRIPTOR,lpStreamDesc)); //作成が完了したので戻す *lplpNode = lpNode; lpNode = NULL; hr = S_OK; } while(0); if(lpNode != NULL){ lpNode->Release(); } return hr; } //出力部のトポロジーノードを作成する HRESULT CMFSession::CreateOutputNode(IMFStreamDescriptor *lpStreamDesc,IMFTopologyNode **lplpNode) { IMFTopologyNode *lpNode = NULL; IMFMediaTypeHandler *lpHandler = NULL; IMFActivate *lpActivate = NULL; GUID guidMajorType; DWORD dwStreamID; HRESULT hr; do{ memset(&guidMajorType,0x00,sizeof(guidMajorType)); //ストリームのIDを取得 lpStreamDesc->GetStreamIdentifier(&dwStreamID); //ストリームのメデイア種別を取得 FAILED_BREAK(hr,lpStreamDesc->GetMediaTypeHandler(&lpHandler)); FAILED_BREAK(hr,lpHandler->GetMajorType(&guidMajorType)); //出力部のトポロジーノードを作成 FAILED_BREAK(hr,MFCreateTopologyNode(MF_TOPOLOGY_OUTPUT_NODE,&lpNode)); //レンダラを作成 if(IsEqualGUID(guidMajorType,MFMediaType_Audio)){ //オーディオレンダラを作成 FAILED_BREAK(hr,MFCreateAudioRendererActivate(&lpActivate)); m_nStatusCode |= MFSSTATUS_HASAUDIOLINE; } else if(IsEqualGUID(guidMajorType,MFMediaType_Video)){ //ビデオレンダラを作成 FAILED_BREAK(hr,MFCreateVideoRendererActivate(m_hWnd,&lpActivate)); m_nStatusCode |= MFSSTATUS_HASVIDEOLINE; } //出力できる種別でないときは失敗する else{ hr = E_FAIL; break; } //ノードに出力を設定する FAILED_BREAK(hr,lpNode->SetObject(lpActivate)); //作成が完了したので戻す *lplpNode = lpNode; lpNode = NULL; hr = S_OK; } while(0); if(lpActivate != NULL){ lpActivate->Release(); } if(lpHandler != NULL){ lpHandler->Release(); } if(lpNode != NULL){ lpNode->Release(); } return S_OK; }
この辺はサンプルとほぼ同じです。gotoが無いので見やすくなっているはずです。
ちなみに、この場合のTopologyはDirectShowのGraphとほぼ同一の意味で、TopologyNodeはDirectShowのFilterとほぼ同一の意味と思えばいいと思います。
つまるところ、DirectShowでSourceFilterとRendererFilterを作っているのとあまり変わりはありません。
入力部の場合はこれ以上変える必要はないです。自前作成時はここではなくIMFByteStreamを実装する形で動作させることができるからです。
出力部の場合はレンダラをシステムから貸してもらうのでヘルパを使って作成しています。
自作のレンダラを追加するときはこのコードの作成部に割り込ませることで出力を変更することができます。
内部再生位置設定
HRESULT CMFSession::InnerSeekTime(MFTIME nTime) { if(!InnerTestStatus(MFSSTATUS_LOAD)) return E_ABORT; if(nTime < 0){ nTime = 0; } else if((uint64_t)nTime >= m_mtMediaLength){ nTime = m_mtMediaLength; } PROPVARIANT varStart; varStart.vt = VT_I8; varStart.hVal.QuadPart = nTime; m_lpMediaSession->Start(NULL,&varStart); if(!InnerTestStatus(MFSSTATUS_PLAY)){ m_lpMediaSession->Stop(); } else if(InnerTestStatus(MFSSTATUS_PAUSE)){ m_lpMediaSession->Pause(); } return S_OK; }
ただの再生位置移動ですが、停止しているときに再生位置を移動させると問題があるので注意してください。
再生命令時に再生開始位置を変更できるのを利用して位置を伝達しているだけのコードです。
内部音量設定
//ボリュームを設定する HRESULT CMFSession::InnerSetVolume(void) { float *lpfVolumes; uint32_t i,nChannels; CheckPointer(m_lpAudioVolume,E_FAIL); m_lpAudioVolume->GetChannelCount(&nChannels); lpfVolumes = (float *)_malloca(sizeof(float) * nChannels); for(i = 0;i < nChannels;i++){ if(i & 0x01){ lpfVolumes[i] = (m_fAudioBalance >= 0.0f ? 1.0f : (1.0f + m_fAudioBalance)) * m_fAudioVolume; } else{ lpfVolumes[i] = (m_fAudioBalance <= 0.0f ? 1.0f : (1.0f - m_fAudioBalance)) * m_fAudioVolume; } } if(nChannels & 0x01){ lpfVolumes[nChannels - 1] = m_fAudioVolume; } m_lpAudioVolume->SetAllVolumes(nChannels,lpfVolumes); _freea(lpfVolumes); return S_OK; }
このコードも簡易で組んでいていろいろと前提条件が必要です。
この場合の前提条件とは
- 音の出力チャンネルは奇数チャンネル(配列番号では偶数)が左側、偶数チャンネル(配列番号では奇数)が右側にある
- 出力チャンネル数が奇数であるときは最後がセンターにあるものとしている
となります。前提条件が正しいかどうかは本来ならWAVEFORMATEX構造体を取得して調べないとだめなのですが面倒ですので。
古いコードと違ってマルチチャンネルを前提としているためチャンネル数の取得、個別に音量を設定ができます。
ちなみに、このコードではSetAllVolumesを使って音量を設定していますが、forで回しながら各チャンネルごとに設定をしてもかまいません。
その他サポート関数
//ビデオ有効テスト bool CMFSession::IsEnableVideo(void) const { return InnerTestStatus(MFSSTATUS_LOAD | MFSSTATUS_HASVIDEOLINE); } //サウンド有効テスト bool CMFSession::IsEnableAudio(void) const { return InnerTestStatus(MFSSTATUS_LOAD | MFSSTATUS_HASAUDIOLINE); } //内部的なステータステスト bool CMFSession::InnerTestStatus(uint32_t nStatus) const { return (m_nStatusCode & nStatus) == nStatus; } //ミリ秒をメディアタイムに変換する uint64_t CMFSession::MilliSecondToMediaTime(uint32_t t) { return (uint64_t)t * (1000 * 10); } //メディアタイムをミリ秒に変換する uint32_t CMFSession::MediaTimeToMilliSecond(uint64_t t) { return (uint32_t)(t / (1000 * 10)); }
ただのフラグテストと単位系の変換だけです。
単位系の変換はクラスのメンバ変数を必要としないのでstaticで定義しています。
残ったのはMediaFoundationから受け取るイベントの処理
難しいものが残りましたね~。これを実装してやっとメディアを扱うクラスとして動くことができるようになります。