Media Foundation ⑤ WebCam + WPF プロジェクトの作成とC++の実装

以前紹介したMedia Foundation の MFCaptureD3D サンプルを基に、WebCam のビデオ出力を D3DImage 経由で WPF で表示してみましょう。WPF で表示できれば、回転やスケールは思いのままですし、ブラーなどのエフェクトも容易です。

image

WPFソリューションの作成と混合アセンブリに変換

D3DImage チュートリアルを参考にして、WPFソリューションを作成し、MFCaptureD3Dプロジェクトを追加し、C++/CLIの混合アセンブリに変換します。マニフェストは明示的に追加しなくても、自動生成のもので構いません。ついでに、WPFプロジェクトの設定もしておいてください。

C++コードの編集

まず、キャプチャしたビデオを表示するウィンドウを生成し、メッセージポンプを定義している winmain.cpp を「プロジェクトから除外」します。次にD3DWrapper.cppを追加します。D3DWrapper.cppの内容は以下の通りです。初期化するときにD3Dサーフェイスとビデオの幅と高さを取得して、呼び出し元(つまりC#側)に返します。あとは、SampleメソッドでのMFReaderの非同期サンプリングの命令と、クリーンアップだけです。非同期サンプリングなので、このSampleメソッドが返っても、サンプリングは終了していません。初期化時にMFStartupを、クリーンアップ時にMFShutdownを呼び出していることにも注意してください。

#include "MFcaptureD3D.h"
#include <vcclr.h>
using namespace System;
CPreview* g_pPreview;

namespace MFCaptureViewer
{
  public ref class D3DWrapper
  {
  public:
    IntPtr Initialize(IntPtr hwnd, int% width, int% height)
    {
      LPDIRECT3DSURFACE9 g_pd3dSurface;
      MFStartup(MF_VERSION);
      if (SUCCEEDED(CPreview::CreateInstance((HWND)
hwnd.ToPointer(),
(HWND)hwnd.ToPointer(),
&g_pPreview)))
      {
        // Get Surface
        g_pPreview->m_draw.m_pSwapChain->GetBackBuffer
(0,D3DBACKBUFFER_TYPE_MONO, &g_pd3dSurface);
        // Video width & height
        width = g_pPreview->m_draw.m_width;
        height = g_pPreview->m_draw.m_height;
        return IntPtr(g_pd3dSurface);
      }
    return IntPtr::Zero;
    }

    VOID Sample()
    {
        g_pPreview->m_pReader->ReadSample(
          (DWORD)MF_SOURCE_READER_FIRST_VIDEO_STREAM,
          0, NULL, NULL, NULL, NULL);
    }

    VOID Cleanup()
    {
       g_pPreview->Release();
       MFShutdown();
  }
  };
}

device.h と device.cpp

device.cppでは主にD3D系の処理を行っています。まずバックバッファのD3DSrufaceとビデオの幅・高さを D3DWrapperで取得できるように、device.hで4つのプロパティをパブリックにします。

public:
    UINT m_width; // moved to public
    UINT m_height; // moved to public
    IDirect3DSwapChain9 *m_pSwapChain; // moved to public
    DrawDevice(); // moved to public

    virtual ~DrawDevice();

device.cpp では、マルチスレッドの解決、およびスワップチェーンのブリットは不要なので、次の2か所を変更します。

    hr = m_pD3D->CreateDevice(
        D3DADAPTER_DEFAULT,
        D3DDEVTYPE_HAL,
        hwnd,
        D3DCREATE_HARDWARE_VERTEXPROCESSING |
D3DCREATE_FPU_PRESERVE |
        D3DCREATE_MULTITHREADED, // Add
        &pp,
        &m_pDevice
        );

    // Present the frame.
    // Removed
    // hr = m_pDevice->Present(NULL, NULL, NULL, NULL);

preview.hと preview.cpp

preview.cpp では主にMF関連の処理を行っています。まず、D3DWrapperで使えるように preview.h で2つのプロパティをパブリックにします。

public:
    IMFSourceReader *m_pReader; // moved to public
    DrawDevice m_draw; // moved to public

preview.cpp では、以下の2つのメソッドを変更します。前者で追加しているのは winmain.cppで行われていたWebCamデバイスを取得するコードです。後者では、非同期のコールバック内で次のサンプリングを呼び出していたのを、WM_PAINT メッセージの送付に変更しています(必ずしもWM_PAINTでなければならないわけではありません)。WPFではこのメッセージを基にD3DImageへの書き込みを行います。灰色は変更しないコードです。

HRESULT CPreview::Initialize()
{
    HRESULT hr = S_OK;
    hr = m_draw.CreateDevice(m_hwndVideo);
    // Add to get WebCam
    IMFActivate **ppDevices;
    UINT32 count = 0;
    IMFAttributes *pAttributes=NULL;
    hr = MFCreateAttributes(&pAttributes,1);
    hr = pAttributes->SetGUID(
      MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE,
MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_GUID);
    hr = MFEnumDeviceSources(pAttributes, &ppDevices, &count);
    if (count >0)
      hr = this->SetDevice(ppDevices[0]);
    // done
    return hr;
}

HRESULT CPreview::OnReadSample(
    HRESULT hrStatus,
    DWORD ,
    DWORD ,
    LONGLONG ,
    IMFSample *pSample // Can be NULL
    )
{
    HRESULT hr = S_OK;
    IMFMediaBuffer *pBuffer = NULL;
    EnterCriticalSection(&m_critsec);
    if (FAILED(hrStatus))
    {
        hr = hrStatus;
        goto done;
    }
    if (pSample)
    {
        // Get the video frame buffer from the sample.
        hr = pSample->GetBufferByIndex(0, &pBuffer);
        if (FAILED(hr)) { goto done; }
        // Draw the frame.
        hr = m_draw.DrawFrame(pBuffer);
        if (FAILED(hr)) { goto done; }
    }
    // Add
    hr = SendMessage(m_hwndEvent, WM_PAINT, 0, 0);
    // Request the next frame.
// Removed
    // hr = m_pReader->ReadSample(
    // (DWORD)MF_SOURCE_READER_FIRST_VIDEO_STREAM,
    // 0,
    // NULL, // actual
    // NULL, // flags
    // NULL, // timestamp
    // NULL // sample
    // );
done:
    if (FAILED(hr))
    {
        NotifyError(hr);
    }
    SafeRelease(&pBuffer);
    LeaveCriticalSection(&m_critsec);
    return hr;
}

ここで追加した SendMessage が非同期サンプリング時のC++とC#との同期問題を解決するカギの一つです。

この時点で、ソリューション エクスプローラーでこのMFCaptureD3Dプロジェクトを右クリックして、[プロジェクトのみ]→[MFCaptureD3Dのみをリビルド]を実行して、エラーが出ないことを確認してください。

つづく