WinRTのWindowsGraphicsCaptureAPIでキャプチャしたウィンドウをDirectShowで自作した仮想カメラに映そう

N.Mです。

以前、DirectShowで仮想カメラを作成し、Qiitaに[記事](https://qiita.com/HexagramNM/items/2311f025f3af758c83a0)を投稿しました。
（DirectShowについては、この以前の記事をご覧ください。）

その時はアプリケーションで表示する映像をZoomなどのカメラに表示するために、GDI関数であるBitBltでピクセル情報を取得しました。

今回は古いGDI関数ではなく、WinRTに含まれるWindows.Graphics.Captureという比較的新しいAPIを利用して、アプリケーションの映像をキャプチャし、DirectShowの仮想カメラフィルタに送ってみました。


# BitBltでの問題

ChromeやDiscordなどのアプリケーションでは、ハードウェアアクセラレーションというレンダリングを高速化するための機能があります。これを使用してウィンドウを表示している場合は、BitBltでピクセル情報を取得することができません。

ハードウェアアクセラレーションをONにするとGPU上で描画の処理を行い、その結果をウィンドウに貼り付けますが、BitBltだとGPU上で処理された映像を取得することができず、貼り付ける前の真っ黒な映像が取得されます。

WinRTのWindows.Graphics.Captureでは、GPU上で描画処理をした映像でもDirectXのテクスチャという形で取得することができ、以下のようにハードウェアアクセラレーションをONにしたChromeの映像も仮想カメラに映すことができます。

![imgInZoom.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152791/b98facd5-d5df-30c2-3d68-a627a277347f.png)


# サンプルコード

[Githubリポジトリ](https://github.com/HexagramNM/NMUniversalVCamFilter/tree/Old_cpuProcessing)にあげているので、参考にしてください。


# コンパイル時の注意点

今回、DirectShowというかなり古いAPIとWinRTという新しめのAPIを組み合わせようとしているため、コンパイルの段階で注意すべき点が3つあります。

- 新しめのWindowsSDK (10.0.19041.0)を使用する必要があります。Visual Studio 2017を使用している場合は、[ここ](https://developer.microsoft.com/ja-jp/windows/downloads/windows-10-sdk/)からインストールする必要があります。

- 言語標準をC++17に従う必要があります。Visual Studioでコンパイルする場合は、プロジェクトのプロパティで`C/C++ → 言語 → C++言語標準`を`ISO C++17標準`に設定する必要があります。（WinRTではco_awaitなど新しいC++の標準に則ったプログラミングもされているため。）

- ヘッダファイルでWinRTに関係するヘッダファイルをインクルードする前に`#undef DisplayType`を記述する必要があります。（DirectShowで定義されている`DisplayType`というマクロがWinRTのヘッダファイルで競合しているため。）


# WinRTからDirectShowに映像を渡すまでの流れ

WinRTからDirectShowに映像を渡すまでは、以下のような流れになります。

1. Direct3DDeviceとCPUで読み出し可能なバッファテクスチャの設定

2. キャプチャする対象となるウィンドウ情報をもつ`GraphicsCaptureItem`の取得

3. 1のDirect3DDeviceと2の`GraphicsCaptureItem`から、`Direct3D11CaptureFramePool`を作成

4. 3で作成した`Direct3D11CaptureFramePool`のイベント`FrameArrived`で、キャプチャした画像を1で設定したCPUで読み出し可能なバッファテクスチャにコピー

5. CPUで読み出し可能なバッファテクスチャからピクセル情報を取得

6. DirectShowの`FillBuffer`関数内で、引数である`pSample`に5で取得したピクセル情報を渡す。

この後、1つ1つ詳細を解説していきます。

## Step1. Direct3DDeviceとCPUで読み出し可能なバッファテクスチャの設定

以下の`createDirect3DDevice`をソースフィルタのピンクラスである`CSourceStream`のコンストラクタで呼び出して、Direct3DDeviceの設定をします。設定したDirect3DDeviceを利用して、CPUで読み出し可能なテクスチャの作成も行います。

CPUで読み出し可能なテクスチャはウィンドウサイズの変更に対応できるように、大きめに作成しておきます。

[参考](https://qiita.com/eguo/items/90604787a6098af404d9#direct3d11captureframepool%E3%81%AE%E4%BD%9C%E6%88%90)

```C++

//コンストラクタあたりでこの関数を呼び出します。
void NMVCamPin::createDirect3DDevice() {
    UINT createDeviceFlags = D3D11_CREATE_DEVICE_BGRA_SUPPORT | D3D11_CPU_ACCESS_READ;
#ifdef _DEBUG
    createDeviceFlags |= D3D11_CREATE_DEVICE_DEBUG;
#endif
    //m_dxDevice...IDirect3DDevice型
    //今回取得する目的のオブジェクト。m_dxDeviceはStep3, 4で使用します。
    if (m_dxDevice != nullptr) {
        m_dxDevice.Close();
    }
    com_ptr<ID3D11Device> d3dDevice = nullptr;
    check_hresult(D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE,
    nullptr, createDeviceFlags, nullptr, 0, D3D11_SDK_VERSION,
        d3dDevice.put(), nullptr, nullptr));
    com_ptr<IDXGIDevice> dxgiDevice = d3dDevice.as<IDXGIDevice>();
    com_ptr<::IInspectable> device = nullptr;
    check_hresult(::CreateDirect3D11DeviceFromDXGIDevice(dxgiDevice.get(), device.put()));
    m_dxDevice = device.as<winrt::Windows::Graphics::DirectX::Direct3D11::IDirect3DDevice>();

    //m_deviceCtx...com_ptr<ID3D11DeviceContext>型
    //m_deviceCtxはStep4, 5で使用する。
    d3dDevice->GetImmediateContext(m_deviceCtx.put());

    //CPUから読みだすためのバッファテクスチャ
    //m_bufferTextureDesc...D3D11_TEXTURE2D_DESC型
    //リサイズのために大きめのサイズでテクスチャを作っておきます。
    //（サンプルでは仮想カメラが1280px×720pxに対し、バッファテクスチャは3840px×2160pxで設定しております。）
    m_bufferTextureDesc.Width = MAX_CAP_WIDTH;
    m_bufferTextureDesc.Height = MAX_CAP_HEIGHT;
    m_bufferTextureDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
    m_bufferTextureDesc.ArraySize = 1;
    m_bufferTextureDesc.BindFlags = 0;
    m_bufferTextureDesc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
    m_bufferTextureDesc.MipLevels = 1;
    m_bufferTextureDesc.MiscFlags = 0;
    m_bufferTextureDesc.SampleDesc.Count = 1;
    m_bufferTextureDesc.SampleDesc.Quality = 0;
    m_bufferTextureDesc.Usage = D3D11_USAGE_STAGING;

    //m_bufferTexture...ID3D11Texture2D型
    d3dDevice->CreateTexture2D(&m_bufferTextureDesc, 0, &m_bufferTexture);
}

```

## Step2. `GraphicsCaptureItem`の取得

キャプチャ対象となるウィンドウの情報をもつGraphicsCaptureItemを取得する必要がありますが、WinRTに含まれるGraphicsCapturePickerを使用する方法と、ウィンドウハンドル(HWND)を用いて作成する方法の2通りあります。

前者は長くなるのでTipsに後述します。前者の方法だとユーザがどのウィンドウをキャプチャするか指定することができるため、汎用性は高いです~~が、場合によってピッカーUIが開かないなどの問題があります~~。（ピッカーが開かない問題は解決しました。）

サンプルコードの方では前者のGraphicsCapturePickerを使用する方法でやっております。

後者については以下の記事が参考になります。特定のウィンドウをキャプチャする場合は後者のほうが手っ取り早いです。

[参考](https://qiita.com/eguo/items/90604787a6098af404d9#graphicscaptureitem%E3%81%AE%E4%BD%9C%E6%88%90)

ほぼ後者の記事の引用になってしまいますが、特定ウィンドウ名のGraphicsCaptureItemを取得するコードです。
（サンプルでは前者を採用しているため、このコードはリポジトリには含まれておりません。）

```C++

struct WindowCell {
    HWND window;
    WCHAR name[256];
};

BOOL CALLBACK EnumWndProc(HWND hWnd, LPARAM lParam) {
    //1つのウィンドウについて、指定した名前のウィンドウか確認し、そうならば、lParam（WindowCellのポインタを渡す）に
    //そのウィンドウハンドルを入れて返すコールバック関数。
    WCHAR buff[256] = L"";
    SendMessageTimeoutW(hWnd, WM_GETTEXT, sizeof(buff), (LPARAM)buff, SMTO_BLOCK, 100, NULL);
    if (wcscmp(buff, ((WindowCell *)lParam)->name) == 0) {
        ((WindowCell *)lParam)->window = hWnd;
        return FALSE;
    }
    return TRUE;
}

GraphicsCaptureItem CreateCaptureItemForWindow(HWND hwnd)
{
    namespace abi = ABI::Windows::Graphics::Capture;

    WindowCell wc;
   	wc.window = NULL;
    //キャプチャ対象とするウィンドウ名をwc.nameにコピー
    wcscpy(wc.name, "TARGET_WINDOW_NAME");
    //デスクトップ上にあるウィンドウを走査し、特定ウィンドウ名のウィンドウハンドルを取得します。
    EnumWindows(EnumWndProc, (LPARAM)&wc);

    //以降のコードは参考記事からほぼ引用
    auto factory = get_activation_factory<GraphicsCaptureItem>();
    auto interop = factory.as<::IGraphicsCaptureItemInterop>();
    GraphicsCaptureItem item{ nullptr };
    if (wc.window != NULL) {
        check_hresult(interop->CreateForWindow(wc.window, guid_of<abi::IGraphicsCaptureItem>(),       
        reinterpret_cast<void**>(put_abi(item))));
    }
    return item;
}

```

## Step3. `Direct3D11CaptureFramePool`の作成
Direct3DDeviceとGraphicsCaptureItemを取得することができれば、以下のようなコードでキャプチャを開始することができます。

この時に`GraphicsCaptureSession.IsCursorCaptureEnabled(bool)`を使用して、マウスカーソルもキャプチャするかの設定も可能です。

```C++

//m_graphicsCaptureItem...GraphicsCaptureItem型
//ここに取得したGraphicsCaptureItemを入れます。
m_graphicsCaptureItem = targetCaptureItem;

//m_capWinSize...SizeInt32型
//現在のウィンドウのサイズを格納し、ウィンドウのサイズが変更されたかを検出するためのデータ
m_capWinSize = m_graphicsCaptureItem.Size();

//m_capWinSizeInTexture...D3D11_BOX型
//こちらも現在のウィンドウサイズを格納しますが、Step4のテクスチャのコピーの際に使用します。
m_capWinSizeInTexture.right = m_capWinSize.Width;
m_capWinSizeInTexture.bottom = m_capWinSize.Height;

//m_framePool...Direct3D11CaptureFramePool型
m_framePool = Direct3D11CaptureFramePool::CreateFreeThreaded(m_dxDevice, DirectXPixelFormat::B8G8R8A8UIntNormalized, 2, m_capWinSize);

//m_frameArrived...event_revoker<IDirect3D11CaptureFramePool>型
//1フレームキャプチャできた時に呼ばれるイベントFrameArrivedを設定します。詳細はStep4
m_frameArrived = m_framePool.FrameArrived(auto_revoke, { this, &NMVCamPin::onFrameArrived });

//m_captureSession...GraphicsCaptureSession型
//このオブジェクトを介してキャプチャを開始できます。
m_captureSession = m_framePool.CreateCaptureSession(m_graphicsCaptureItem);

//IsCursorCaptureEnabledでマウスカーソルもキャプチャするか指定できます。
//trueでカーソルも入り、falseでカーソルは消えます。
m_captureSession.IsCursorCaptureEnabled(false);
m_captureSession.StartCapture();

```

## Step4. `Direct3D11CaptureFramePool.FrameArrived`で`Direct3D11CaptureFrame`の取得
Step3で`m_frameArrived`にイベントを設定していましたが、そのイベント時に呼び出す処理が以下の通りです。

`onFrameArrived`の引数である`sender`の`TryGetNextFrame`でキャプチャしたフレームを取得できます。これを`ID3D11Texture2D`のテクスチャの形に変換し、CPUで読み出し可能なバッファテクスチャにコピーします。

[参考](http://www-fps.nifs.ac.jp/ito/memo/d3dx11_02.html)

`ID3D11DeviceContext::CopySubresourceRegion`（[詳細](https://docs.microsoft.com/en-us/windows/win32/api/d3d11/nf-d3d11-id3d11devicecontext-copysubresourceregion)）
でCPUで読み出し可能なテクスチャの一部にコピーするようにしております。ここを参考サイトのように`ID3D11DeviceContext::CopyResource`を使用してコピーしようとすると、コピー元とコピー先でサイズが一致する必要があります。結果としてウィンドウのリサイズに対応しようとするといちいちCPUで読み出し可能なテクスチャをそのウィンドウサイズに合わせて作り直す必要が発生し、処理が重くなってしまいます。

```C++

void NMVCamPin::onFrameArrived(Direct3D11CaptureFramePool const &sender,
    winrt::Windows::Foundation::IInspectable const &args)
{
    auto frame = sender.TryGetNextFrame();

    SizeInt32 itemSize = frame.ContentSize();
    if (itemSize.Width <= 0) {
        itemSize.Width = 1;
    }
    if (itemSize.Height <= 0) {
        itemSize.Height = 1;
    }
    if (itemSize.Width != m_capWinSize.Width || itemSize.Height != m_capWinSize.Height) {
        //ウィンドウサイズが変更された場合の処理。
        //Direct3D11CaptureFramePoolのRecreateメソッドでキャプチャ中も簡単にサイズ変更ができます。
        m_capWinSize.Width = itemSize.Width;
        m_capWinSize.Height = itemSize.Height;
        m_capWinSizeInTexture.right = m_capWinSize.Width;
        m_capWinSizeInTexture.bottom = m_capWinSize.Height;
        m_framePool.Recreate(m_dxDevice, DirectXPixelFormat::B8G8R8A8UIntNormalized, 2, m_capWinSize);
    }

    //frameからID3D11Texture2Dに変換します。
    com_ptr<ID3D11Texture2D> texture2D = getDXGIInterfaceFromObject<::ID3D11Texture2D>(frame.Surface());

    //CPU読み込み可能なバッファテクスチャにGPU上でデータコピー
    m_deviceCtx->CopySubresourceRegion(m_bufferTexture, 0, 0, 0, 0,
        texture2D.get(), 0, &m_capWinSizeInTexture);

    //Step5の処理
    convertFrameToBits();
}
```

## Step5. CPUで読み出し可能なバッファテクスチャからピクセル情報の取得
::: note info
記事執筆当時はテクスチャから取り出した画像に対し、CPUでフォーマット修正をしていました。この部分はDirectXのCompute Shaderを使用することで、最適化することができます。詳細は以下の記事をご覧ください。

[ComputeShaderでの最適化例: DirectShowの仮想カメラ](https://qiita.com/HexagramNM/items/b75ca3894b88bdaf1c0b)
:::

CPUで読み出し可能なテクスチャにコピーできれば、`D3D11_MAPPED_SUBRESOURCE`にマップすることで、キャプチャされた画像の画素にアクセスできます。この画素情報を`unsigned char`型の配列に格納していきます。

```C++
void NMVCamPin::convertFrameToBits() {
    //m_frameBitsはunsigned charのポインタで、コンストラクタで（仮想カメラの横ピクセル数×仮想カメラの縦ピクセル数×3）の要素数の配列として
    //動的生成しております。1画素につきRGBの計24ビット=3バイトであるため×3となっております。

    D3D11_MAPPED_SUBRESOURCE mapd;
    HRESULT hr;
    hr = m_deviceCtx->Map(m_bufferTexture, 0, D3D11_MAP_READ, 0, &mapd);
    const unsigned char *source = static_cast<const unsigned char *>(mapd.pData);
    int texBitPos = 0;

    //取得したピクセル情報からビットマップを作る処理

    //説明を簡単にするためのコードであるため、以下のコードだとキャプチャされた画像がそのままのサイズで上下反転した状態で出ます。
    //仮想カメラ内に収める場合については、サンプルリポジトリのNMVCamPin::changePixelPos()やNMVCamPin::convertFrameToBits()をご覧ください。
    int pixelPosition = 0;
    int bitPosition = 0;
    for (int y = 0; y < WINDOW_HEIGHT; y++) {
        for (int x = 0; x < WINDOW_WIDTH; x++) {
            texBitPos = mapd.RowPitch * y + 4 * x;
            for (int cIdx = 0; cIdx < 3; cIdx++) {
                m_frameBits[bitPosition + cIdx] = source[texBitPos + cIdx];
            }
            pixelPosition++;
            bitPosition += 3;
        }
    }
    m_deviceCtx->Unmap(m_bufferTexture, 0);
}
```

## Step6. DirectShowの`FillBuffer`関数での、ピクセル情報の送信
あとは継承しているソースフィルタのピンクラス`CSourceStream`の`FillBuffer`メソッドで、以下のようにStep5で取得した画素情報を送ることで、仮想カメラにキャプチャした映像が表示されます。

`FillBuffer`メソッドの最後に`sleep_for`メソッドを入れ、適度に他のスレッドにCPU時間を渡すと、仮想カメラ起動中もZoomの応答が良くなりました。（なかった場合は、Zoomのボタンがなかなか反応しませんでした。）

```C++

HRESULT NMVCamPin::FillBuffer(IMediaSample *pSample) {
    HRESULT hr=E_FAIL;
    CheckPointer(pSample,E_POINTER);
    // ダウンストリームフィルタが
    // フォーマットを動的に変えていないかチェック
    ASSERT(m_mt.formattype == FORMAT_VideoInfo);
    ASSERT(m_mt.cbFormat >= sizeof(VIDEOINFOHEADER));

    LPBYTE pSampleData=NULL;
    const long size=pSample->GetSize();
    //ここで画素情報を送るべきメモリへのポインタpSampleDataを取得しています。
    pSample->GetPointer(&pSampleData);

    //中略

    //pSampleDataへ画素情報をコピー
    //WINDOW_WIDTH...仮想カメラの横の画素数
    //WINDOW_HEIGHT...仮想カメラの縦の画素数
    memcpy(pSampleData, m_frameBits, WINDOW_WIDTH * WINDOW_HEIGHT * 3);

    //中略
    //CPU使用率を抑えて、ZoomなどのUIの反応をしやすくするために適度にSleepします。
    std::this_thread::sleep_for(std::chrono::milliseconds(10));
}

```

# Tips

## 終了時やウィンドウ切替え時の後始末
ウィンドウを切り替える時やキャプチャを終了するときは、一度以下のようなコードで後始末をする必要があります。

[参考](https://github.com/opysky/examples/blob/master/winrt/GraphicsCapture/CapturePreview/CaptureView.cpp)

```C++

void NMVCamPin::stopCapture() {
    if (m_framePool != nullptr) {
        //FrameArrivedイベントの取り消し
        m_frameArrived.revoke();

        //GraphicsCaptureSessionの後始末
        m_captureSession = nullptr;

        //Direct3D11CaptureFramePoolの後始末
        m_framePool.Close();
        m_framePool = nullptr;

        //前に使用していたGraphicsCaptureItemの後始末
        m_graphicsCaptureItem = nullptr;
    }
}

```

ただ、この後始末は`Direct3D11CaptureFramePool`や`GraphicsCaptureSession`を生成したスレッドと同じスレッドで行う必要があります。別のスレッドで後始末をしてしまうと`wrong_thread`という例外が発生し、Visual Studioでは「別スレッド用にマーシャリングされたインタフェースを呼び出しました」というメッセージが表示され、止まってしまいます。

キャプチャするウィンドウをいつでも切り替えられるように、Step3を`CSourceStream`の`FillBuffer`メソッドで行っていた場合は、後始末を`CSourceStream`クラスのデストラクタで呼ぶと上記の`wrong_thread`の例外が発生します。`CSourceStream`は別スレッドで`FillBuffer`を実行しているようで、デストラクタが実行されるスレッドとは異なるようです。

`CSourceStream`クラスには、カメラ切替えなどで終了し、`FillBuffer`を実行していたスレッドが破棄される際に`OnThreadDestroy`という仮想メソッドが呼ばれるため、このメソッドをオーバーライドし、ここで後始末をすれば、例外は発生しません。

```C++

HRESULT	NMVCamPin::OnThreadDestroy() {
    stopCapture();
    return NOERROR;
}

```

## GraphicsCapturePickerの利用

GraphicsCapturePickerを起動する際に、別スレッドで起動をするようにしました。
（サンプルコードでは、`FillBuffer`メソッド内でDirectInputによるキー入力を受け付けることで、起動しております。）

```C++

//m_isSelectingWindow...bool型
//ピッカーでウィンドウを選択中かどうかを管理します。
m_isSelectingWindow = true;
m_capturePickerThread = new std::thread([](NMVCamPin *inst) {inst->openCaptureWindowPicker(); }, this);

```

生成した別スレッドでは、以下のような処理をしております。（[参考](https://qiita.com/eguo/items/90604787a6098af404d9#graphicscaptureitem%E3%81%AE%E4%BD%9C%E6%88%90)）
UWPアプリではない場合は、ピッカーをウィンドウと関連付ける必要があるため、ウィンドウを生成し、ピッカーの選択肢から生成ウィンドウを外すために`SetWindowDisplayAffinity`で設定をしています。

また、ピッカーを開く前に、`init_apartment();`をする必要がありますが、`CSourceStream`のメソッドでやると、仮想カメラを開くアプリケーションによっては例外が発生してしまいます。
（`init_apartment();`はWinRTのCOMの初期化をしているようです。仮想カメラを開く頃にはアプリケーション側で初期化が済んでおり、この時の初期化の設定（STAかMTA）がアプリケーションで行う初期化での設定とずれていると例外が発生します。）

そのため、別スレッドでピッカーの起動処理を行っているわけです。

```C++

void NMVCamPin::openCaptureWindowPicker() {
    //参考にした記事にSingle Thread Apartmentに指定したほうが良いという情報がありました。
    init_apartment(winrt::apartment_type::single_threaded);

    //ピッカーに関連付けるウィンドウの生成します。
	m_attatchedWindow = CreateWindowW(L"STATIC", L"NMUniversalVCam", SS_WHITERECT,
	    0, 0, 300, 1, NULL, NULL, GetModuleHandleW(NULL), NULL);
	  
    //ピッカーが正常に開くように、生成したウィンドウをいったん最小化して復元する必要があります。
    //これにより生成ウィンドウがフォアグラウンド化します。
    ShowWindow(m_attatchedWindow, SW_MINIMIZE);
    ShowWindow(m_attatchedWindow, SW_RESTORE);

    //SW_SHOWだと一部のアプリケーションでピッカーが開かなくなります。
    //ShowWindow(m_attatchedWindow, SW_SHOW);
	
    UpdateWindow(m_attatchedWindow);

	//ピッカーを開くために作ったウィンドウをピッカーの選択肢から除外します。
	SetWindowDisplayAffinity(m_attatchedWindow, WDA_EXCLUDEFROMCAPTURE);
	updateAttatchedWindow();

    //ウィンドウとピッカーとの関連づけます。
	GraphicsCapturePicker picker;
	auto interop = picker.as<::IInitializeWithWindow>();
	m_pickerResult = interop->Initialize(m_attatchedWindow);

    //ピッカーからの結果を受け取るためのオブジェクトをm_graphicsCaptureAsyncResultに格納します。
	m_graphicsCaptureAsyncResult = picker.PickSingleItemAsync();

	while (m_isSelectingWindow) {
        //結果を受け取るまで生成したウィンドウへのメッセージを処理します。
        //FillBufferのスレッドで結果を受け取ると、m_isSelectingWindowがfalseになります。
	    updateAttatchedWindow();
	    std::this_thread::sleep_for(std::chrono::milliseconds(10));
	}
	updateAttatchedWindow();
}

void NMVCamPin::updateAttatchedWindow() {
	MSG msg;
    //GetMessageだとメッセージを受け取るまでここでブロックしてしまうので、
    //PeekMessageを使用します。
	if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE) > 0) {
	    TranslateMessage(&msg);
	    DispatchMessage(&msg);
    }
}

```

仮想カメラピンクラスの`FillBuffer`メソッド側では、以下のようにしてピッカーからの結果を受け取ります。

`GraphicsCapturePicker`に関する参考記事等では`m_graphicsCaptureAsyncResult`に対し`co_await`を使用することで、待機しているようですが、co_awaitを使用すると、使用した関数（今回だと`FillBuffer`メソッド）の返り値も`IAsyncAction`にする必要があります。

[参考](https://stackoverflow.com/questions/52840223/co-await-expression-needs-await-ready-function)

`FillBuffer`メソッドはオーバーライドしたメソッドで、返り値を変更できないため、手動で待機処理を再現しております。

```C++

if (m_isSelectingWindow) {
    if (m_graphicsCaptureAsyncResult) {
        //ピッカーから結果が来ると、statusがCompletedになります。
        //FillBufferで毎回このstatusを確認することで、ピッカーから結果が返ってきたかどうかを確認します。
        winrt::Windows::Foundation::AsyncStatus status = m_graphicsCaptureAsyncResult.Status();
        if (status == winrt::Windows::Foundation::AsyncStatus::Completed) {
            GraphicsCaptureItem tmpTarget = m_graphicsCaptureAsyncResult.GetResults();

            //changeWindowで、生成ウィンドウの破棄とStep3のDirect3D11CaptureFramePool生成処理を行っております。
            changeWindow(tmpTarget);
            m_isSelectingWindow = false;

            //ピッカー生成のために起動した別スレッドの終了
            m_capturePickerThread->join();
            delete m_capturePickerThread;
            m_capturePickerThread = nullptr;
        }
    }
}

```

~~ただ、ここまでの方法だと一部のアプリケーションではうまくピッカーが開きませんでした。別スレッドで生成するウィンドウは開きますが、`GraphicsCapturePicker`のピッカーが立ち上がらず、すぐに生成したウィンドウが閉じてしまいます。~~

~~ZoomやFireFox, OpenCVでカメラ管理をする自作アプリではピッカーが開きましたが、DiscordやChrome, Slackでは開きませんでした。この部分は時間があるときに調査しようと思います。（ご存知の方がいらっしゃれば、コメント等で教えていただけると幸いです。）~~

### 2024/11/30 追記

3年半ぶりに調査したところ、原因が判明しました。ピッカーを開く際、ピッカーに紐づいているウィンドウがフォアグラウンド化されている必要があったようです。一部のアプリケーション（DiscordやChromeなど）ではウィンドウを生成、表示しただけではフォアグラウンド化されていなかったようです。

`SetForegroundWindow`という関数がありますが、仮想カメラが動いているプロセスに権限がないため、失敗してしまいました。（`AllowSetForegroundWindow`も権限の関係で失敗してしまいました。）ウィンドウを一旦最小化してから、復元することでフォアグラウンド化するらしく、今回はこれで対応しております。 [^1]

[^1]: ChatGPTからの入れ知恵です。公式ドキュメントにはGraphicsCapturePickerにフォアグラウンドやフォーカスが関係あるという記述はありませんでした。しかし、ChatGPTによるとピッカーがこれらの影響で動作しないという話が開発者フォーラムで報告されていたりするそうです。そのソースを追うことはできていませんが、実際この修正で動作するようになったので、深追いせずそういうことにしておきます。

# まとめ

前回の仮想カメラの記事から発展して、新しめであるWinRTのAPIを古いDirectShowでも利用できるという話でした。これを利用すれば、Zoomなどにある元々の画面共有に加えて自分のカメラも画面共有として利用することもできるでしょう。