MediaFoundationVirtualCamera - HexagramNM/NM_WindowCaptureVirtualCamera GitHub Wiki
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.cpp
でActivator
のインスタンスを生成するところから始まります。以下のように、各クラスがつながっている形です。
-
Activator
(IMFActivate
を継承): 仮想カメラ周りの設定を行い、MediaSource
のインスタンスを生成。 -
MediaSource
(IMFMediaSource
などを継承): メディアソース周りの設定を行い、MediaStream
のインスタンスを生成。 -
MediaStream
(IMFMediaStream
などを継承): メディアストリームや映像形式周りの設定を行い、FrameGenerator
のインスタンスを生成。このRequestSample
メソッドでFrameGenerator
からの映像サンプルを映像として渡している。 -
FrameGenerator
(独自クラス): 仮想カメラに渡す映像サンプルIMFSample
を作る。
NM_WCVCam_MF
では、映像の画素数を変更するためにMediaStream
を、共有テクスチャの画像を映像として表示するためにFrameGenerator
を修正しています。
詳細はMF_WCVCam_MF/FrameGenerator.h
やMF_WCVCam_MF/FrameGenerator.cpp
をご覧ください。
共有テクスチャの取得やテクスチャのコピーを行うため、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
を使用して、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のテクスチャを渡すことができます。
- 共有テクスチャを別のバッファテクスチャにコピー
- コピーしたテクスチャからIMFMediaBufferを作成し、映像サンプルに追加
-
IMFTransform
でフォーマット変換 - 変換後のものを出力映像サンプルに設定
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;
}