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