2010年9月22日水曜日

EVRカスタムプレゼンタを実装する(2)

前回のまとめ。

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

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

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

IMFVideoPresenter

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

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

プレゼンタの現在の入力メディアタイプを返す。

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

GetCurrentMediaType
// メンバ変数
CCritSec m_ObjLock;
CComPtr<IMFMediaType> 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<IMFVideoMediaType>(ppMediaType);
    return hr;
}

IsShutdowned はプレゼンタの状態が RENDER_STATE_SHUTDOWN かどうかチェックするプロパティである。

GetCurrentMediaType
__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* で始まるシンボルが定義されている。

コードは以下。コマ送り機能は実装しないため、それに関わるメッセージは未実装としている。

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<HRESULT (void)> func;
    };
    const std::array<func_table, 9> 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](const func_table & a) -> bool { return eMessage == a.msg; });
    ATLENSURE_THROW(it != funcs.end(), E_INVALIDARG);
    hr = it->func();
    return hr;
}
IMFClockStateSink

IMFClockStateSink はプレゼンテーションクロックの状態が変化した時に呼び出されるメソッドをまとめたインターフェイスである。IMFVideoPresenter は、このインターフェイスから派生しているのでプレゼンタは必ず実装しなければならない。プレゼンタの状態を更新する契機となる。

IMFClockStateSink
メソッド プレゼンタの状態 説明
OnClockPause RENDER_STATE_PAUSED ポーズ(一時停止)。
OnClockRestart RENDER_STATE_STARTED ポーズから再スタート。
OnClockSetRate (変更なし。今回はサポートしない。) レートを変更したとき。今回はサポートしない。
OnClockStart RENDER_STATE_STARTED 開始。
OnClockStop RENDER_STATE_STOPPED 停止。

詳細

プレゼンタに必要なインターフェイスの実装は以上である。ここからは、実装の詳細をみていく。

メディアサンプルのハンドリング

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

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

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

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

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

フォーマットのネゴシエーション
RenegotiateMediaType

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

  1. ミキサーから利用可能な出力メディアタイプを1つ取得する。IMFTransform::GetOutputAvailableType を呼び出す。
  2. メディアタイプがこのプレゼンタでサポートできる形式であることを確認する。(CEvrPres::IsMediaTypeSupported)
  3. ミキサーから受け取るメディアサンプルを直接プレゼンタで取り扱えるように、メディアタイプをより適する内容に書き換える。(CEvrPres::CreateOptimalVideoType)
  4. 書き換えたメディアタイプが設定可能か確認する。IMFTransform::SetOutputTypeMFT_SET_TYPE_TEST_ONLY オプションを与えて呼び出す。
  5. プレゼンタの現在の入力メディアタイプとして、メンバ変数 m_MediaType に記憶する。(CEvrPres::SetMediaType)
  6. ミキサーにメディアタイプを設定する。

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

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. プログレッシブ (つまり非インターレス) であること。
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;
}
CreateOptimalVideoType

ミキサーが提示してきたメディアタイプをベースに、より適するメディアタイプに書き換える。(コード中のシンボルにリンクを張っておいたので、どのような設定をしているのかはリンク先のMSDNを参照のこと。)

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 を生成しておく。

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

メディアサンプルを作成するには Direct3D9 サーフェイスを作成し、それを引数として MFCreateVideoSampleFromSurface を呼び出す。今回は Direct3D10.1 との同期共有サーフェイスを作成するので ID3D10Texture2D など関連するインターフェイスをメディアサンプルの属性として登録する。

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(VS.85).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 の同期共有サーフェイスを作成し、関連インターフェイスを属性として登録
    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;
}

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

(3)に続く。