Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qffmpegwindowcapture_uwp.cpp
Go to the documentation of this file.
1// Copyright (C) 2022 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
6
7#include <QtCore/qloggingcategory.h>
8#include <QtCore/qthread.h>
9#include <QtCore/private/qfactorycacheregistration_p.h>
10#include <QtCore/private/qsystemerror_p.h>
11#include <QtGui/qguiapplication.h>
12#include <QtGui/qwindow.h>
13#include <QtGui/qpa/qplatformscreen_p.h>
14#include <QtMultimedia/qabstractvideobuffer.h>
15#include <QtMultimedia/qvideoframe.h>
16#include <QtMultimedia/private/qcapturablewindow_p.h>
17#include <QtMultimedia/private/qmultimediautils_p.h>
18#include <QtMultimedia/private/qvideoframe_p.h>
19
20#include <unknwn.h>
21#include <winrt/base.h>
22// Workaround for Windows SDK bug.
23// See https://github.com/microsoft/Windows.UI.Composition-Win32-Samples/issues/47
24namespace winrt::impl
25{
26template <typename Async>
27auto wait_for(Async const& async, Windows::Foundation::TimeSpan const& timeout);
28}
29#include <winrt/Windows.Foundation.Collections.h>
30#include <winrt/Windows.Graphics.Capture.h>
31#include <winrt/Windows.Graphics.DirectX.h>
32#include <winrt/Windows.Graphics.DirectX.Direct3D11.h>
33#include <Windows.Graphics.Capture.h>
34#include <Windows.Graphics.Capture.Interop.h>
35#include <windows.graphics.directx.direct3d11.interop.h>
36
37#include <D3d11.h>
38#include <dwmapi.h>
39#include <lowlevelmonitorconfigurationapi.h>
40#include <physicalmonitorenumerationapi.h>
41
42#include <memory>
43#include <system_error>
44
45QT_BEGIN_NAMESPACE
46
47using namespace Qt::StringLiterals;
48
49using namespace winrt::Windows::Graphics::Capture;
50using namespace winrt::Windows::Graphics::DirectX;
51using namespace winrt::Windows::Graphics::DirectX::Direct3D11;
52using namespace Windows::Graphics::DirectX::Direct3D11;
53
54using winrt::check_hresult;
55using winrt::com_ptr;
56using winrt::guid_of;
57
58namespace {
59
60Q_LOGGING_CATEGORY(qLcWindowCaptureUwp, "qt.multimedia.ffmpeg.windowcapture.uwp");
61
62winrt::Windows::Graphics::SizeInt32 getWindowSize(HWND hwnd)
63{
64 RECT windowRect{};
65 ::GetWindowRect(hwnd, &windowRect);
66
67 return { windowRect.right - windowRect.left, windowRect.bottom - windowRect.top };
68}
69
70QSize asQSize(winrt::Windows::Graphics::SizeInt32 size)
71{
72 return { size.Width, size.Height };
73}
74
75struct MultithreadedApartment
76{
77 MultithreadedApartment(const MultithreadedApartment &) = delete;
78 MultithreadedApartment &operator=(const MultithreadedApartment &) = delete;
79
80 MultithreadedApartment() { winrt::init_apartment(); }
81 ~MultithreadedApartment() { winrt::uninit_apartment(); }
82};
83
84class QUwpTextureVideoBuffer : public QAbstractVideoBuffer
85{
86public:
87 QUwpTextureVideoBuffer(com_ptr<IDXGISurface> &&surface) : m_surface(surface) { }
88
89 ~QUwpTextureVideoBuffer() override { Q_ASSERT(m_mapMode == QVideoFrame::NotMapped); }
90
91 MapData map(QVideoFrame::MapMode mode) override
92 {
93 if (m_mapMode != QVideoFrame::NotMapped)
94 return {};
95
96 if (mode == QVideoFrame::ReadOnly) {
97 DXGI_MAPPED_RECT rect = {};
98 HRESULT hr = m_surface->Map(&rect, DXGI_MAP_READ);
99 if (SUCCEEDED(hr)) {
100 DXGI_SURFACE_DESC desc = {};
101 hr = m_surface->GetDesc(&desc);
102
103 MapData md = {};
104 md.planeCount = 1;
105 md.bytesPerLine[0] = rect.Pitch;
106 md.data[0] = rect.pBits;
107 md.dataSize[0] = rect.Pitch * desc.Height;
108
109 m_mapMode = QVideoFrame::ReadOnly;
110
111 return md;
112 } else {
113 qCDebug(qLcWindowCaptureUwp)
114 << "Failed to map DXGI surface" << QSystemError::windowsComString(hr);
115 return {};
116 }
117 }
118
119 return {};
120 }
121
122 void unmap() override
123 {
124 if (m_mapMode == QVideoFrame::NotMapped)
125 return;
126
127 const HRESULT hr = m_surface->Unmap();
128 if (FAILED(hr))
129 qCDebug(qLcWindowCaptureUwp)
130 << "Failed to unmap surface" << QSystemError::windowsComString(hr);
131
132 m_mapMode = QVideoFrame::NotMapped;
133 }
134
135 QVideoFrameFormat format() const override { return {}; }
136
137private:
138 QVideoFrame::MapMode m_mapMode = QVideoFrame::NotMapped;
139 com_ptr<IDXGISurface> m_surface;
140};
141
142struct WindowGrabber
143{
144 WindowGrabber() = default;
145
146 WindowGrabber(IDXGIAdapter1 *adapter, HWND hwnd)
147 : m_captureWindow{ hwnd }, m_frameSize{ getWindowSize(hwnd) }
148 {
149 check_hresult(D3D11CreateDevice(adapter, D3D_DRIVER_TYPE_UNKNOWN, nullptr, 0, nullptr, 0,
150 D3D11_SDK_VERSION, m_device.put(), nullptr, nullptr));
151
152 const auto captureItem = createCaptureItem(hwnd);
153
154 m_framePool = Direct3D11CaptureFramePool::CreateFreeThreaded(
155 getCaptureDevice(m_device), m_pixelFormat, 1,
156 captureItem.Size());
157
158 m_session = m_framePool.CreateCaptureSession(captureItem);
159
160 // If supported, enable cursor capture
161 if (const auto session2 = m_session.try_as<IGraphicsCaptureSession2>())
162 session2.IsCursorCaptureEnabled(true);
163
164 // If supported, disable colored border around captured window to match other platforms
165 if (const auto session3 = m_session.try_as<IGraphicsCaptureSession3>())
166 session3.IsBorderRequired(false);
167
168 m_session.StartCapture();
169 }
170
171 ~WindowGrabber()
172 {
173 m_framePool.Close();
174 m_session.Close();
175 }
176
177 com_ptr<IDXGISurface> tryGetFrame()
178 {
179 const Direct3D11CaptureFrame frame = m_framePool.TryGetNextFrame();
180 if (!frame) {
181
182 // Stop capture and report failure if window was closed. If we don't stop,
183 // testing shows that either we don't get any frames, or we get blank frames.
184 // Emitting an error will prevent this inconsistent behavior, and makes the
185 // Windows implementation behave like the Linux implementation
186 if (!IsWindow(m_captureWindow))
187 throw std::runtime_error("Window was closed");
188
189 // Blank frames may come spuriously if no new window texture
190 // is available yet.
191 return {};
192 }
193
194 if (m_frameSize != frame.ContentSize()) {
195 m_frameSize = frame.ContentSize();
196 m_framePool.Recreate(getCaptureDevice(m_device), m_pixelFormat, 1, frame.ContentSize());
197 return {};
198 }
199
200 return copyTexture(m_device, frame.Surface());
201 }
202
203private:
204 static GraphicsCaptureItem createCaptureItem(HWND hwnd)
205 {
206 const auto factory = winrt::get_activation_factory<GraphicsCaptureItem>();
207 const auto interop = factory.as<IGraphicsCaptureItemInterop>();
208
209 GraphicsCaptureItem item = { nullptr };
210 winrt::hresult status = S_OK;
211
212 // Attempt to create capture item with retry, because this occasionally fails,
213 // particularly in unit tests. When the failure code is E_INVALIDARG, it
214 // seems to help to sleep for a bit and retry. See QTBUG-116025.
215 constexpr int maxRetry = 10;
216 constexpr std::chrono::milliseconds retryDelay{ 100 };
217 for (int retryNum = 0; retryNum < maxRetry; ++retryNum) {
218
219 status = interop->CreateForWindow(hwnd, winrt::guid_of<GraphicsCaptureItem>(),
220 winrt::put_abi(item));
221
222 if (status != E_INVALIDARG)
223 break;
224
225 qCWarning(qLcWindowCaptureUwp)
226 << "Failed to create capture item:"
227 << QString::fromStdWString(winrt::hresult_error(status).message().c_str())
228 << "Retry number" << retryNum;
229
230 if (retryNum + 1 < maxRetry)
231 QThread::sleep(retryDelay);
232 }
233
234 // Throw if we fail to create the capture item
235 check_hresult(status);
236
237 return item;
238 }
239
240 static IDirect3DDevice getCaptureDevice(const com_ptr<ID3D11Device> &d3dDevice)
241 {
242 const auto dxgiDevice = d3dDevice.as<IDXGIDevice>();
243
244 com_ptr<IInspectable> device;
245 check_hresult(CreateDirect3D11DeviceFromDXGIDevice(dxgiDevice.get(), device.put()));
246
247 return device.as<IDirect3DDevice>();
248 }
249
250 static com_ptr<IDXGISurface> copyTexture(const com_ptr<ID3D11Device> &device,
251 const IDirect3DSurface &capturedTexture)
252 {
253 const auto dxgiInterop{ capturedTexture.as<IDirect3DDxgiInterfaceAccess>() };
254 if (!dxgiInterop)
255 return {};
256
257 com_ptr<IDXGISurface> dxgiSurface;
258 check_hresult(dxgiInterop->GetInterface(guid_of<IDXGISurface>(), dxgiSurface.put_void()));
259
260 DXGI_SURFACE_DESC desc = {};
261 check_hresult(dxgiSurface->GetDesc(&desc));
262
263 D3D11_TEXTURE2D_DESC texDesc = {};
264 texDesc.Width = desc.Width;
265 texDesc.Height = desc.Height;
266 texDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
267 texDesc.Usage = D3D11_USAGE_STAGING;
268 texDesc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
269 texDesc.MiscFlags = 0;
270 texDesc.BindFlags = 0;
271 texDesc.ArraySize = 1;
272 texDesc.MipLevels = 1;
273 texDesc.SampleDesc = { 1, 0 };
274
275 com_ptr<ID3D11Texture2D> texture;
276 check_hresult(device->CreateTexture2D(&texDesc, nullptr, texture.put()));
277
278 com_ptr<ID3D11DeviceContext> ctx;
279 device->GetImmediateContext(ctx.put());
280 ctx->CopyResource(texture.get(), dxgiSurface.as<ID3D11Resource>().get());
281
282 return texture.as<IDXGISurface>();
283 }
284
285 MultithreadedApartment m_comApartment{};
286 HWND m_captureWindow{};
287 winrt::Windows::Graphics::SizeInt32 m_frameSize{};
288 com_ptr<ID3D11Device> m_device;
289 Direct3D11CaptureFramePool m_framePool{ nullptr };
290 GraphicsCaptureSession m_session{ nullptr };
291 const DirectXPixelFormat m_pixelFormat = DirectXPixelFormat::R8G8B8A8UIntNormalized;
292};
293
294} // namespace
295
296class QFFmpegWindowCaptureUwp::Grabber : public QFFmpegSurfaceCaptureGrabber
297{
298public:
299 Grabber(QFFmpegWindowCaptureUwp &capture, HWND hwnd)
300 : m_hwnd(hwnd),
303 {
304 const HMONITOR monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONULL);
305 m_adapter = getAdapter(monitor);
306
307 const qreal refreshRate = getMonitorRefreshRateHz(monitor);
308
309 m_format.setStreamFrameRate(refreshRate);
310 setFrameRate(refreshRate);
311
312 addFrameCallback(capture, &QFFmpegWindowCaptureUwp::newVideoFrame);
313 connect(this, &Grabber::errorUpdated, &capture, &QFFmpegWindowCaptureUwp::updateError);
314 }
315
317
318 QVideoFrameFormat frameFormat() const { return m_format; }
319
320protected:
321
323 {
324 if (!m_adapter || !IsWindow(m_hwnd))
325 return; // Error already logged
326
327 try {
328 m_windowGrabber = std::make_unique<WindowGrabber>(m_adapter.get(), m_hwnd);
329
331 } catch (const winrt::hresult_error &err) {
332
333 const QString message = QLatin1String("Unable to capture window: ")
334 + QString::fromWCharArray(err.message().c_str());
335
336 updateError(InternalError, message);
337 }
338 }
339
341 {
343 m_windowGrabber = nullptr;
344 }
345
346 QVideoFrame grabFrame() override
347 {
348 try {
349 com_ptr<IDXGISurface> texture = m_windowGrabber->tryGetFrame();
350 if (!texture)
351 return {}; // No frame available yet
352
353 const QSize size = getTextureSize(texture);
354
355 m_format.setFrameSize(size);
356
357 return QVideoFramePrivate::createFrame(
358 std::make_unique<QUwpTextureVideoBuffer>(std::move(texture)), m_format);
359
360 } catch (const winrt::hresult_error &err) {
361
362 const QString message = QLatin1String("Window capture failed: ")
363 + QString::fromWCharArray(err.message().c_str());
364
365 updateError(InternalError, message);
366 } catch (const std::runtime_error& e) {
367 updateError(CaptureFailed, QString::fromLatin1(e.what()));
368 }
369
370 return {};
371 }
372
373private:
374 static com_ptr<IDXGIAdapter1> getAdapter(HMONITOR handle)
375 {
376 com_ptr<IDXGIFactory1> factory;
377 check_hresult(CreateDXGIFactory1(guid_of<IDXGIFactory1>(), factory.put_void()));
378
379 com_ptr<IDXGIAdapter1> adapter;
380 for (quint32 i = 0; factory->EnumAdapters1(i, adapter.put()) == S_OK; adapter = nullptr, i++) {
381 com_ptr<IDXGIOutput> output;
382 for (quint32 j = 0; adapter->EnumOutputs(j, output.put()) == S_OK; output = nullptr, j++) {
383 DXGI_OUTPUT_DESC desc = {};
384 HRESULT hr = output->GetDesc(&desc);
385 if (hr == S_OK && desc.Monitor == handle)
386 return adapter;
387 }
388 }
389 return {};
390 }
391
392 static QSize getTextureSize(const com_ptr<IDXGISurface> &surf)
393 {
394 if (!surf)
395 return {};
396
397 DXGI_SURFACE_DESC desc;
398 check_hresult(surf->GetDesc(&desc));
399
400 return { static_cast<int>(desc.Width), static_cast<int>(desc.Height) };
401 }
402
403 static qreal getMonitorRefreshRateHz(HMONITOR handle)
404 {
405 DWORD count = 0;
406 if (GetNumberOfPhysicalMonitorsFromHMONITOR(handle, &count)) {
407 std::vector<PHYSICAL_MONITOR> monitors{ count };
408 if (GetPhysicalMonitorsFromHMONITOR(handle, count, monitors.data())) {
409 for (const auto &monitor : std::as_const(monitors)) {
410 MC_TIMING_REPORT screenTiming = {};
411 if (GetTimingReport(monitor.hPhysicalMonitor, &screenTiming)) {
412 // Empirically we found that GetTimingReport does not return
413 // the frequency in updates per second as documented, but in
414 // updates per 100 seconds.
415 return static_cast<qreal>(screenTiming.dwVerticalFrequencyInHZ) / 100.0;
416 }
417 }
418 }
419 }
420 return DefaultScreenCaptureFrameRate;
421 }
422
423 HWND m_hwnd{};
424 com_ptr<IDXGIAdapter1> m_adapter{};
425 std::unique_ptr<WindowGrabber> m_windowGrabber;
426 QVideoFrameFormat m_format;
427};
428
429QFFmpegWindowCaptureUwp::QFFmpegWindowCaptureUwp() : QPlatformSurfaceCapture(WindowSource{})
430{
431 qCDebug(qLcWindowCaptureUwp) << "Creating UWP screen capture";
432}
433
434QFFmpegWindowCaptureUwp::~QFFmpegWindowCaptureUwp() = default;
435
437{
438 if (!IsWindow(hwnd))
439 return u"Invalid window handle"_s;
440
441 if (hwnd == GetShellWindow())
442 return u"Cannot capture the shell window"_s;
443
444 wchar_t className[MAX_PATH] = {};
445 GetClassName(hwnd, className, MAX_PATH);
446 if (QString::fromWCharArray(className).length() == 0)
447 return u"Cannot capture windows without a class name"_s;
448
449 if (!IsWindowVisible(hwnd))
450 return u"Cannot capture invisible windows"_s;
451
452 if (GetAncestor(hwnd, GA_ROOT) != hwnd)
453 return u"Can only capture root windows"_s;
454
455 const LONG_PTR style = GetWindowLongPtr(hwnd, GWL_STYLE);
456 if (style & WS_DISABLED)
457 return u"Cannot capture disabled windows"_s;
458
459 const LONG_PTR exStyle = GetWindowLongPtr(hwnd, GWL_EXSTYLE);
460 if (exStyle & WS_EX_TOOLWINDOW)
461 return u"No tooltips"_s;
462
463 DWORD cloaked = FALSE;
464 const HRESULT hr = DwmGetWindowAttribute(hwnd, DWMWA_CLOAKED, &cloaked, sizeof(cloaked));
465 if (SUCCEEDED(hr) && cloaked == DWM_CLOAKED_SHELL)
466 return u"Cannot capture cloaked windows"_s;
467
468 return {};
469}
470
471bool QFFmpegWindowCaptureUwp::setActiveInternal(bool active)
472{
473 if (static_cast<bool>(m_grabber) == active)
474 return false;
475
476 if (m_grabber) {
477 m_grabber.reset();
478 return true;
479 }
480
481 const auto window = source<WindowSource>();
482 const auto handle = QCapturableWindowPrivate::handle(window);
483
484 const auto hwnd = reinterpret_cast<HWND>(handle ? handle->id : 0);
485 if (const QString error = isCapturableWindow(hwnd); !error.isEmpty()) {
486 updateError(InternalError, error);
487 return false;
488 }
489
490 m_grabber = std::make_unique<Grabber>(*this, hwnd);
491 m_grabber->start();
492
493 return true;
494}
495
496bool QFFmpegWindowCaptureUwp::isSupported()
497{
498 return GraphicsCaptureSession::IsSupported();
499}
500
501QVideoFrameFormat QFFmpegWindowCaptureUwp::frameFormat() const
502{
503 if (m_grabber)
504 return m_grabber->frameFormat();
505 return {};
506}
507
508QT_END_NAMESPACE
Grabber(QFFmpegWindowCaptureUwp &capture, HWND hwnd)
auto wait_for(Async const &async, Windows::Foundation::TimeSpan const &timeout)
Windows::Foundation::AsyncStatus AsyncStatus
static QString isCapturableWindow(HWND hwnd)