Date

https://github.com/mahorigahama/mf_player1_with_custom_evr

Media Foundation が使う標準のビデオレンダラーは EVR (Enhanced Video Renderer) である。EVR はDirectShowとMedia Foundationのインターフェイスに対応し、 Vista以降に搭載されている。

EVR は任意のウィンドウハンドルを渡すと、そのウィンドウにビデオを描画することができるが、この描画処理を自前でやりたいときがある。例えば Direct3D 対応のアプリケーションを作って、任意のサーフェイスやテクスチャにレンダリングしたい時である。そのような場合、カスタム プレゼンタ (Custom Presenter)を実装しなければならない。

今回は、以下のような仕様のカスタム プレゼンタの実装を進めながら、その方法について解説する。

  • EVR の標準ミキサーと連携する。
  • コード量を少なくするため、単なる再生のみサポートする。一時停止やシーク、逆再生などはサポートしない。
  • (プレゼンタ側ではなく)アプリケーション側で初期化した Direct3D10.1 に対応する。
  • Direct3D10.1 の2Dテクスチャにビデオ画像をレンダリングする。
  • DirectWrite で2Dテクスチャに再生位置をテキストで描画する機能を追加する。
  • 2DテクスチャからDirect3D10.1のバックバッファに転送し、ウィンドウに表示する。
  • 再生には Media Session を使用する。

(実は、サンプルプログラムとして EVRPresenter があるので、それを見ながら、というか、ほとんどコピペで実装できてしまう。しかし、カスタムプレゼンタの内部でDirect3D9を初期化・解放しているし、最近ではDirect3D10以上への対応も必要であろうから、改めて記述する。)

EVRの概要

EVRの構成要素

EVRは、ミキサーとプレゼンタの2つのコンポーネントから成り立っている。ミキサーはアップストリームフィルタから映像のストリームを受け取り、合成(composite)する。プレゼンタがミキサーから合成済の映像を受け取り、画面などのデバイスに出力する。この辺りはVMR9とよく似た構成である。

EVR の構成とデータフロー

プレゼンタの状態

プレゼンタは「状態」があり、以下のいずれかになる。状態はプレゼンテーションクロックによって変化する。シンボルはSDKで定義されているわけではなく、後で示すように自分で定義する。

プレゼンタの状態
状態 シンボル 説明
Started RENDER_STATE_STARTED プレゼンテーションクロックは開始の状態。プレゼンタは到着したビデオサンプルを表示するためのスケジュールをする。
Paused RENDER_STATE_PAUSED プレゼンテーションクロックは一時停止の状態。プレゼンタは新しいサンプルを表示しない。保持しているサンプル、新たに到着したサンプルは保持する。
Stopped RENDER_STATE_STOPPED プレゼンテーションクロックは停止の状態。プレゼンタは保持しているサンプルを破棄する。
Shut down RENDER_STATE_SHUTDOWN 使用中のリソースは全て破棄する。プレゼンタの初期と、破棄される前の状態がこれ。

```c++ プレゼンタの状態(コード) enum RENDER_STATE { RENDER_STATE_STARTED = 1, RENDER_STATE_STOPPED, RENDER_STATE_PAUSED, RENDER_STATE_SHUTDOWN, };

 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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
### プレゼンタが実装すべきインターフェイス

プレゼンタは VMR と同様、 co-class であり、以下のインターフェイスを実装しなければならない。
<div class="caption">
実装するインターフェイス</div>
<table><tbody>
<tr> <th>インターフェイス名</th> <th>説明</th> </tr>
<tr> <td>IMFGetService</td> <td>アプリケーションや他のコンポーネントが、プレゼンタからインターフェイスを取得するためのインターフェイス。</td> </tr>
<tr> <td>IMFTopologyServiceLookupClient</td> <td>プレゼンタが EVR やミキサーからインターフェイスを取得する。</td> </tr>
<tr> <td>IMFVideoDeviceID</td> <td>ミキサーが使っているテクノロジと互換性があるか判断するためのインターフェイス。標準ミキサーと連携させる場合は Direct3D9 デバイスでなければならない。</td> </tr>
<tr> <td>IMFVideoPresenter</td> <td>EVR から届くメッセージを処理するインターフェイス。</td> </tr>
<tr> <td>IMFClockStateSink</td> <td>`IMFVideoPresenter `の継承元。EVRのクロック状態が変わったときに呼び出されるインターフェイス。</td> </tr>
</tbody></table>

実装すべきインターフェイスが多くて気が遠くなりそうだ。VMR9 がいかにシンプルだったか…。
## サンプルコードの構成

サンプルコードには以下のクラスで構成されている。

![サンプルコードのクラス](/img/ClassDiagram1.png)

<table>
<caption>クラスの説明</caption>
    <tbody>
<tr> <th>クラス名</th> <th>説明</th> </tr>
<tr> <td>CMyWindow</td> <td>アプリケーションウィンドウのクラス。ATLの`CWindowImpl`を実装する。`CDGrph`のインスタンスを持つ。Media Session の制御も行う。</td> </tr>
<tr> <td>CCustomPresActivateObj</td> <td>プレゼンタをActivateする、独自のActivation Object。</td> </tr>
<tr> <td>CEvrPres</td> <td>プレゼンタのクラス。`IMFVideoDisplayControl`を追加で実装している。</td> </tr>
<tr> <td>ISurfaceSharing</td> <td>`CMyWindow` - `CEvrPres` 間でサーフェイスを共有するためのインターフェイス。`CEvrPres`に実装している。</td> </tr>
<tr> <td>CDGrph</td> <td>Direct3D9Ex, Direct3D10.1, Direct2D, DirectWrite を管理する。</td> </tr>
<tr> <td>SharedSurface</td> <td>Direct3D9Ex - Direct3D10.1 の同期共有サーフェイス。</td> </tr>
</tbody></table>
## プレゼンタを初期化する
### Direct3D9Ex/10.1を初期化する

Direct3D10.1 の2Dテクスチャにビデオ画像をレンダリングするため、まずは Direct3D10.1 を初期化する。再生位置を描画するために Direct2D、DirectWrite も初期化する。残念ながら EVR はWindows 7 になっても Direct3D9 がベースとなっているので Direct3D9 または 9Ex の初期化も必要だ。EVR は Vista 以降にしかないので Direct9Ex を選択することになるだろう。これらの処理を `CDGrph` クラスに実装する。`CDGrph` のインスタンスは`CMyWindow`が持つ。

前述したように、初期化した Direct3D デバイスはミキサーとデコーダも使用する。そのために Direct3D9Ex を初期化したあとに [Direct3D Device Manager](http://msdn.microsoft.com/en-us/library/aa965267%28VS.85%29.aspx) の初期化が必要だ。コードを以下に示す。

```c++ Direct3D Device Manager の初期化
// CDGrph のメンバ変数
UINT m_DeviceResetToken;
CComPtr<IDirect3DDeviceManager9> m_DevManager;  // プロパティ DevManager で取得可

HRESULT CDGrph::CreateDevice(HWND window)
{
    CHResult hr; // HRESULT をラップしたクラス

    // (Direct3D9Ex が初期済とする)

    hr = DXVA2CreateDirect3DDeviceManager9(&m_DeviceResetToken, &m_DevManager);
    // Direct3D9Exをリセットトークンと共に渡す
    hr = m_DevManager->ResetDevice(m_Device9Ex, m_DeviceResetToken);
    ...
}

Direct3D9Ex/10.1 をプレゼンタに渡す

初期化が終わったので、次は CMyWindow から CEvrPresCDGrph のインスタンスを渡す。いろいろなやり方があるが、今回はプレゼンタを Activate する Activation Object に CDGrph のインスタンスを属性として設定し、IMFActivate::ActivateObject 内で CEvrPresIMFAttributes を渡す方法をとる。文で説明すると分かりづらいので簡単なシーケンス図を用意した。

CDGrphを渡すシーケンス

```c++ CDGrph クラスに GUID を設定する // 属性はGUIDをキーとしているので、CDGrphクラス定義時にGUIDを設定しておく class __declspec(uuid("2A7D5E8A-8DE1-4e0a-B10F-ECA8DCE9C86E")) CDGrph ...

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
```c++ Activation Object に CDGrph のインスタンスを設定する
/* CCustomPresActivateObj : プレゼンタをActivateするIMFActivateを実装したクラス
m_DGrph : CDGrphクラスのCMyWindowのメンバ変数
activate : IMFTopologyNode に渡す Activation Object */
hr = MFCreateVideoRendererActivate(m_Static.m_hWnd, &activate);
CComObject<CCustomPresActivateObj> * my_activate_obj;
CComObject<CCustomPresActivateObj>::CreateInstance(&my_activate_obj);
PROPVARIANT pval;
pval.vt = VT_UI8;
pval.uhVal.QuadPart = (ULONGLONG)(&m_DGrph);
hr = my_activate_obj->SetItem(__uuidof(CDGrph), pval);
CComPtr<IUnknown> unk(my_activate_obj);
hr = activate->SetUnknown(MF_ACTIVATE_CUSTOM_VIDEO_PRESENTER_ACTIVATE, unk);

```c++ CDGrphのインスタンスをCEvrPresに渡す // m_Attr : MFCreateAttributesで作成したメンバ変数 HRESULT CCustomPresActivateObj::ActivateObject(REFIID riid, void ppv) { ATLENSURE_RETURN_HR(riid == __uuidof(IMFVideoPresenter), MF_E_CANNOT_CREATE_SINK); ATLENSURE_RETURN_HR(ppv != NULL, E_POINTER); if (m_CustomPres != NULL) { CComQIPtr mfvp(m_CustomPres); ppv = mfvp; mfvp.p->AddRef(); return S_OK; } CComObject * object; CComObject::CreateInstance(&object); // プレゼンタに自身の属性を引数として渡す object->Init(m_Attr); m_CustomPres.Attach(object); m_CustomPres.p->AddRef(); CComQIPtr mfvp(object); if (mfvp == NULL) { return MF_E_CANNOT_CREATE_SINK; } ppv = mfvp; mfvp.p->AddRef(); return S_OK; }

1
2
3
4
5
6
7
8
```c++ CDGrph のインスタンスを取得する
void CEvrPres::Init(IMFAttributes * attr)
{
    PROPVARIANT val;
    attr->GetItem(__uuidof(CDGrph), &val);
    m_DGrph = (CDGrph *)val.uhVal.QuadPart;
    PropVariantClear(&val);
}

これでアプリケーション側で初期化したDirect3Dをプレゼンタで使える。

プレゼンタに必要なインターフェイスを実装する

IMFGetService

IMFGetServiceは他のコンポーネントから特定のインターフェイスを取得するためのインターフェイスである。メソッドは IMFGetService:GetService の1つだけである。

プレゼンタでは IIDが IDirect3DDeviceManager9 を要求されたとき、そのインターフェイスを返す。それ以外のときは QueryInterface に移譲するように実装する。Direct3D Device Manager を外部に公開することにより、デコーダやミキサーとDirect3Dデバイスを共有できるようになる。

```c++ IMFGetService::GetService // CEvrPresのメンバ変数 CDGrph * m_DGrph;

HRESULT CEvrPres::GetService(REFGUID guidService, REFIID riid, LPVOID * ppvObject) { ATLENSURE_RETURN_HR(ppvObject, E_POINTER); if (riid == __uuidof(IDirect3DDeviceManager9)) { ATLENSURE_RETURN_HR(m_DGrph->DevManager, E_UNEXPECTED); *ppvObject = m_DGrph->DevManager; m_DGrph->DevManager->AddRef(); return S_OK; } return QueryInterface(riid, ppvObject); }

 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
36
37
38
39
40
41
42
43
44
### IMFTopologyServiceLookupClient

[`IMFTopologyServiceLookupClient`](http://msdn.microsoft.com/en-us/library/ms702001%28VS.85%29.aspx) は、プレゼンタが必要なインターフェイスを取得するためのインターフェイスである。

EVR が初期化されるとプレゼンタの [`IMFTopologyServiceLookupClient::InitServicePointers`](http://msdn.microsoft.com/en-us/library/bb970502%28VS.85%29.aspx) が呼ばれる。引数に [`IMFTopologyServiceLookup`](http://msdn.microsoft.com/en-us/library/ms702001%28VS.85%29.aspx) があるので [`IMFTopologyServiceLookup::LookupService`](http://msdn.microsoft.com/en-us/library/bb970504%28v=VS.85%29.aspx) でインターフェイスを取得する。取得するインターフェイスは以下。

<table>
    <caption>取得するインターフェイス</caption>
    <tbody>
<tr> <th>SID</th> <th>インターフェイス</th> <th>解説</th> </tr>
<tr> <td>MR_VIDEO_MIXER_SERVICE</td> <td>IMFTransform</td> <td>ミキサーのインターフェイス。ミキサーは`IMFTransform`を実装している。</td> </tr>
<tr> <td>MR_VIDEO_RENDER_SERVICE</td> <td>IMediaEventSink</td> <td>EVR 上のメディアイベントシンク。</td> </tr>
<tr> <td>MR_VIDEO_RENDER_SERVICE</td> <td>IMFClock</td> <td>EVR 上のプレゼンテーションクロック。</td> </tr>
</tbody></table>

これらのインターフェイスを取得するコードは以下。

```c++ インターフェイスを取得する
// CEvrPres のメンバ変数
CComPtr<IMFTransform> m_Mixer;
CComPtr<IMediaEventSink> m_MediaEventSink;
CComPtr<IMFClock> m_Clock;
CCritSec m_ObjLock; // クリティカルセクション

// HRESULT CEvrPres::InitServicePointers(IMFTopologyServiceLookup * pLookup)
CAutoCritSecLock lock(m_ObjLock);

DWORD obj_count;
obj_count = 1;
hr = pLookup->LookupService(MF_SERVICE_LOOKUP_GLOBAL, 0,
    MR_VIDEO_MIXER_SERVICE, IID_PPV_ARGS(&m_Mixer), &obj_count);
CComQIPtr<IMFVideoDeviceID> vid_dev_id(m_Mixer); // (IMFVideoDeviceIDについては後述)
IID dev_id = GUID_NULL;
hr = vid_dev_id->GetDeviceID(&dev_id);
if (dev_id != __uuidof(IDirect3DDevice9)) {
    return MF_E_INVALIDREQUEST;
}
obj_count = 1;
hr = pLookup->LookupService(MF_SERVICE_LOOKUP_GLOBAL, 0,
    MR_VIDEO_RENDER_SERVICE, IID_PPV_ARGS(&m_MediaEventSink), &obj_count);
// クロックを取得する。(無い場合もある。)
obj_count = 1;
pLookup->LookupService(MF_SERVICE_LOOKUP_GLOBAL, 0,
    MR_VIDEO_RENDER_SERVICE, IID_PPV_ARGS(&m_Clock), &obj_count);

更に追加のタスクとして以下がある。

  1. プレゼンタの状態を停止に更新する。
  2. ミキサーのソース矩形をフレーム全体となるように設定する。ミキサーのIMFAttributesを取得し、VIDEO_ZOOM_RECT を設定する。

```c++ InitServicePointers の追加のタスクのコード // 状態を停止に更新 m_RenderState = RENDER_STATE_STOPPED; // ミキサーのソース矩形をフレーム全体に設定する(ここではコードは省略) SetMixerSourceRect();

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
`LookupService`で取得したインターフェイスが無効になると、[`IMFTopologyServiceLookupClient::ReleaseServicePointers`](http://msdn.microsoft.com/en-us/library/ms693584%28V=VS.85%29.aspx) が呼ばれる。ここでは`InitServicePointers`で取得したインターフェイスを解放する。そしてプレゼンタの状態をシャットダウンに更新する。
### IMFVideoDeviceID

VMR9 では、ミキサー、プレゼンタともに Direct3D9 をベースに実装するしかなかった。EVR ではお互いが使っているテクノロジを公開するインターフェイスを設けることにより、この制限が緩和されている。そのインターフェイスというのが [`IMFVideoDeviceID`](http://msdn.microsoft.com/en-us/library/ms703065%28VS.85%29.aspx) である。

[`IMFVideoDeviceID::GetDeviceID`](http://msdn.microsoft.com/en-us/library/ms704630%28V=VS.85%29.aspx) が呼ばれたとき、使っているテクノロジの IID を返す。標準ミキサーは  Direct3D9 をベースとしているため、それと連携するには `__uuidof(IDirect3DDevice9)` でなければならない。ミキサーとプレゼンタ両方を置き換えるなら他の  IID を返すことができる。

```c++ GetDeviceID
HRESULT CEvrPres::GetDeviceID(IID * pDeviceID)
{
    ATLENSURE_RETURN_HR(pDeviceID != NULL, E_POINTER);
    *pDeviceID = __uuidof(IDirect3DDevice9);
    return S_OK;
}

ここまでのまとめ。

  1. EVRの構成要素
  2. プレゼンタの状態、実装すべきインターフェイス
  3. プレゼンタの初期化、Direct3Dの受け渡し
  4. IMFGetService, MFTopologyServiceLookupClient, IMFVideoDeviceID の実装

引き続き、残りのインターフェイスの実装について見ていくことにする。

プレゼンタに必要なインターフェイスを実装する(2)

IMFVideoPresenter

IMFVideoPresenter は以下の2つのメソッドを持つインターフェイスである。

IMFVideoPresenter
メソッド 説明
GetCurrentMediaType プレゼンタの現在の入力メディアタイプを返す。
ProcessMessage EVR から渡されたメッセージを処理する。ストリーミングが開始したとき、フレームを受け取ったときなどのタイミングで呼ばれる。

GetCurrentMediaType

プレゼンタの現在の入力メディアタイプを返す。 プレゼンタの入力メディアタイプを CEvrPres のメンバ変数 m_MediaType として記憶しておき、それを返す。メディアタイプはミキサーとフォーマットをネゴシエーションして決定する(後述)。他のスレッドでメディアタイプが書き換わるのを防ぐため、メソッド全体をクリティカルセクションでロックする。

```c++ GetCurrentMediaType // メンバ変数 CCritSec m_ObjLock; CComPtr m_MediaType;

HRESULT CEvrPres::GetCurrentMediaType(IMFVideoMediaType * ppMediaType) { HRESULT hr; ATLENSURE_RETURN_HR(ppMediaType, E_POINTER); ppMediaType = NULL; CAutoCritSecLock lock(m_ObjLock); // ロック ATLENSURE_RETURN_HR(SUCCEEDED(IsShutdowned), IsShutdowned); // 状態チェック if (m_MediaType == NULL) { // メディアタイプが未設定?? return MF_E_NOT_INITIALIZED; } // IMFVideoMediaType インターフェイスを取得して返す hr = m_MediaType.QueryInterface(ppMediaType); return hr; }

1
2
3
4
5
6
7
`IsShutdowned` はプレゼンタの状態が `RENDER_STATE_SHUTDOWN` かどうかチェックするプロパティである。
```c++
__declspec(property(get=get_IsShutdowned)) HRESULT IsShutdowned;
HRESULT CEvrPres::get_IsShutdowned() const
{
    return m_RenderState == RENDER_STATE_SHUTDOWN ? MF_E_SHUTDOWN : S_OK;
}

ProcessMessage

メソッドの引数としてメッセージ(eMessage)とメッセージの引数(ulParam)が渡されてくるので、メッセージを処理するメソッドにディスパッチする。 メッセージは MFVP_MESSAGE*で始まるシンボルが定義されている。

コードは以下。コマ送り機能は実装しないため、それに関わるメッセージは未実装としている。 ```c++ ProcessMessage // メンバ変数 // MFVP_MESSAGE_PROCESSINPUTNOTIFY を受け取った時に true bool m_InputNotify; // MFVP_MESSAGE_ENDOFSTREAM を受け取ったときに true bool m_EOS;

HRESULT CEvrPres::ProcessMessage(MFVP_MESSAGE_TYPE eMessage, ULONG_PTR ulParam) { HRESULT hr; CAutoCritSecLock lock(m_ObjLock); // メッセージ処理中はスレッドセーフにする ATLENSURE_RETURN_HR(SUCCEEDED(IsShutdowned), IsShutdowned); struct func_table { MFVP_MESSAGE_TYPE msg; std::function func; }; const std::array funcs = { { {MFVP_MESSAGE_FLUSH, this -> HRESULT { return Flush(); }}, {MFVP_MESSAGE_INVALIDATEMEDIATYPE, this -> HRESULT { return RenegotiateMediaType(); }}, {MFVP_MESSAGE_PROCESSINPUTNOTIFY, this -> HRESULT { return ProcessInputNotify(); }}, {MFVP_MESSAGE_BEGINSTREAMING, this -> HRESULT { return BeginStreaming(); }}, {MFVP_MESSAGE_ENDSTREAMING, this -> HRESULT { return EndStreaming(); }}, {MFVP_MESSAGE_ENDOFSTREAM, this -> HRESULT { m_EOS = true; return CheckEndOfStream(); }}, // 今回は以下の2つのメッセージは未実装。コマ送りを実現するなら実装が必要。 {MFVP_MESSAGE_STEP, this, ulParam -> HRESULT { return E_NOTIMPL; }}, {MFVP_MESSAGE_CANCELSTEP, this -> HRESULT { return E_NOTIMPL; }}, } }; m_InputNotify = false; auto it = std::find_if(funcs.begin(), funcs.end(), eMessage -> bool { return eMessage == a.msg; }); ATLENSURE_THROW(it != funcs.end(), E_INVALIDARG); hr = it->func(); return hr; }

 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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
### IMFClockStateSink

[`IMFClockStateSink`](http://msdn.microsoft.com/en-us/library/ms701593%28VS.85%29.aspx)はプレゼンテーションクロックの状態が変化した時に呼び出されるメソッドをまとめたインターフェイスである。`IMFVideoPresenter` は、このインターフェイスから派生しているのでプレゼンタは必ず実装しなければならない。プレゼンタの状態を更新する契機となる。

IMFClockStateSink

メソッド|プレゼンタの状態|説明
----|----|----
[OnClockPause](http://msdn.microsoft.com/en-us/library/ms697032%28v=VS.85%29.aspx) | RENDER_STATE_PAUSED|ポーズ(一時停止)。
[OnClockRestart](http://msdn.microsoft.com/en-us/library/bb970573%28v=VS.85%29.aspx) | RENDER_STATE_STARTED|ポーズから再スタート。
[OnClockSetRate](http://msdn.microsoft.com/en-us/library/bb970573%28v=VS.85%29.aspx) | (変更なし。今回はサポートしない。)|レートを変更したとき。今回はサポートしない。
[OnClockStart](http://msdn.microsoft.com/en-us/library/bb970573%28v=VS.85%29.aspx) | RENDER_STATE_STARTED|開始。
[OnClockStop](http://msdn.microsoft.com/en-us/library/bb970573%28v=VS.85%29.aspx) | RENDER_STATE_STOPPED|停止。

## 詳細

プレゼンタに必要なインターフェイスの実装は以上である。ここからは、実装の詳細をみていく。
### メディアサンプルのハンドリング

プレゼンタがミキサーからメディアサンプルを受け取るスレッドと、画面にレンダリングするスレッドは異なる。そこでプレゼンタは受け取ったサンプルをキューに蓄積することにする。ただしオーバーヘッドを避けるために深いコピーではなくインターフェイスへのポインタコピーするだけとする。レンダリングスレッドはキューからサンプルを取り出し、バックバッファにレンダリングする。これを実現するために以下のメンバ変数を設ける。

<table>
    <caption>メディアサンプルをハンドリングするためのメンバ変数</caption>
    <tbody>
<tr> <th>メンバ変数名</th> <th>型</th> <th>解説</th> </tr>
<tr> <td>m_SamplesPool</td> <td>CInterfaceArray<IMFSample></td> <td>メディアサンプルプール。プレゼンタはここからメディアサンプルを一つ取り出す。ミキサーからの出力を書き込む。</td> </tr>
<tr> <td>m_QueuedSamples</td> <td>CInterfaceList<IMFSample></td> <td>メディアサンプルキュー。プレゼンタはメディアサンプルをここにキューする。レンダリングスレッドは、ここから1つ取り出してバックバッファに書き込む。</td> </tr>
<tr> <td>m_Consumed</td> <td>std::vector<HANDLE></td> <td>Win32 イベントの配列。メディアサンプルプールの各サンプルと1対1で紐づいている。プールにあるメディアサンプルがキューされると対応するハンドルがリセットされる。レンダリングスレッドがメディアサンプルを取り出すか、キューがフラッシュされとシグナル状態になる。プール内のどれかのメディアサンプルが使用可能になるのを `WaitForMultipleObject` で待つことができる。</td> </tr>
<tr> <td>m_SamplesPoolLock</td> <td>CCritSec</td> <td>メディアサンプルプール、メディアサンプルキューへのアクセスを排他制御するためのクリティカルセクション。</td> </tr>
</tbody></table>

![アクティビティ](/img/handling_mf_samples.png)

`IMFStateClockSink` のメソッドで以下の場合はメディアサンプルキューをフラッシュ(Flush)する。

* `IMFStateClockSink::OnClockStart` で開始位置が `PRESENTATION_CURRENT_POSITION` でないとき。
* `IMFStateClockSink::OnClockStop` が呼ばれたとき。


フラッシュはキューに残ってるサンプルをすべてレンダリングするのが普通だが、ここでは単にキューの内容を破棄する。
### フォーマットのネゴシエーション
### RenegotiateMediaType

`MFVP_MESSAGE_INVALIDATEMEDIATYPE` メッセージを受け取ったとき、ミキサーが出力するフォーマットと、プレゼンタが受け入れるフォーマットを調整する。。大まかな処理の手順は以下。

1. ミキサーから利用可能な出力メディアタイプを1つ取得する。 [`IMFTransform::GetOutputAvailableType`](http://msdn.microsoft.com/en-us/library/ms703812%28V=VS.85%29.aspx) を呼び出す。
2. メディアタイプがこのプレゼンタでサポートできる形式であることを確認する。(`CEvrPres::IsMediaTypeSupported`)
3. ミキサーから受け取るメディアサンプルを直接プレゼンタで取り扱えるように、メディアタイプをより適する内容に書き換える。(`CEvrPres::CreateOptimalVideoType`)
4. 書き換えたメディアタイプが設定可能か確認する。 [`IMFTransform::SetOutputType`](http://msdn.microsoft.com/en-us/library/ms702016%28V=VS.85%29.aspx) に `MFT_SET_TYPE_TEST_ONLY` オプションを与えて呼び出す。
5. プレゼンタの現在の入力メディアタイプとして、メンバ変数 `m_MediaType` に記憶する。(`CEvrPres::SetMediaType`)
6. ミキサーにメディアタイプを設定する。

サンプルでは `CEvrPres::RenegotiateMediaType` にその処理を実装している。コードは以下。

```c++ RenegotiateMediaType
HRESULT CEvrPres::RenegotiateMediaType()
{
    HRESULT hr = S_OK;
    CComPtr<IMFMediaType> mixer_type;
    CComPtr<IMFMediaType> optimal_type;
    CComPtr<IMFVideoMediaType> video_type;
    ATLENSURE_RETURN_HR(m_Mixer != NULL, MF_E_INVALIDREQUEST);
    DWORD type_index = 0;
    while (hr != MF_E_NO_MORE_TYPES) {
        mixer_type.Release();
        hr = m_Mixer->GetOutputAvailableType(0, type_index++, &mixer_type);
        if (FAILED(hr)) {
            break;
        }
        CHResult nego_hr;
        try {
            nego_hr = IsMediaTypeSupported(mixer_type);
            optimal_type.Release();
            nego_hr = CreateOptimalVideoType(mixer_type, &optimal_type);
            nego_hr = m_Mixer->SetOutputType(0, optimal_type, MFT_SET_TYPE_TEST_ONLY);
            nego_hr = SetMediaType(optimal_type);
            nego_hr = m_Mixer->SetOutputType(0, optimal_type, 0);
            hr = S_OK;
            break;
        }
        catch (CAtlException) {
            SetMediaType(NULL);
        }
    }
    return hr;
}

IsMediaTypeSupported

メディアタイプに対し以下の項目を確認する。

  1. 非圧縮であること。
  2. フォーマットはD3DFMT_X8R8G8B8であること。
  3. プログレッシブ (つまり非インターレス) であること。

```c++ IsMediaTypeSupported HRESULT CEvrPres::IsMediaTypeSupported(IMFMediaType * mt) { ATLENSURE_RETURN_HR(mt != NULL, E_POINTER); CHResult hr; try { // 非圧縮? BOOL compressed; hr = mt->IsCompressedFormat(&compressed); if (compressed) { return MF_E_INVALIDMEDIATYPE; } // フォーマットは D3DFMT_X8R8G8B8? GUID sub_type; hr = mt->GetGUID(MF_MT_SUBTYPE, &sub_type); D3DFORMAT format = (D3DFORMAT)sub_type.Data1; if (format != D3DFMT_X8R8G8B8) { return MF_E_INVALIDMEDIATYPE; } // プログレッシブ? UINT32 interlace_mode; hr = mt->GetUINT32(MF_MT_INTERLACE_MODE, (UINT32*)&interlace_mode); if (interlace_mode != MFVideoInterlace_Progressive) { return MF_E_INVALIDMEDIATYPE; } } catch (CAtlException &e) { return e.m_hr; } return hr; }

 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
### CreateOptimalVideoType

ミキサーが提示してきたメディアタイプをベースに、より適するメディアタイプに書き換える。

```c++ CreateOptimalVideoType
HRESULT CEvrPres::CreateOptimalVideoType(IMFMediaType * propsed_mt, IMFMediaType ** optimal_mt)
{
    CHResult hr;
    ATLENSURE_RETURN_HR(propsed_mt != NULL, E_POINTER);
    ATLENSURE_RETURN_HR(optimal_mt != NULL, E_POINTER);
    try {
        CRect outupt_rect;
        MFVideoArea displayArea = {0};
        hr = MFCreateMediaType(optimal_mt);
        hr = propsed_mt->CopyAllItems(*optimal_mt);
        hr = MFSetAttributeRatio(*optimal_mt, MF_MT_PIXEL_ASPECT_RATIO, 1, 1);
        hr = (*optimal_mt)->SetUINT32(MF_MT_YUV_MATRIX, MFVideoTransferMatrix_BT709);
        hr = (*optimal_mt)->SetUINT32(MF_MT_TRANSFER_FUNCTION, (UINT32)MFVideoTransFunc_709);
        hr = (*optimal_mt)->SetUINT32(MF_MT_VIDEO_PRIMARIES, (UINT32)MFVideoPrimaries_BT709);
        hr = (*optimal_mt)->SetUINT32(MF_MT_VIDEO_NOMINAL_RANGE, (UINT32)MFNominalRange_16_235);
        hr = (*optimal_mt)->SetUINT32(MF_MT_VIDEO_LIGHTING, (UINT32)MFVideoLighting_dim);
        hr = (*optimal_mt)->SetUINT32(MF_MT_PAN_SCAN_ENABLED, (UINT32)FALSE);
    }
    catch (CAtlException &e) {
        return e.m_hr;
    }
    return hr;
}

SetMediaType と CreateMFSample

メディアタイプが決定したら、プレゼンタのメンバ変数 m_MediaType に記憶する。そして画像フレームを受け取るためのメディアサンプルを作成する。メディアサンプルは 1 つではなく複数個用意し配列で管理する。メディアサンプルが使用済みかどうかを判断する Win32 イベントハンドルを ATTR_CONSUMED 属性に記憶する。 ATTR_CONSUMED は guidgen.exe で GUID を生成しておく。 ```c++ SetMediaType HRESULT CEvrPres::SetMediaType(IMFMediaType * mt) { if (mt == NULL) { m_MediaType.Release(); ReleaseResources(); return S_OK; } ATLENSURE_RETURN_HR(SUCCEEDED(IsShutdowned), IsShutdowned); DWORD flags; if (m_MediaType) { HRESULT is_equal = m_MediaType->IsEqual(mt, &flags); if (is_equal == S_OK) { return S_OK; } m_MediaType.Release(); } ReleaseResources(); // メディアサンプルを作成する CHResult hr; try { m_SamplesPool.SetCount(BUFFER_SIZE); for (size_t n = 0;n < m_SamplesPool.GetCount(); n++) { CComPtr obj; hr = m_DGrph->CreateMFSample(mt, &obj); // (後述) HANDLE e = CreateEvent(NULL, TRUE, TRUE, NULL); obj->SetUINT64(ATTR_CONSUMED, (UINT64)e); m_SamplesPool.SetAt(n, obj); m_Consumed.push_back(e); } ATLASSERT(m_SamplesPool.GetCount() == m_Consumed.size()); } catch (CAtlException &e) { m_SamplesPool.RemoveAll(); return e.m_hr; } m_MediaType.Attach(mt); mt->AddRef(); return hr; }

 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
メディアサンプルを作成するには Direct3D9 サーフェイスを作成し、それを引数として [`MFCreateVideoSampleFromSurface`](http://msdn.microsoft.com/en-us/library/ms703859%28VS.85%29.aspx) を呼び出す。今回は Direct3D10.1 との同期共有サーフェイスを作成するので `ID3D10Texture2D` など関連するインターフェイスをメディアサンプルの属性として登録する。

```c++ CreateMFSample
HRESULT CDGrph::CreateMFSample(IMFMediaType * mt, IMFSample ** mf_sample)
{
    CHResult hr;
    ATLENSURE_RETURN_HR(m_Window, MF_E_INVALIDREQUEST);
    ATLENSURE_RETURN_HR(mt, MF_E_UNEXPECTED);
    ATLENSURE_RETURN_HR(mf_sample, MF_E_UNEXPECTED);
    CAutoCritSecLock lock(m_ObjLock);
    // サーフェイスフォーマットを決める
    UINT32 width, height;
    hr = MFGetAttributeSize(mt, MF_MT_FRAME_SIZE, &width, &height);
    // note : SUBTYPEのData1はD3DFORMATと同じ値
    // http://msdn.microsoft.com/en-us/library/aa370819%28VS.85%29.aspx
    GUID sub_type;
    hr = mt->GetGUID(MF_MT_SUBTYPE, &sub_type);
    D3DFORMAT d3dformat = (D3DFORMAT)sub_type.Data1;
    ATLASSERT(d3dformat == D3DFMT_X8R8G8B8);
    // Direct3D9Ex - 10.1 の<a href="http://msdn.microsoft.com/en-us/library/ee913554%28VS.85%29.aspx">同期共有サーフェイス</a>を作成し、関連インターフェイスを属性として登録
    SharedSurface surf;
    hr = CreateSharedSurface(D3DFMT_A8R8G8B8, CSize(width, height), surf);
    hr = MFCreateVideoSampleFromSurface(surf.Surface9, mf_sample);
    hr = (*mf_sample)->SetUnknown(__uuidof(ID3D10Texture2D), surf.Texture10);
    hr = (*mf_sample)->SetUnknown(__uuidof(ID3D10ShaderResourceView1), surf.SRView);
    hr = (*mf_sample)->SetUnknown(__uuidof(IDXGISurface), surf.DxgiSurf);
    hr = (*mf_sample)->SetUnknown(__uuidof(ID2D1RenderTarget) , surf.D2DTarget);
    hr = (*mf_sample)->SetUnknown(__uuidof(ID2D1SolidColorBrush), surf.Brush);
    return S_OK;
}

次はミキサーからメディアサンプルを受信し、バックバッファにレンダリングする処理を実装していく。

メディアサンプルのハンドリング(続き)

メディアサンプルの受信

ProcessOutput

MFVP_MESSAGE_PROCESSINPUTNOTIFY メッセージを受け取ったとき、プレゼンタはミキサーからメディアサンプルを受け取る。Media Foundation では DirectShow でいうところのプルモデルを採用しているので、プレゼンタ側からミキサーに対しサンプルを要求する。ミキサー上の IMFTransform::ProcessOutput を呼び出しメディアサンプルを受け取る。

CEvrPres::ProcessMessage で当該メッセージを受け取ったとき CEvrPres::ProcessInputNotify を呼び出している。実際には、さらにその中で呼び出している CEvrPres::ProcessOutput でメディアサンプルを受信している。おおまかな処理の流れは以下。

  1. メディアサンプルプールに空きができるのを待つ。CEvrPres::SetMediaType で作成しておいた Win32 イベントハンドル群を待つように WaitForMultipleObjects を呼び出す。(ProcessOutput から抜ける前に空きを待っているので、必ず1つ以上のあるはず。)
  2. メディアサンプルにミキサーの出力を書き込む。ここでミキサー上の IMFTransform::ProcessOutput を呼び出す。
  3. メディアサンプルキューに書き込んだメディアサンプルを追加する。
  4. EC_PROCESSING_LATENCY イベントを EVR に通知する。これは、どの程度処理に時間がかかったかをEVRに報告するメッセージである。
  5. メディアサンプルプールがいっぱいなら、空きができる(キューが消費される)まで待つ。

```c++ ProcessOutput HRESULT CEvrPres::ProcessOutput() { HRESULT hr = S_OK; MFTIME hns_time; ATLENSURE_RETURN_HR(m_Mixer, MF_E_INVALIDREQUEST); ATLENSURE_RETURN_HR(m_SamplesPool.GetCount() > 0, MF_E_TRANSFORM_TYPE_NOT_SET); CComPtr mf_sample; size_t sample_num; // 空きサンプル待ち DWORD wait_result; wait_result = WaitForMultipleObjects(m_Consumed.size(), &m_Consumed.at(0), FALSE, MFSAMPLE_TIMEOUT); ATLENSURE_RETURN_HR(wait_result != WAIT_TIMEOUT, S_FALSE); ATLASSERT(wait_result >= WAIT_OBJECT_0 && wait_result < (WAIT_OBJECT_0 + m_Consumed.size())); sample_num = wait_result - WAIT_OBJECT_0; // ミキサーからサンプルを受信する CAutoCritSecLock pool_lock(m_SamplesPoolLock); MFT_OUTPUT_DATA_BUFFER data_buffer = {0}; mf_sample = m_SamplesPool.GetAt(sample_num); if (mf_sample == NULL) { return E_FAIL; } data_buffer.pSample = mf_sample; LONGLONG mixer_start_time, mixer_end_time; if (m_Clock) { m_Clock->GetCorrelatedTime(0, &mixer_start_time, &hns_time); } DWORD status; hr = m_Mixer->ProcessOutput(0, 1, &data_buffer, &status); if (FAILED(hr)) { if (hr == MF_E_TRANSFORM_NEED_MORE_INPUT) { return hr; } else if (hr == MF_E_TRANSFORM_TYPE_NOT_SET) { hr = RenegotiateMediaType(); return hr; } else if (hr == MF_E_TRANSFORM_STREAM_CHANGE) { // メディアタイプが変更された SetMediaType(NULL); } return hr; } // EC_PROCESSING_LATENCY を送信する if (m_Clock) { MFCLOCK_STATE mfc_state; m_Clock->GetState(0, &mfc_state); m_Clock->GetCorrelatedTime(0, &mixer_end_time, &hns_time); const LONGLONG latency = mixer_end_time - mixer_start_time; if (mfc_state == MFCLOCK_STATE_RUNNING) { NotifyEvent(EC_PROCESSING_LATENCY, (LONG_PTR)&latency, 0); } } ResetEvent(m_Consumed.at(sample_num)); m_QueuedSamples.AddTail(mf_sample); pool_lock.Unlock(); // サンプルプールのサイズとキュー済サンプルのサイズが同じ // → サンプルプールがいっぱいのときは、キューが消費されるまで待つ size_t count; do { Sleep(0); { CAutoCritSecLock pool_lock(m_SamplesPoolLock); GetCount(&count); } } while (count == m_SamplesPool.GetCount()); ATLASSERT(count < m_SamplesPool.GetCount()); return hr; }

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
## レンダリングスレッド

レンダリングスレッドでは、メディアサンプルキューからメディアサンプルを1つ取り出し、バックバッファにレンダリングする。ここでは PPLの `Concurrency::task_group` を使ってスレッドを作成し、 `CMyWindow::Render` に実装している。おおまかな処理の流れは以下。

1. Media Session の現在のプレゼンテーション時刻を取得する。あらかじめMedia Session から`IMFClock` を取得しておき [`IMFClock::GetCorrelatedTime`](http://msdn.microsoft.com/en-us/library/ms694122%28VS.85%29.aspx) を呼ぶ。
2. メディアサンプルキューからメディアサンプルを1つ取り出す。但し削除しない(Peek)。
3. メディアサンプルのプレゼンテーション時刻と長さ(Duration)を取得する。
4. プレゼンテーション時刻と比較して、メディアサンプルを表示すべきかどうか確認する。
5. メディアサンプルから `IDirect3DSurface9` を取得し、バックバッファへ転送。

レンダリングスレッドからメディアサンプルキューにアクセスするために `CEvrPres` は `ISurfaceSharing` インターフェイスを公開している。

```c++ ISurfaceSharing の定義
__interface __declspec(uuid("{AD4A803A-06DC-45f5-983E-DAE5D1EB10E8}")) ISurfaceSharing : IUnknown
{
    HRESULT STDMETHODCALLTYPE GetLock(LPCRITICAL_SECTION * lock);
    HRESULT STDMETHODCALLTYPE PeekHead(IMFSample ** mf_sample);
    HRESULT STDMETHODCALLTYPE GetCount(size_t * count);
    HRESULT STDMETHODCALLTYPE RemoveHead();
};

レンダリングスレッドのコードは以下。全部書くと冗長なのでかいつまんで。

```c++ CMyWindow::Render // Topologyを構築したときに ISurfaceSharing インターフェイスを取得しておく。 CComPtr m_SurfSharing; MFGetService(m_MFSession, MR_VIDEO_RENDER_SERVICE, PPV_ARGS(&m_VideoDisplay)); m_VideoDisplay->QueryInterface(IID_PPV_ARGS(&m_SurfSharing));

// 1. 現在のプレゼンテーション時刻を取得 LONGLONG clock_time; MFTIME hns_time; m_Clock->GetCorrelatedTime(0, &clock_time, &hns_time);

CComPtr mf_sample; LPCRITICAL_SECTION crit_sec; m_SurfSharing->GetLock(&crit_sec); EnterCriticalSection(crit_sec);

CComPtr head_sample; LONGLONG sample_time, sample_duration; do { // 2. メディアサンプルを取り出す head_sample.Release(); m_SurfSharing->PeekHead(&head_sample); // 取り出し if (head_sample == NULL) { // サンプルが一つもない LeaveCriticalSection(crit_sec); return 0; } // 3. 時刻と長さを取得 head_sample->GetSampleTime(&sample_time); head_sample->GetSampleDuration(&sample_duration); UINT32 consumed = TRUE; head_sample->GetUINT32(ATTR_CONSUMED, &consumed); // 4. 表示すべきかどうか確認する if ((sample_time + sample_duration) < clock_time) { // 表示すべき break; } . . . } while (true);

// 5. IDirect3DSurface9 を取得し、バックバッファに転送 CComPtr mf_mbuf; CComPtr surface; head_sample->GetBufferByIndex(0, &mf_mbuf); MFGetService(mf_mbuf, MR_BUFFER_SERVICE, __uuidof(IDirect3DSurface9), (void**)&surface); ```


Comments

comments powered by Disqus