LoginSignup
36
46

More than 3 years have passed since last update.

Windows 10のウィンドウキャプチャAPI

Last updated at Posted at 2019-11-09

Windows.Graphics.Capture

自分のプロセスから参照できるウィンドウだけでなく、OS上で表示されている他のプロセスのウィンドウの内容をキャプチャしたい場合、従来であれば太古のGDI関数(もはやまともに動くか怪しい)や、DwmGetDxSharedSurface(隠しAPIなのでお行儀が悪い上にDWM内で同期が取れていないのか取得できるイメージが中途半端)などが利用されてきたようですが、Windows 10 version 1803でキャプチャ専用のAPIがWinRTに用意されました。

Screen capture
Starting in Windows 10, version 1803, the Windows.Graphics.Capture namespace provides APIs to acquire frames from a display or application window, to create video streams or snapshots to build collaborative and interactive experiences.

Windows.Graphics.Captureが1803で実装された当初は、Win32的なデスクトップアプリで動作しない、WindowsのエディションがProやEnterpriseを要求されるなど、ちょっと意味のわからない制限がありましたが、それらも緩和されているのでようやく使い物になりそうです。

このAPIの最大のメリットはウィンドウのイメージをGPUリソース(::ID3D11Texture2D)のまま渡してくれるところでしょうか。例えばキャプチャイメージを3Dシーンに張り付けたり、ハードウェアエンコーダに渡したりといった処理が低負荷で行える可能性が広がります。

キャプチャAPI自体も低負荷で動作するので、今までDWMでプライベートに管理されていたウィンドウ毎のサーフェイスをGPUのみを介してアプリケーション側に転送しているものと思われます。

キャプチャされたイメージをGPUリソースのまま取得できる標準APIはWin8の時点でDesktop Duplication APIが提供されていましたが、名前の通り取得できるのはデスクトップ丸ごとのイメージのみでした。

渡されるイメージもDwmGetDxSharedSurfaceとは異なりDirect3DやOpenGLの結果が正常に反映されており、アクリル効果やSetWindowCompositionAttributeの効果が反映された後のイメージで取得されます。
2019-11-09 221522.png
タスクビューやピークで表示されるサムネイルのオリジナルと同等のイメージが取得できると思って良さそうです(いくつかの手法で実装される非矩形ウィンドウがどのようにキャプチャされるかは今回試してませんが)。

注意点として、キャプチャ対象がウィンドウの場合は例えキャプチャしているウィンドウ上に表示されていてもポップアップウィンドウはキャプチャされません。よってポップアップメニューやツールチップの類は基本的にキャプチャされません。また、プッシュ式でイメージが流れてくるためエンコードや配信用途で扱いやすく、スナップショットのみの用途では若干扱いにくい構成になっています。

C++デスクトップアプリでの実装

Windows.Graphics.CaptureはWinRTのAPIとして提供されています。C++言語でWinRTのAPIを呼び出すにはいくつか方法がありますが、ここでは最新の手法となるC++/WinRTを使用します。

用意したサンプルはこちら

公式ドキュメントには基本的にUWPアプリでの手順で書かれており、デスクトップアプリでも流れは変わりませんが、いくつか専用の手続きや注意点が必要になります。

スレッドアパートメントの初期化

メインとなるUIスレッドをSTAとして設定します。STAにしないと後述するピッカーが適切に動作しない、co_awaitで元のスレッドに戻ってこないなどいつくかの不都合が生じます。

main.cpp
init_apartment(apartment_type::single_threaded);

GraphicsCaptureItemの作成

まずキャプチャ対象となるウィンドウあるいはモニタを指定したGraphicsCaptureItemオブジェクトを作成します。これには専用ピッカーであるGraphicsCapturePickerを起動して取得するのが一番簡単です。これはアプリケーションウィンドウ及びモニタの一覧がモーダルダイアログで表示され、ユーザー自身に選択を促すことができます。
2019-11-09 222224.png

main.cpp
fire_and_forget OnButtonClicked(UINT, CPoint const&) {
  GraphicsCapturePicker picker;
  auto interop = picker.as<::IInitializeWithWindow>();
  // オーナーとなるウィンドウのハンドルを渡す
  interop->Initialize(m_hWnd); 

  auto item = co_await picker.PickSingleItemAsync();
  if (!!item) {
    _captureView.StartCapture(item);
  }
}

UWPアプリでは単純に既定のコンストラクタからインスタンスを作成するだけで機能しますが、デスクトップアプリの場合はGraphicsCapturePicker.PickSingleItemAsyncメソッドを呼び出す前に::IInitializeWithWindowというインターフェイスを介してオーナーウィンドウを手動で設定する必要があります。

PickSingleItemAsyncは名前の通りWinRTの非同期関数です。これらは戻り値が非同期操作の型でラップされていて、実際の値を取得するにはC++/CXならばconcurrency::taskの継続チェーンなどで記述する必要がありましたが、C++/WinRTではC++20のコルーチン拡張に則ったアダプタなどが実装されており、良い感じにイマドキのasync/await風に記述できるようになっています。

デスクトップアプリの場合、ウィンドウハンドルやモニタのハンドルを渡して直接GraphicsCaptureItemを作成することもできます。

CaptureView.cpp
auto CreateCaptureItemForWindow(HWND hwnd)
{
  namespace abi = ABI::Windows::Graphics::Capture;

  auto factory = get_activation_factory<GraphicsCaptureItem>();
  auto interop = factory.as<::IGraphicsCaptureItemInterop>();
  GraphicsCaptureItem item{ nullptr };
  check_hresult(interop->CreateForWindow(hwnd, guid_of<abi::IGraphicsCaptureItem>(), reinterpret_cast<void**>(put_abi(item))));
  return item;
}

正直C++/WinRTの情報自体が公式ドキュメントにすら少ないので、このへんの手続きはMicrosoftのサンプルだけが頼りですねー。

Direct3D11CaptureFramePoolの作成

OSからキャプチャイメージを受け取り、アプリ側に渡してくれるのがDirect3D11CaptureFramePoolです。名前の通りDirect3D 11を基盤としており、Direct3D 11のデバイスとバッファのフォーマットやサイズを指定して作成します。

ここでC++からDirect3Dデバイスを渡す方法ですが、あらゆる言語から呼び出せることを目的としたWinRTには例えばインターフェイスポインタをIntPtrに割り当てて引数として渡すような雑な(?)シグネチャはありません。代わりに相互運用の為のWindows.Graphics.DirectX.Direct3D11.IDirect3DDeviceという型がWinRTに定義されているのでこれを仲介します。

ネイティブの::ID3D11DeviceWindows.Graphics.DirectX.Direct3D11.IDirect3DDeviceの相互変換にはヘルパーとなるインターフェイスや関数がWindows SDKに定義されています。::CreateDirect3D11DeviceFromDXGIDevice::Windows::Graphics::DirectX::Direct3D11::IDirect3DDxgiInterfaceAccessがそれにあたります。

Direct3DHelper.h
auto CreateDirect3DDevice()
{
    UINT createDeviceFlags = D3D11_CREATE_DEVICE_BGRA_SUPPORT;
#ifdef _DEBUG
    createDeviceFlags |= D3D11_CREATE_DEVICE_DEBUG;
#endif
    com_ptr<ID3D11Device> d3dDevice;
    check_hresult(D3D11CreateDevice(
        nullptr, 
        D3D_DRIVER_TYPE_HARDWARE,
        nullptr, 
        createDeviceFlags, 
        nullptr, 
        0, 
        D3D11_SDK_VERSION, 
        d3dDevice.put(), 
        nullptr, 
        nullptr));

    // ID3D11DeviceからWinRTのIDirect3DDeviceを作成
    auto dxgiDevice = d3dDevice.as<IDXGIDevice>();
    com_ptr<::IInspectable> device;
    check_hresult(::CreateDirect3D11DeviceFromDXGIDevice(dxgiDevice.get(), device.put()));
    return device.as<winrt::Windows::Graphics::DirectX::Direct3D11::IDirect3DDevice>();
}

template <typename T>
auto GetDXGIInterfaceFromObject(winrt::Windows::Foundation::IInspectable const& object)
{
    // WinRTの型からネイティブのインターフェイスを取得
    auto access = object.as<::Windows::Graphics::DirectX::Direct3D11::IDirect3DDxgiInterfaceAccess>();
    com_ptr<T> result;
    check_hresult(access->GetInterface(guid_of<T>(), result.put_void()));
    return result;
}

なんだかもうWin32の型なのかWinRTの型なのかC++/WinRTの型なのかワケがわからなくなってきますねー。とまれ、Windows.Graphics.DirectX.Direct3D11.IDirect3DDeviceが入手できればDirect3D11CaptureFramePoolが作成できます。

CaptureView.cpp
    _framePool = Direct3D11CaptureFramePool::Create(
        _device,
        DirectXPixelFormat::B8G8R8A8UIntNormalized,
        2,
        item.Size());

    _frameArrived = _framePool.FrameArrived(auto_revoke, { this, &CaptureView::OnFrameArrived });

バッファのサイズは自由に決められますが、キャプチャ対象となるウィンドウより小さくてもまず意味がないのでGraphicsCaptureItem.Sizeプロパティと同じにすれば良いでしょう。

Direct3D11CaptureFramePool.FrameArrivedの部分はC++/WinRT流のイベント登録です。トークンを返すパターンとこのリボーカーを返すパターンがありますが、リボーカーの方が結合が緩くデストラクタで接続解除してくれるのでお勧めです。

キャプチャ開始

CaptureView.cpp
    _captureSession = _framePool.CreateCaptureSession(item);
    _captureSession.StartCapture();

Direct3D11CaptureFramePool.CreateSessionGraphicsCaptureItemを渡しGraphicsCaptureSessionを取得、GraphicsCaptureSession.StartCaptureメソッドで実際のキャプチャが開始されます。

Direct3D11CaptureFramePool.FrameArrivedイベント

キャプチャを開始すると、Direct3D11CaptureFramePoolにキャプチャフレームが届いて利用可能になったタイミングでDirect3D11CaptureFramePool.FrameArrivedイベントが発行されます。試行した環境下では対象となるウィンドウ自身のレンダリング頻度に関わらずvsyncの間隔で発行されました。

ちなみに、Direct3D11CaptureFramePool.Createメソッドで作成した場合FrameArrivedイベントは作成元と同じスレッド(UIスレッド)から呼び出されます。独自のスレッドを作ってそこからFrameArrivedイベントが発行されるDirect3D11CaptureFramePool.CreateFreeThreadedメソッドも存在します。

CaptureView.cpp
void CaptureView::OnFrameArrived(
    Direct3D11CaptureFramePool const& sender, 
    winrt::Windows::Foundation::IInspectable const& args)
{
    auto frame = sender.TryGetNextFrame();
    auto frameSurface = GetDXGIInterfaceFromObject<::ID3D11Texture2D>(frame.Surface());
    auto contentSize = frame.ContentSize();

OnFrameArrivedイベントにも引数はあるのですが、なぜか目的のイメージは発行元のDirect3D11CaptureFramePoolTryGetNextFrameメソッドから取得する流れになるようです。Direct3D11CaptureFrame.SurfaceプロパティからWindows.Graphics.DirectX.Direct3D11.IDirect3DSurfaceを取得し、前述のヘルパーを介して::ID3D11Texture2Dとしてアクセスできます。D3D11_BIND_SHADER_RESOURCEも割り当てられているのでシェーダの入力として扱うことが可能です。

キャプチャ中に対象となるウィンドウのサイズが変更されたらどうなるか? という点ですが、これは手動でDirect3D11CaptureFramePoolのバッファを再作成して追従させる必要があります。

CaptureView.cpp
    // Direct3D11CaptureFramePoolは作成時のパラメータを取得するプロパティが無いのでフレームから取得する
    auto surfaceDesc = frame.Surface().Description();
    auto itemSize = _captureItem.Size();
    if (itemSize.Width != surfaceDesc.Width || itemSize.Height != surfaceDesc.Height) {
        SizeInt32 size;
        size.Width = std::max(itemSize.Width, 1);
        size.Height = std::max(itemSize.Height, 1);
        _framePool.Recreate(_device, DirectXPixelFormat::B8G8R8A8UIntNormalized, 2, size);
    }

再作成用のDirect3D11CaptureFramePool.Recreateメソッドがあるのでキャプチャのセッションを停止せずにバッファサイズを変更することができます。

おしまい

VistaからDWMが導入されて10年以上経過した今、ようやく素直にウィンドウのサーフェイスを差し出してくれるまでにデレてくれた感がありますねー。

36
46
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
36
46