MediaFoundationVirtualCamera - HexagramNM/NM_WindowCaptureVirtualCamera GitHub Wiki

MediaFoundationの仮想カメラ

MediaFoundationというWindows用のビデオや音声周りのフレームワークを利用することで、PC内の映像をカメラ扱いで配信する仮想カメラを実装することができます。以下のような特徴があります。

  • 比較的新しいフレームワークである。DirectShowの後継のフレームワークであるため、すぐには非推奨にはならないと思われる。

    • Windows11のみのサポートで、Windows10やそれ以前の古いWindowsでは使用できない。
  • 仮想カメラの処理はFrameServerのプロセス上で動作する

    • FrameServerのプロセスでdllが使用できれば良いため、x64版のdllだけ作っておけばよい。

    • LocalServiceのアカウントでプロセスが実行される。LocalServiceの権限が弱いため、あらかじめdllファイルや親のフォルダのアクセス権を設定しておく必要がある。

      • 具体的にはUsersに対して、「読み取りと実行」の権限が必要
    • FrameServerのプロセスがセッション0というユーザから分離されたところで動作するため、キャプチャしたウィンドウの映像を表示しようとすると、DirectXの共有テクスチャなどでプロセス間通信を行う必要がある。

  • アプリケーションが起動した時のみ使用可能になる仮想カメラを作成できる

    • DirectShowだと一度dllがレジストリに登録されると常に使用できる状態になるため、アプリケーションが起動していない時は"No Signal"を表示する処理を作っておく必要がある。
  • Windows標準のカメラアプリケーションでも、MediaFoundationの仮想カメラなら使用できる


NM_WCVCam_MFはMediaFoundation仮想カメラのサンプルであるsmourier/VCamSampleを改造したもので、NM_WindowCaptureからの共有テクスチャを映像として表示できるようにしています。

クラスの構成

MediaFoundationの仮想カメラではNM_WCVCam_MF/dllmain.cppActivatorのインスタンスを生成するところから始まります。以下のように、各クラスがつながっている形です。

  • Activator (IMFActivateを継承): 仮想カメラ周りの設定を行い、MediaSourceのインスタンスを生成。

  • MediaSource (IMFMediaSourceなどを継承): メディアソース周りの設定を行い、MediaStreamのインスタンスを生成。

  • MediaStream (IMFMediaStreamなどを継承): メディアストリームや映像形式周りの設定を行い、FrameGeneratorのインスタンスを生成。このRequestSampleメソッドでFrameGeneratorからの映像サンプルを映像として渡している。

  • FrameGenerator (独自クラス): 仮想カメラに渡す映像サンプルIMFSampleを作る。

NM_WCVCam_MFでは、映像の画素数を変更するためにMediaStreamを、共有テクスチャの画像を映像として表示するためにFrameGeneratorを修正しています。

FrameGeneratorでやっていること

詳細はMF_WCVCam_MF/FrameGenerator.hMF_WCVCam_MF/FrameGenerator.cppをご覧ください。

DirectXのデバイスやデバイスコンテキストの作成

共有テクスチャの取得やテクスチャのコピーを行うため、DirectXのデバイスやデバイスコンテキストを作成しています。共有テクスチャを送信するプロセスと同じGPU(DirectXのデバイス)を使用する必要があります。そのため、DXGIの機能でDirectXのデバイスを列挙し、各デバイスから共有テクスチャの取得を試みています。共有テクスチャの取得ができたDirectXのデバイスを以降で使用しています。

DirectShowの仮想カメラでも同様のことをやっているので、詳細はこちらをご覧ください。

共有テクスチャは、DirectXのデバイスのOpenSharedResourceByNameメソッドで取得できます。詳しくはこちらをご覧ください。

UINT createDeviceFlags = D3D11_CREATE_DEVICE_BGRA_SUPPORT;
createDeviceFlags |= D3D11_CREATE_DEVICE_VIDEO_SUPPORT;
#ifdef _DEBUG
createDeviceFlags |= D3D11_CREATE_DEVICE_DEBUG;
#endif

wil::com_ptr_nothrow<IDXGIFactory1> factory;
wil::com_ptr_nothrow<IDXGIAdapter1> adapter;
wil::com_ptr_nothrow<ID3D11Device> device;
D3D_FEATURE_LEVEL d3dFeatures[7] = {
    D3D_FEATURE_LEVEL_11_1
};

UINT adapterIdx = 0;
HRESULT hr = S_OK;
RETURN_IF_FAILED(CreateDXGIFactory1(IID_PPV_ARGS(factory.put())));
while (factory->EnumAdapters1(adapterIdx, adapter.put()) != DXGI_ERROR_NOT_FOUND) 
{
    hr = D3D11CreateDevice(adapter.get(), D3D_DRIVER_TYPE_UNKNOWN,
        nullptr, createDeviceFlags, d3dFeatures, 1, D3D11_SDK_VERSION,
        device.put(), nullptr, _dxDeviceContext.put());
    if (hr != S_OK) {
        adapterIdx++;
        continue;
    }

    hr = device->QueryInterface(IID_PPV_ARGS(_dxDevice.put()));
    if (hr != S_OK) {
        adapterIdx++;
        continue;
    }

    // キャプチャウィンドウの共有テクスチャをハンドルから取得。
    hr = _dxDevice->OpenSharedResourceByName(
        SHARED_CAPTURE_WINDOW_TEXTURE_PATH, DXGI_SHARED_RESOURCE_READ,
        IID_PPV_ARGS(_sharedCaptureWindowTexture.put()));
    if (hr != S_OK) {
        adapterIdx++;
        continue;
    }

    break;
}

RETURN_IF_FAILED(hr);

バッファとなる別テクスチャの作成

MediaFoundationの仮想カメラはDirectXのテクスチャを映像サンプルとして渡すことができます。しかし、取得してきた共有テクスチャを渡すことはフラグ設定の関係でできなかったため、別のバッファテクスチャを作成し、そこにコピーすることにしました。

D3D11_TEXTURE2D_DESC bufferTextureDesc;
bufferTextureDesc.Width = VCAM_VIDEO_WIDTH;
bufferTextureDesc.Height = VCAM_VIDEO_HEIGHT;
bufferTextureDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
bufferTextureDesc.ArraySize = 1;
bufferTextureDesc.BindFlags = 0;
bufferTextureDesc.CPUAccessFlags = 0;
bufferTextureDesc.MipLevels = 1;
bufferTextureDesc.MiscFlags = 0;
bufferTextureDesc.SampleDesc.Count = 1;
bufferTextureDesc.SampleDesc.Quality = 0;
bufferTextureDesc.Usage = D3D11_USAGE_DEFAULT;
device->CreateTexture2D(&bufferTextureDesc, 0, _bufferTexture.put());

IMFTransformの作成

元の実装ではIMFTransformを使用して、1ピクセルにつきBGRAの32ビット使用するフォーマットをNV12という1ピクセルにつき12ビット使用するフォーマットに変換しています。この処理はNM_WCVCam_MFでも使用しています。

このIMFTransformは内部でDirectXの機能を用いて処理するためか、DirectXのデバイスマネージャ(IMFDXGIDeviceManager)と紐づけておく必要があります。このデバイスマネージャで複数スレッドからDirectXのデバイスを使用できるようになります。以下のコードで作成することができます。

// _dxgiManager: IMFDXGIDeviceManager
UINT resetToken;
RETURN_IF_FAILED(MFCreateDXGIDeviceManager(&resetToken, _dxgiManager.put()));

// マルチスレッドの設定は無くても大丈夫そうだが念のため
wil::com_ptr_nothrow<ID3D11Multithread> dxMultiThread;
_dxDeviceContext->QueryInterface(IID_PPV_ARGS(dxMultiThread.put()));
dxMultiThread->SetMultithreadProtected(true);

// 作成したDirectXのデバイスをデバイスマネージャに紐づけ
_dxgiManager->ResetDevice(device.get(), resetToken);

Note

元の実装だと外部からこのデバイスマネージャを取得していましたが、共有テクスチャを取得する関係でDirectXのデバイスが必須であり、仮想カメラ側のコードでデバイスマネージャを作成しても問題なく動作したので、そのように修正しています。


デバイスマネージャを作成した後は、以下のコードでRGB32のフォーマットからNV12のフォーマットに変換するIMFTransformを作成できます。ここは元のコードの実装をそのまま使用しております。

// NV12フォーマットのコンバータ (IMFTransform) 作成
HRESULT FrameGenerator::SetupNV12Converter() 
{
    // create GPU RGB => NV12 converter
    RETURN_IF_FAILED(CoCreateInstance(CLSID_VideoProcessorMFT, nullptr, CLSCTX_ALL, IID_PPV_ARGS(_converter.put())));

    wil::com_ptr_nothrow<IMFAttributes> atts;
    RETURN_IF_FAILED(_converter->GetAttributes(&atts));

    MFT_OUTPUT_STREAM_INFO info{};
    RETURN_IF_FAILED(_converter->GetOutputStreamInfo(0, &info));
    
    wil::com_ptr_nothrow<IMFMediaType> inputType;
    RETURN_IF_FAILED(MFCreateMediaType(&inputType));
    inputType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video);
    inputType->SetGUID(MF_MT_SUBTYPE, MFVideoFormat_RGB32);
    MFSetAttributeSize(inputType.get(), MF_MT_FRAME_SIZE, _width, _height);
    RETURN_IF_FAILED(_converter->SetInputType(0, inputType.get(), 0));

    wil::com_ptr_nothrow<IMFMediaType> outputType;
    RETURN_IF_FAILED(MFCreateMediaType(&outputType));
    outputType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video);
    outputType->SetGUID(MF_MT_SUBTYPE, MFVideoFormat_NV12);
    MFSetAttributeSize(outputType.get(), MF_MT_FRAME_SIZE, _width, _height);
    RETURN_IF_FAILED(_converter->SetOutputType(0, outputType.get(), 0));

    // make sure the video processor works on GPU
    RETURN_IF_FAILED(_converter->ProcessMessage(MFT_MESSAGE_SET_D3D_MANAGER, (ULONG_PTR)_dxgiManager.get()));

    return S_OK;
}

映像サンプルの作成

FrameGeneratorクラスのGenerateメソッドでは、以下の処理を行っております。MediaFoundationの仮想カメラでは映像サンプルとして直接DirectXのテクスチャを渡すことができます。

  1. 共有テクスチャを別のバッファテクスチャにコピー
  2. コピーしたテクスチャからIMFMediaBufferを作成し、映像サンプルに追加
  3. IMFTransformでフォーマット変換
  4. 変換後のものを出力映像サンプルに設定
HRESULT FrameGenerator::Generate(IMFSample* sample, REFGUID format, IMFSample** outSample)
{
    RETURN_HR_IF_NULL(E_POINTER, sample);
    RETURN_HR_IF_NULL(E_POINTER, outSample);
    *outSample = nullptr;

    wil::com_ptr_nothrow<IMFMediaBuffer> mediaBuffer;

    // sampleから全てのバッファを削除(リセット)
    RETURN_IF_FAILED(sample->RemoveAllBuffers());

    // 共有テクスチャをバッファテクスチャにコピー(処理1)
    // コピーする前に共有テクスチャのmutexを取得し、コピー後にmutexを解放する。
    wil::com_ptr_nothrow<IDXGIKeyedMutex> mutex;
    _sharedCaptureWindowTexture->QueryInterface(IID_PPV_ARGS(mutex.put()));
    mutex->AcquireSync(MUTEX_KEY, INFINITE);
    _dxDeviceContext->CopyResource(_bufferTexture.get(), _sharedCaptureWindowTexture.get());
    mutex->ReleaseSync(MUTEX_KEY);

    // コピーしたテクスチャからIMFMediaBufferを作成し、映像サンプルに追加(処理2)
    RETURN_IF_FAILED(MFCreateDXGISurfaceBuffer(__uuidof(ID3D11Texture2D), _bufferTexture.get(), 0, 0, &mediaBuffer));
    RETURN_IF_FAILED(sample->AddBuffer(mediaBuffer.get()));

    if (format == MFVideoFormat_NV12)
    {
        // MediaFoundationTransformでB8G8R8A8からNV12に変換(処理3)
        assert(_converter);
        RETURN_IF_FAILED(_converter->ProcessInput(0, sample, 0));

        MFT_OUTPUT_DATA_BUFFER buffer = {};
        DWORD status = 0;
        RETURN_IF_FAILED(_converter->ProcessOutput(0, 1, &buffer, &status));

        // 変換後のものを出力映像サンプルに設定(処理4)
        *outSample = buffer.pSample;
    }
    else
    {
        sample->AddRef();
        *outSample = sample;
    }

    return S_OK;
}

戻る

⚠️ **GitHub.com Fallback** ⚠️