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/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)
149 : m_captureWindow{ hwnd }, m_frameSize{ getWindowSize(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 m_framePool = Direct3D11CaptureFramePool::CreateFreeThreaded(
157 getCaptureDevice(m_device), m_pixelFormat, 1,
158 captureItem.Size());
159
160 m_session = m_framePool.CreateCaptureSession(captureItem);
161
162 // If supported, enable cursor capture
163 if (const auto session2 = m_session.try_as<IGraphicsCaptureSession2>())
164 session2.IsCursorCaptureEnabled(true);
165
166 // If supported, disable colored border around captured window to match other platforms
167 if (const auto session3 = m_session.try_as<IGraphicsCaptureSession3>())
168 session3.IsBorderRequired(false);
169
170 m_session.StartCapture();
171 }
172
173 ~WindowGrabber()
174 {
175 m_framePool.Close();
176 m_session.Close();
177 }
178
179 com_ptr<IDXGISurface> tryGetFrame()
180 {
181 const Direct3D11CaptureFrame frame = m_framePool.TryGetNextFrame();
182 if (!frame) {
183
184 // Stop capture and report failure if window was closed. If we don't stop,
185 // testing shows that either we don't get any frames, or we get blank frames.
186 // Emitting an error will prevent this inconsistent behavior, and makes the
187 // Windows implementation behave like the Linux implementation
188 if (!IsWindow(m_captureWindow))
189 throw std::runtime_error("Window was closed");
190
191 // Blank frames may come spuriously if no new window texture
192 // is available yet.
193 return {};
194 }
195
196 if (m_frameSize != frame.ContentSize()) {
197 m_frameSize = frame.ContentSize();
198 m_framePool.Recreate(getCaptureDevice(m_device), m_pixelFormat, 1, frame.ContentSize());
199 return {};
200 }
201
202 return copyTexture(m_device, frame.Surface());
203 }
204
205private:
206 static GraphicsCaptureItem createCaptureItem(HWND hwnd)
207 {
208 const auto factory = winrt::get_activation_factory<GraphicsCaptureItem>();
209 const auto interop = factory.as<IGraphicsCaptureItemInterop>();
210
211 GraphicsCaptureItem item = { nullptr };
212 winrt::hresult status = S_OK;
213
214 // Attempt to create capture item with retry, because this occasionally fails,
215 // particularly in unit tests. When the failure code is E_INVALIDARG, it
216 // seems to help to sleep for a bit and retry. See QTBUG-116025.
217 constexpr int maxRetry = 10;
218 constexpr std::chrono::milliseconds retryDelay{ 100 };
219 for (int retryNum = 0; retryNum < maxRetry; ++retryNum) {
220
221 status = interop->CreateForWindow(hwnd, winrt::guid_of<GraphicsCaptureItem>(),
222 winrt::put_abi(item));
223
224 if (status != E_INVALIDARG)
225 break;
226
227 qCWarning(qLcWindowCaptureUwp)
228 << "Failed to create capture item:"
229 << QString::fromStdWString(winrt::hresult_error(status).message().c_str())
230 << "Retry number" << retryNum;
231
232 if (retryNum + 1 < maxRetry)
233 QThread::sleep(retryDelay);
234 }
235
236 // Throw if we fail to create the capture item
237 check_hresult(status);
238
239 return item;
240 }
241
242 static IDirect3DDevice getCaptureDevice(const com_ptr<ID3D11Device> &d3dDevice)
243 {
244 const auto dxgiDevice = d3dDevice.as<IDXGIDevice>();
245
246 com_ptr<IInspectable> device;
247 check_hresult(CreateDirect3D11DeviceFromDXGIDevice(dxgiDevice.get(), device.put()));
248
249 return device.as<IDirect3DDevice>();
250 }
251
252 static com_ptr<IDXGISurface> copyTexture(const com_ptr<ID3D11Device> &device,
253 const IDirect3DSurface &capturedTexture)
254 {
255 const auto dxgiInterop{ capturedTexture.as<IDirect3DDxgiInterfaceAccess>() };
256 if (!dxgiInterop)
257 return {};
258
259 com_ptr<IDXGISurface> dxgiSurface;
260 check_hresult(dxgiInterop->GetInterface(guid_of<IDXGISurface>(), dxgiSurface.put_void()));
261
262 DXGI_SURFACE_DESC desc = {};
263 check_hresult(dxgiSurface->GetDesc(&desc));
264
265 D3D11_TEXTURE2D_DESC texDesc = {};
266 texDesc.Width = desc.Width;
267 texDesc.Height = desc.Height;
268 texDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
269 texDesc.Usage = D3D11_USAGE_STAGING;
270 texDesc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
271 texDesc.MiscFlags = 0;
272 texDesc.BindFlags = 0;
273 texDesc.ArraySize = 1;
274 texDesc.MipLevels = 1;
275 texDesc.SampleDesc = { 1, 0 };
276
277 com_ptr<ID3D11Texture2D> texture;
278 check_hresult(device->CreateTexture2D(&texDesc, nullptr, texture.put()));
279
280 com_ptr<ID3D11DeviceContext> ctx;
281 device->GetImmediateContext(ctx.put());
282 ctx->CopyResource(texture.get(), dxgiSurface.as<ID3D11Resource>().get());
283
284 return texture.as<IDXGISurface>();
285 }
286
287 MultithreadedApartment m_comApartment{};
288 HWND m_captureWindow{};
289 winrt::Windows::Graphics::SizeInt32 m_frameSize{};
290 com_ptr<ID3D11Device> m_device;
291 Direct3D11CaptureFramePool m_framePool{ nullptr };
292 GraphicsCaptureSession m_session{ nullptr };
293 const DirectXPixelFormat m_pixelFormat = DirectXPixelFormat::R8G8B8A8UIntNormalized;
294};
295
296} // namespace
297
298class QFFmpegWindowCaptureUwp::Grabber : public QFFmpegSurfaceCaptureGrabber
299{
300public:
301 Grabber(QFFmpegWindowCaptureUwp &capture, HWND hwnd)
303 m_hwnd(hwnd),
306 {
307 const HMONITOR monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONULL);
308 m_adapter = getAdapter(monitor);
309
310 addFrameCallback(&capture, &QFFmpegWindowCaptureUwp::newVideoFrame);
311 connect(this, &Grabber::errorUpdated, &capture, &QFFmpegWindowCaptureUwp::updateError);
312
313 const auto refreshRate = getMonitorRefreshRateHz(monitor);
314 const auto defaultRate = refreshRate ? qMin(*refreshRate, DefaultScreenCaptureFrameRate)
315 : DefaultScreenCaptureFrameRate;
316 setFrameRate(capture.frameRate().value_or(defaultRate));
317 m_format.setStreamFrameRate(frameRate());
318 }
319
321
322 QVideoFrameFormat frameFormat() const { return m_format; }
323
324protected:
325
327 {
328 if (!m_adapter || !IsWindow(m_hwnd))
329 return; // Error already logged
330
331 try {
332 m_windowGrabber = std::make_unique<WindowGrabber>(m_adapter.get(), m_hwnd);
333
335 } catch (const winrt::hresult_error &err) {
336
337 const QString message = QLatin1String("Unable to capture window: ")
338 + QString::fromWCharArray(err.message().c_str());
339
340 updateError(InternalError, message);
341 }
342 }
343
345 {
347 m_windowGrabber = nullptr;
348 }
349
350 QVideoFrame grabFrame() override
351 {
352 try {
353 com_ptr<IDXGISurface> texture = m_windowGrabber->tryGetFrame();
354 if (!texture)
355 return {}; // No frame available yet
356
357 const QSize size = getTextureSize(texture);
358
359 m_format.setFrameSize(size);
360
361 if (m_format.streamFrameRate() != frameRate())
362 m_format.setStreamFrameRate(frameRate());
363
364 return QVideoFramePrivate::createFrame(
365 std::make_unique<QUwpTextureVideoBuffer>(std::move(texture)), m_format);
366
367 } catch (const winrt::hresult_error &err) {
368
369 const QString message = QLatin1String("Window capture failed: ")
370 + QString::fromWCharArray(err.message().c_str());
371
372 updateError(InternalError, message);
373 } catch (const std::runtime_error& e) {
374 updateError(CaptureFailed, QString::fromLatin1(e.what()));
375 }
376
377 return {};
378 }
379
380private:
381 static com_ptr<IDXGIAdapter1> getAdapter(HMONITOR handle)
382 {
383 com_ptr<IDXGIFactory1> factory;
384 check_hresult(CreateDXGIFactory1(guid_of<IDXGIFactory1>(), factory.put_void()));
385
386 com_ptr<IDXGIAdapter1> adapter;
387 for (quint32 i = 0; factory->EnumAdapters1(i, adapter.put()) == S_OK; adapter = nullptr, i++) {
388 com_ptr<IDXGIOutput> output;
389 for (quint32 j = 0; adapter->EnumOutputs(j, output.put()) == S_OK; output = nullptr, j++) {
390 DXGI_OUTPUT_DESC desc = {};
391 HRESULT hr = output->GetDesc(&desc);
392 if (hr == S_OK && desc.Monitor == handle)
393 return adapter;
394 }
395 }
396 return {};
397 }
398
399 static QSize getTextureSize(const com_ptr<IDXGISurface> &surf)
400 {
401 if (!surf)
402 return {};
403
404 DXGI_SURFACE_DESC desc;
405 check_hresult(surf->GetDesc(&desc));
406
407 return { static_cast<int>(desc.Width), static_cast<int>(desc.Height) };
408 }
409
410 static q23::expected<qreal, QString> getMonitorRefreshRateHz(HMONITOR handle)
411 {
412 if (!handle)
413 return q23::unexpected{ u"No monitor"_s };
414
415 DWORD count = 0;
416 if (GetNumberOfPhysicalMonitorsFromHMONITOR(handle, &count)) {
417 std::vector<PHYSICAL_MONITOR> monitors{ count };
418 if (GetPhysicalMonitorsFromHMONITOR(handle, count, monitors.data())) {
419 const auto cleanup = qScopeGuard([count, &monitors] {
420 DestroyPhysicalMonitors(count, monitors.data());
421 });
422 for (const auto &monitor : std::as_const(monitors)) {
423 MC_TIMING_REPORT screenTiming = {};
424 if (GetTimingReport(monitor.hPhysicalMonitor, &screenTiming)) {
425 // Empirically we found that GetTimingReport does not return
426 // the frequency in updates per second as documented, but in
427 // updates per 100 seconds.
428 return static_cast<qreal>(screenTiming.dwVerticalFrequencyInHZ) / 100.0;
429 }
430 }
431 }
432 }
433 return q23::unexpected{ u"Couldn't get monitor refresh rate"_s };
434 }
435
436 HWND m_hwnd{};
437 com_ptr<IDXGIAdapter1> m_adapter{};
438 std::unique_ptr<WindowGrabber> m_windowGrabber;
439 QVideoFrameFormat m_format;
440};
441
442QFFmpegWindowCaptureUwp::QFFmpegWindowCaptureUwp() : QPlatformSurfaceCapture(WindowSource{})
443{
444 qCDebug(qLcWindowCaptureUwp) << "Creating UWP screen capture";
445}
446
447QFFmpegWindowCaptureUwp::~QFFmpegWindowCaptureUwp() = default;
448
450{
451 if (!IsWindow(hwnd))
452 return u"Invalid window handle"_s;
453
454 if (hwnd == GetShellWindow())
455 return u"Cannot capture the shell window"_s;
456
457 wchar_t className[MAX_PATH] = {};
458 GetClassName(hwnd, className, MAX_PATH);
459 if (QString::fromWCharArray(className).length() == 0)
460 return u"Cannot capture windows without a class name"_s;
461
462 if (!IsWindowVisible(hwnd))
463 return u"Cannot capture invisible windows"_s;
464
465 if (GetAncestor(hwnd, GA_ROOT) != hwnd)
466 return u"Can only capture root windows"_s;
467
468 const LONG_PTR style = GetWindowLongPtr(hwnd, GWL_STYLE);
469 if (style & WS_DISABLED)
470 return u"Cannot capture disabled windows"_s;
471
472 const LONG_PTR exStyle = GetWindowLongPtr(hwnd, GWL_EXSTYLE);
473 if (exStyle & WS_EX_TOOLWINDOW)
474 return u"No tooltips"_s;
475
476 DWORD cloaked = FALSE;
477 const HRESULT hr = DwmGetWindowAttribute(hwnd, DWMWA_CLOAKED, &cloaked, sizeof(cloaked));
478 if (SUCCEEDED(hr) && cloaked == DWM_CLOAKED_SHELL)
479 return u"Cannot capture cloaked windows"_s;
480
481 return {};
482}
483
484bool QFFmpegWindowCaptureUwp::setActiveInternal(bool active)
485{
486 if (static_cast<bool>(m_grabber) == active)
487 return false;
488
489 if (m_grabber) {
490 m_grabber.reset();
491 return true;
492 }
493
494 const auto window = source<WindowSource>();
495 const auto handle = QCapturableWindowPrivate::handle(window);
496
497 const auto hwnd = reinterpret_cast<HWND>(handle ? handle->id : 0);
498 if (const QString error = isCapturableWindow(hwnd); !error.isEmpty()) {
499 updateError(InternalError, error);
500 return false;
501 }
502
503 m_grabber = std::make_unique<Grabber>(*this, hwnd);
504 m_grabber->start();
505
506 return true;
507}
508
509bool QFFmpegWindowCaptureUwp::isSupported()
510{
511 return GraphicsCaptureSession::IsSupported();
512}
513
514QVideoFrameFormat QFFmpegWindowCaptureUwp::frameFormat() const
515{
516 if (m_grabber)
517 return m_grabber->frameFormat();
518 return {};
519}
520
521QT_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)