Date

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;
}

実行結果

https://github.com/mahorigahama/mf_player1


Comments

comments powered by Disqus