Media Foundation の Media Session というAPIを使って動画プレイヤーアプリを作ってみる。
前提知識
Node, Topology, Pipeline object - ノード、トポロジ、パイプラインオブジェクト
Media Foundation は「Node (ノード)」と呼ばれるオブジェクトを繋いでメディアを処理する。ノードの組み合わせと繋ぎ方をまとめて「Topology(トポロジ)」という。
DirectShow がフィルタを繋いでフィルタグラフを作ることに似ている。DirectShow ではフィルタのインスタンスを作成し、直接フィルタグラフに追加していましたが、Media Foundation においては、まずノードを作成し、そのノードに「Pipeline object(パイプラインオブジェクト)」を「属性(Attribute)」として設定する。
パイプラインオブジェクトには「メディアソース (Media Source)」「メディアファンデーショントランスフォーム (Media Foundation Transform ; MFT)」「メディアシンク (Media Sink)」の 3 種類がある。Media Sourceはネットワークストリームやファイルからデータを読み取り、ビデオとオーディオのストリームを生成する。MFTs はデコーダ、エンコーダ、エフェクタの役割を担っている。Media sinkは画面表示、音声再生(サウンドデバイスへの出力)、ファイルへの記録を行いる。Windows 7 ではキャプチャもサポートされるようになり、Media Sources はビデオカメラやキャプチャポードである場合もある。
Media session - メディアセッション
「Media Session (メディアセッション)」は、Topologyの制御を行うオブジェクトである。DirectShow のグラフビルダ、メディアコントロールに近い役割を担っている。
Source resolver - ソースリゾルバ
「Source resolver(ソースリゾルバ)」は、URLやバイトストリームをアプリケーションから指定すると適切なMedia sourceを作成してくれるオブジェクトである。今回はSource resoloverを使ってMedia sourceを作成する。
Topology loader - トポロジローダー
「Topology loader(トポロジローダー)」は、「部分的なトポロジ (partial topology) 」から「完全なトポロジ(full topology)」に解決してくれるオブジェクトである。
たとえば圧縮されたファイルを読み取るメディアソースと、それをレンダリングを行うMedia sinkをTopologyに置き、それらを接続すると、Topology loaderが自動的にそれら2つの間に適切なデコーダを挿入してくれる。DirectShow のインテリジェント接続のようなものですが、Topology loaderはオブジェクトになっていて、独自に実装し、使用可能になっている。
今回は Media Foundation の既定のTopology loaderを使う。
表示領域の作成
動画を表示する領域を作成する。CAtlExeModuleT::PreMessageLoop
内でウィンドウを作成する。ウィンドウクラスはCMyWindow
である。実際の開発では直接アプリケーションウィンドウに表示するのではなく子ウィンドウに表示することが多いだろう。ここではスタティックコントロールを作成し、そこに動画を表示するようにしてみる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | class CMyWindow :
public CWindowImpl<CMyWindow>,
public CComObjectRootEx<CComMultiThreadModel>
...
BEGIN_MSG_MAP(CMyWindow)
MESSAGE_HANDLER(WM_CREATE, OnCreate)
END_MSG_MAP()
CWindow m_Static;
public:
static CWndClassInfo& GetWndClassInfo()
{
static CWndClassInfo wc={
{sizeof(WNDCLASSEX), CS_HREDRAW | CS_VREDRAW,
CWindowImplBaseT::StartWindowProc,
0, 0, NULL, NULL, NULL, (HBRUSH)(COLOR_WINDOW+1),
MAKEINTRESOURCE(IDC_MF_PLAYER1),
APP_NAME, NULL},
NULL, NULL, IDC_ARROW, TRUE, 0, _T("")
};
return wc;
}
...
private:
LRESULT OnCreate(UINT, WPARAM, LPARAM, BOOL&)
{
RECT video_rect;
GetClientRect(&video_rect);
m_Static.Create(_T("STATIC"), m_hWnd
, video_rect, _T(""),
WS_VISIBLE | WS_CHILD | SS_GRAYRECT);
return 0;
}
|
Media Session を使ってTopology を構築する
Media Session を作成する
Media Session は IMFMediaSession
で表され、MFCreateMediaSession
で作成する。
1 2 3 4 5 | CComPtr<IMFMediaSession> m_MFSession; // メンバ変数
CHResult hr;
// MediaSession を準備する。
hr=MFCreateMediaSession(NULL, &m_MFSession);
hr=m_MFSession->BeginGetEvent(this, NULL);
|
Source resolverを使ってMedia sourceを作成する
MFCreateSourceResolver
を呼び、Source resolverを作成する。次にCreateObjectFromURL
でURLから適切なMedia sourceを作成する。URLは、インターネット上のURIだけでなく、ローカルファイルを指定することもできる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | CComQIPtr<IMFMediaSource> m_Source; // メンバ変数
// Source Resolver を使って Media source を作成する。
CComPtr<IUnknown> src;
MF_OBJECT_TYPE object_type=MF_OBJECT_INVALID;
CComPtr<IMFSourceResolver> src_resolver;
hr=MFCreateSourceResolver(&src_resolver);
hr=src_resolver->CreateObjectFromURL(
MOVIE_FILE_NAME,
MF_RESOLUTION_MEDIASOURCE,
NULL,
&object_type,
&src);
ATLASSERT(object_type==MF_OBJECT_MEDIASOURCE);
m_Source=src;
|
Topology のインスタンスを作成する
Topology のインスタンスを作成する。これをするためにMFCreateTopology
を呼ぶ。
1 2 | CComPtr<IMFTopology> m_Topology;
hr=MFCreateTopology(&m_Topology);
|
Media sourceの各ストリームごとにノードを作成する
作成したMedia sourceが音声付き動画であれば、そこからビデオとオーディオの2つのストリームを取り出すことができる。DirectShow とは異なり、スプリッタを介さず直接Media sourceからストリームを取り出す。
まず、Media sourceにストリームがいくつ含まれるのか調べる。Media sourceからPresentation descriptorを作成し、ストリーム数を取得する。
1 2 3 4 5 | // Presentation descriptorを作成し、ストリーム数を取得する
CComPtr<IMFPresentationDescriptor> pres_desc;
DWORD stream_count;
hr=m_Source->CreatePresentationDescriptor(&pres_desc);
hr=pres_desc->GetStreamDescriptorCount(&stream_count);
|
次にストリームごとに(つまりビデオとオーディオの2つ)、ソースノードと出力ノードを作成する。Presentation descriptorから、インデックス値を指定してStream descriptorをGetStreamDescriptorByIndex
を呼んで取得する。もしストリームが選択状態であれば、ソースノード、出力ノードを作成し、Topology に追加する。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | // 各ストリームごとに、ノードを作成する
for(DWORD i=0;i<stream_count;i++) {
CComPtr<IMFStreamDescriptor> stream_desc;
CComPtr<IMFActivate> sink_activate;
CComPtr<IMFTopologyNode> src_node;
CComPtr<IMFTopologyNode> output_node;
BOOL selected=FALSE;
hr=pres_desc->GetStreamDescriptorByIndex(i, &selected, &stream_desc);
if(selected) {
hr=CreateMediaSinkActivate(stream_desc, sink_activate);
if(SUCCEEDED(hr)) {
AddSourceNode(pres_desc, stream_desc, src_node);
AddOutputNode(sink_activate, output_node);
hr=src_node->ConnectOutput(0, output_node, 0);
}
}
}
|
ソースノードを作成するために、MFCreateTopologyNode
を呼んで空のノードを作成する。IMFAttributes::SetUnknown
を呼んで、ノードの属性としてMedia sourceを設定することにより、ソースノードとして動く。そしてIMFTopology::AddNode
を呼んでTopology を追加する。
1 2 3 4 5 6 7 8 9 10 11 12 | void AddSourceNode(
CComPtr<IMFPresentationDescriptor> pres_desc,
CComPtr<IMFStreamDescriptor> stream_desc,
CComPtr<IMFTopologyNode> &node)
{
CHResult hr;
hr=MFCreateTopologyNode(MF_TOPOLOGY_SOURCESTREAM_NODE, &node);
hr=node->SetUnknown(MF_TOPONODE_SOURCE, m_Source);
hr=node->SetUnknown(MF_TOPONODE_PRESENTATION_DESCRIPTOR, pres_desc);
hr=node->SetUnknown(MF_TOPONODE_STREAM_DESCRIPTOR, stream_desc);
hr=m_Topology->AddNode(node);
}
|
出力ノードもソースノードと同じく、空のノードにMedia sinkのPipeline objectを設定する。Media sinkの場合は、IMFActivate
インターフェイスで表されるアクティベーションオブジェクトを作成する。これは、ビデオであればEVRをアクティベートするMFCreateVideoRendererActivate
、オーディオであればSAR(標準オーディオレンダラ)をアクティベートするMFCreateAudioRendererActivate
を呼ぶことにより作成する。
メディアタイプの判別には、Stream Descriptor からMedia Handlerを取得し、メジャータイプを取得する。MFMediaType_Video
であればビデオ、MFMediaType_Audio
であればオーディオである。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | HRESULT CreateMediaSinkActivate(
CComPtr<IMFStreamDescriptor> stream_desc,
CComPtr<IMFActivate> &activate)
{
CComPtr<IMFMediaTypeHandler> handler;
GUID guidMajorType=GUID_NULL;
CHResult hr;
hr=stream_desc->GetMediaTypeHandler(&handler);
hr=handler->GetMajorType(&guidMajorType);
if(guidMajorType==MFMediaType_Audio) {
hr=MFCreateAudioRendererActivate(&activate);
}else if(guidMajorType==MFMediaType_Video) {
hr=MFCreateVideoRendererActivate(m_Static.m_hWnd, &activate);
}else {
hr=MF_E_INVALIDMEDIATYPE;
}
return hr;
}
|
アクティベーションオブジェクトの作成ができたら、新しいノードを作成し、それにアクティベーションオブジェクトを属性として設定し、Topology へ追加する。
1 2 3 4 5 6 7 8 9 10 | void AddOutputNode(
CComPtr<IMFActivate> activate,
CComPtr<IMFTopologyNode> &node)
{
CHResult hr;
hr=MFCreateTopologyNode(MF_TOPOLOGY_OUTPUT_NODE, &node);
hr=node->SetObject(activate);
hr=node->SetUINT32(MF_TOPONODE_STREAMID, 0);
hr=m_Topology->AddNode(node);
}
|
ノードの追加が終わったら、Media Session にTopology を設定する。
1 | hr=m_MFSession->SetTopology(0, m_Topology);
|
Topology を制御する
Media Foundation から発生したイベントを捕捉できるようにする。Media Foundation では、様々な処理が非同期的に実行され、その結果はIMFAsyncCallback
インターフェイスを継承したクラスに通知される。今回はCMyWindow
に実装する。
1 2 3 4 5 6 7 8 | class CMyWindow :
public CWindowImpl<CMyWindow>,
public CComObjectRootEx<CComMultiThreadModel>,
IMFAsyncCallback // これを追加する
// COMマップにIMFAsyncCallbackを追加
BEGIN_COM_MAP(CMyWindow)
COM_INTERFACE_ENTRY(IMFAsyncCallback)
END_COM_MAP()
|
イベントが発生するとIMFAsyncCallback::Invoke
が呼ばれる。今回は、イベントが発生したらWM_APP + 1
というウィンドウメッセージを投げて、その中で処理することにする。イベントを受け取るたびにEndGetEvent
を呼ぶ。イベントの処理が終わったらBeginGetEvent
を呼び、次のイベントを受け取れるようにする。
もし、イベントのタイプがMESessionClosed
の場合、イベントをシグナルにして、これ以上イベントを受け取らないようにする。MESessionClosed
は Media Session の IMFSession::Close
メソッドを呼んで、その処理が終わったときに発生するイベントである。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | CEvent m_MFSessionClosed; // Media Session がクローズするとシグナルになるメンバ変数
HRESULT STDMETHODCALLTYPE Invoke(IMFAsyncResult *pAsyncResult)
{
CHResult hr;
try {
MediaEventType me_type=MEUnknown;
CComPtr<IMFMediaEvent> media_event;
hr=m_MFSession->EndGetEvent(pAsyncResult, &media_event);
hr=media_event->GetType(&me_type);
if(me_type==MESessionClosed) {
ATLTRACE(_T("MESessionClosed\n"));
m_MFSessionClosed.Set();
// 以降、イベントを待たないので
// BeginGetEventは呼ばない。
}else {
media_event.p->AddRef();
PostMessage(WM_APP + 1,
(WPARAM)(IMFMediaEvent*)media_event, (LPARAM)0);
hr=m_MFSession->BeginGetEvent(this, NULL);
}
}
catch(...)
{
}
return hr;
}
|
さて、これでイベントが発生するたびにウィンドウメッセージ(WM_APP +1
)として受け取ることができる。受け取ったら、すべきことは次の2つである。
- Topology の状態が準備完了になったら再生する。
- Media Sourceがデータを読み始めたら
IMFVideoDisplayControl
を取得する。
IMFSession::SetTopology
を呼ぶと、非同期でTopology の準備が始まる。Topology の準備が完了すると、イベントが発生する。イベントの種類はMESessionTopologyStatus
で、MF_EVENT_TOPOLOGY_STATUS
属性の値がMF_TOPOSTATUS_READY
となる。
IMFVideoDisplayControl
は、ウィンドウ内にビデオを表示するためにレンダラーの制御を行うインターフェイスである。同じイベントの種類で属性の値がMF_TOPOSTATUS_STARTED_SOURCE
になったらインターフェイスを取得する。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | LRESULT OnMFEvent(UINT, WPARAM wparam, LPARAM, BOOL&)
{
CHResult hr;
HRESULT event_status;
CComPtr<IMFMediaEvent> media_event;
media_event.Attach((IMFMediaEvent*)wparam);
MediaEventType me_type=MEUnknown;
MF_TOPOSTATUS topo_status=MF_TOPOSTATUS_INVALID;
hr=media_event->GetType(&me_type);
hr=media_event->GetStatus(&event_status);
if(FAILED(event_status)) {
try {
hr=event_status;
}
catch(...)
{
}
}else {
if(me_type==MESessionTopologyStatus) {
media_event->GetUINT32(MF_EVENT_TOPOLOGY_STATUS, (UINT32*)&topo_status);
ATLTRACE(_T("topo_status=%d\n"), topo_status);
if(topo_status==MF_TOPOSTATUS_READY) {
OnTopologyReady(media_event);
}
if(topo_status==MF_TOPOSTATUS_STARTED_SOURCE) {
MFGetService(m_MFSession,
MR_VIDEO_RENDER_SERVICE,
__uuidof(IMFVideoDisplayControl),
(void**)&m_VideoDisplay);
}
}
}
media_event.Release();
return 0;
}
|
Comments
comments powered by Disqus