Qt
Internal/Contributor docs for the Qt SDK. <b>Note:</b> These are NOT official API docs; those are found <a href='https://doc.qt.io/'>here</a>.
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#include <private/qabstractvideobuffer_p.h>
7
8#include <unknwn.h>
9#include <winrt/base.h>
10#include <QtCore/private/qfactorycacheregistration_p.h>
11// Workaround for Windows SDK bug.
12// See https://github.com/microsoft/Windows.UI.Composition-Win32-Samples/issues/47
13namespace winrt::impl
14{
15template <typename Async>
16auto wait_for(Async const& async, Windows::Foundation::TimeSpan const& timeout);
17}
18#include <winrt/Windows.Foundation.Collections.h>
19#include <winrt/Windows.Graphics.Capture.h>
20#include <winrt/Windows.Graphics.DirectX.h>
21#include <winrt/Windows.Graphics.DirectX.Direct3D11.h>
22#include <Windows.Graphics.Capture.h>
23#include <Windows.Graphics.Capture.Interop.h>
24#include <windows.graphics.directx.direct3d11.interop.h>
25
26#include <D3d11.h>
27#include <dwmapi.h>
28#include <lowlevelmonitorconfigurationapi.h>
29#include <physicalmonitorenumerationapi.h>
30
31#include "qvideoframe.h"
32#include <qwindow.h>
33#include <qthread.h>
34#include <qloggingcategory.h>
35#include <qguiapplication.h>
36#include <private/qmultimediautils_p.h>
37#include <private/qwindowsmultimediautils_p.h>
38#include <private/qcapturablewindow_p.h>
39#include <qpa/qplatformscreen_p.h>
40
41#include <memory>
42#include <system_error>
43
45
46using namespace winrt::Windows::Graphics::Capture;
47using namespace winrt::Windows::Graphics::DirectX;
48using namespace winrt::Windows::Graphics::DirectX::Direct3D11;
49using namespace Windows::Graphics::DirectX::Direct3D11;
50using namespace QWindowsMultimediaUtils;
51
52using winrt::check_hresult;
53using winrt::com_ptr;
54using winrt::guid_of;
55
56namespace {
57
58Q_LOGGING_CATEGORY(qLcWindowCaptureUwp, "qt.multimedia.ffmpeg.windowcapture.uwp");
59
60winrt::Windows::Graphics::SizeInt32 getWindowSize(HWND hwnd)
61{
62 RECT windowRect{};
63 ::GetWindowRect(hwnd, &windowRect);
64
65 return { windowRect.right - windowRect.left, windowRect.bottom - windowRect.top };
66}
67
68QSize asQSize(winrt::Windows::Graphics::SizeInt32 size)
69{
70 return { size.Width, size.Height };
71}
72
73struct MultithreadedApartment
74{
75 MultithreadedApartment(const MultithreadedApartment &) = delete;
76 MultithreadedApartment &operator=(const MultithreadedApartment &) = delete;
77
78 MultithreadedApartment() { winrt::init_apartment(); }
79 ~MultithreadedApartment() { winrt::uninit_apartment(); }
80};
81
82class QUwpTextureVideoBuffer : public QAbstractVideoBuffer
83{
84public:
85 QUwpTextureVideoBuffer(com_ptr<IDXGISurface> &&surface)
86 : QAbstractVideoBuffer(QVideoFrame::NoHandle), m_surface(surface)
87 {
88 }
89
90 ~QUwpTextureVideoBuffer() override { QUwpTextureVideoBuffer::unmap(); }
91
92 QVideoFrame::MapMode mapMode() const override { return m_mapMode; }
93
94 MapData map(QVideoFrame::MapMode mode) override
95 {
96 if (m_mapMode != QVideoFrame::NotMapped)
97 return {};
98
100 DXGI_MAPPED_RECT rect = {};
101 HRESULT hr = m_surface->Map(&rect, DXGI_MAP_READ);
102 if (SUCCEEDED(hr)) {
103 DXGI_SURFACE_DESC desc = {};
104 hr = m_surface->GetDesc(&desc);
105
106 MapData md = {};
107 md.nPlanes = 1;
108 md.bytesPerLine[0] = rect.Pitch;
109 md.data[0] = rect.pBits;
110 md.size[0] = rect.Pitch * desc.Height;
111
112 m_mapMode = QVideoFrame::ReadOnly;
113
114 return md;
115 } else {
116 qCDebug(qLcWindowCaptureUwp) << "Failed to map DXGI surface" << errorString(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) << "Failed to unmap surface" << errorString(hr);
132
133 m_mapMode = QVideoFrame::NotMapped;
134 }
135
136private:
138 com_ptr<IDXGISurface> m_surface;
139};
140
141struct WindowGrabber
142{
143 WindowGrabber() = default;
144
145 WindowGrabber(IDXGIAdapter1 *adapter, HWND hwnd)
146 : m_frameSize{ getWindowSize(hwnd) }, m_captureWindow{ hwnd }
147 {
148 check_hresult(D3D11CreateDevice(adapter, D3D_DRIVER_TYPE_UNKNOWN, nullptr, 0, nullptr, 0,
149 D3D11_SDK_VERSION, m_device.put(), nullptr, nullptr));
150
151 const auto captureItem = createCaptureItem(hwnd);
152
153 m_framePool = Direct3D11CaptureFramePool::CreateFreeThreaded(
154 getCaptureDevice(m_device), m_pixelFormat, 1,
155 captureItem.Size());
156
157 m_session = m_framePool.CreateCaptureSession(captureItem);
158
159 // If supported, enable cursor capture
160 if (const auto session2 = m_session.try_as<IGraphicsCaptureSession2>())
161 session2.IsCursorCaptureEnabled(true);
162
163 // If supported, disable colored border around captured window to match other platforms
164 if (const auto session3 = m_session.try_as<IGraphicsCaptureSession3>())
165 session3.IsBorderRequired(false);
166
167 m_session.StartCapture();
168 }
169
170 ~WindowGrabber()
171 {
172 m_framePool.Close();
173 m_session.Close();
174 }
175
176 com_ptr<IDXGISurface> tryGetFrame()
177 {
178 const Direct3D11CaptureFrame frame = m_framePool.TryGetNextFrame();
179 if (!frame) {
180
181 // Stop capture and report failure if window was closed. If we don't stop,
182 // testing shows that either we don't get any frames, or we get blank frames.
183 // Emitting an error will prevent this inconsistent behavior, and makes the
184 // Windows implementation behave like the Linux implementation
185 if (!IsWindow(m_captureWindow))
186 throw std::runtime_error("Window was closed");
187
188 // Blank frames may come spuriously if no new window texture
189 // is available yet.
190 return {};
191 }
192
193 if (m_frameSize != frame.ContentSize()) {
194 m_frameSize = frame.ContentSize();
195 m_framePool.Recreate(getCaptureDevice(m_device), m_pixelFormat, 1, frame.ContentSize());
196 return {};
197 }
198
199 return copyTexture(m_device, frame.Surface());
200 }
201
202private:
203 static GraphicsCaptureItem createCaptureItem(HWND hwnd)
204 {
205 const auto factory = winrt::get_activation_factory<GraphicsCaptureItem>();
206 const auto interop = factory.as<IGraphicsCaptureItemInterop>();
207
208 GraphicsCaptureItem item = { nullptr };
209 winrt::hresult status = S_OK;
210
211 // Attempt to create capture item with retry, because this occasionally fails,
212 // particularly in unit tests. When the failure code is E_INVALIDARG, it
213 // seems to help to sleep for a bit and retry. See QTBUG-116025.
214 constexpr int maxRetry = 10;
215 constexpr std::chrono::milliseconds retryDelay{ 100 };
216 for (int retryNum = 0; retryNum < maxRetry; ++retryNum) {
217
218 status = interop->CreateForWindow(hwnd, winrt::guid_of<GraphicsCaptureItem>(),
219 winrt::put_abi(item));
220
221 if (status != E_INVALIDARG)
222 break;
223
224 qCWarning(qLcWindowCaptureUwp)
225 << "Failed to create capture item:"
226 << QString::fromStdWString(winrt::hresult_error(status).message().c_str())
227 << "Retry number" << retryNum;
228
229 if (retryNum + 1 < maxRetry)
230 QThread::sleep(retryDelay);
231 }
232
233 // Throw if we fail to create the capture item
234 check_hresult(status);
235
236 return item;
237 }
238
239 static IDirect3DDevice getCaptureDevice(const com_ptr<ID3D11Device> &d3dDevice)
240 {
241 const auto dxgiDevice = d3dDevice.as<IDXGIDevice>();
242
243 com_ptr<IInspectable> device;
244 check_hresult(CreateDirect3D11DeviceFromDXGIDevice(dxgiDevice.get(), device.put()));
245
246 return device.as<IDirect3DDevice>();
247 }
248
249 static com_ptr<IDXGISurface> copyTexture(const com_ptr<ID3D11Device> &device,
250 const IDirect3DSurface &capturedTexture)
251 {
252 const auto dxgiInterop{ capturedTexture.as<IDirect3DDxgiInterfaceAccess>() };
253 if (!dxgiInterop)
254 return {};
255
256 com_ptr<IDXGISurface> dxgiSurface;
257 check_hresult(dxgiInterop->GetInterface(guid_of<IDXGISurface>(), dxgiSurface.put_void()));
258
259 DXGI_SURFACE_DESC desc = {};
260 check_hresult(dxgiSurface->GetDesc(&desc));
261
262 D3D11_TEXTURE2D_DESC texDesc = {};
263 texDesc.Width = desc.Width;
264 texDesc.Height = desc.Height;
265 texDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
266 texDesc.Usage = D3D11_USAGE_STAGING;
267 texDesc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
268 texDesc.MiscFlags = 0;
269 texDesc.BindFlags = 0;
270 texDesc.ArraySize = 1;
271 texDesc.MipLevels = 1;
272 texDesc.SampleDesc = { 1, 0 };
273
274 com_ptr<ID3D11Texture2D> texture;
275 check_hresult(device->CreateTexture2D(&texDesc, nullptr, texture.put()));
276
277 com_ptr<ID3D11DeviceContext> ctx;
278 device->GetImmediateContext(ctx.put());
279 ctx->CopyResource(texture.get(), dxgiSurface.as<ID3D11Resource>().get());
280
281 return texture.as<IDXGISurface>();
282 }
283
284 MultithreadedApartment m_comApartment{};
285 HWND m_captureWindow{};
286 winrt::Windows::Graphics::SizeInt32 m_frameSize{};
287 com_ptr<ID3D11Device> m_device;
288 Direct3D11CaptureFramePool m_framePool{ nullptr };
289 GraphicsCaptureSession m_session{ nullptr };
290 const DirectXPixelFormat m_pixelFormat = DirectXPixelFormat::R8G8B8A8UIntNormalized;
291};
292
293} // namespace
294
296{
298public:
299 Grabber(QFFmpegWindowCaptureUwp &capture, HWND hwnd)
300 : m_hwnd(hwnd),
301 m_format(QVideoFrameFormat(asQSize(getWindowSize(hwnd)),
302 QVideoFrameFormat::Format_RGBX8888))
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
314 }
315
316 ~Grabber() override { stop(); }
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
337 }
338 }
339
341 {
343 m_windowGrabber = nullptr;
344 }
345
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 QVideoFrame(new QUwpTextureVideoBuffer(std::move(texture)), m_format);
358
359 } catch (const winrt::hresult_error &err) {
360
361 const QString message = QLatin1String("Window capture failed: ")
362 + QString::fromWCharArray(err.message().c_str());
363
365 } catch (const std::runtime_error& e) {
367 }
368
369 return {};
370 }
371
372private:
373 static com_ptr<IDXGIAdapter1> getAdapter(HMONITOR handle)
374 {
375 com_ptr<IDXGIFactory1> factory;
376 check_hresult(CreateDXGIFactory1(guid_of<IDXGIFactory1>(), factory.put_void()));
377
378 com_ptr<IDXGIAdapter1> adapter;
379 for (quint32 i = 0; factory->EnumAdapters1(i, adapter.put()) == S_OK; adapter = nullptr, i++) {
380 com_ptr<IDXGIOutput> output;
381 for (quint32 j = 0; adapter->EnumOutputs(j, output.put()) == S_OK; output = nullptr, j++) {
382 DXGI_OUTPUT_DESC desc = {};
383 HRESULT hr = output->GetDesc(&desc);
384 if (hr == S_OK && desc.Monitor == handle)
385 return adapter;
386 }
387 }
388 return {};
389 }
390
391 static QSize getTextureSize(const com_ptr<IDXGISurface> &surf)
392 {
393 if (!surf)
394 return {};
395
396 DXGI_SURFACE_DESC desc;
397 check_hresult(surf->GetDesc(&desc));
398
399 return { static_cast<int>(desc.Width), static_cast<int>(desc.Height) };
400 }
401
402 static qreal getMonitorRefreshRateHz(HMONITOR handle)
403 {
404 DWORD count = 0;
405 if (GetNumberOfPhysicalMonitorsFromHMONITOR(handle, &count)) {
406 std::vector<PHYSICAL_MONITOR> monitors{ count };
407 if (GetPhysicalMonitorsFromHMONITOR(handle, count, monitors.data())) {
408 for (const auto &monitor : std::as_const(monitors)) {
409 MC_TIMING_REPORT screenTiming = {};
410 if (GetTimingReport(monitor.hPhysicalMonitor, &screenTiming)) {
411 // Empirically we found that GetTimingReport does not return
412 // the frequency in updates per second as documented, but in
413 // updates per 100 seconds.
414 return static_cast<qreal>(screenTiming.dwVerticalFrequencyInHZ) / 100.0;
415 }
416 }
417 }
418 }
420 }
421
422 HWND m_hwnd{};
423 com_ptr<IDXGIAdapter1> m_adapter{};
424 std::unique_ptr<WindowGrabber> m_windowGrabber;
425 QVideoFrameFormat m_format;
426};
427
429{
430 qCDebug(qLcWindowCaptureUwp) << "Creating UWP screen capture";
431}
432
434
436{
437 if (!IsWindow(hwnd))
438 return "Invalid window handle";
439
440 if (hwnd == GetShellWindow())
441 return "Cannot capture the shell window";
442
443 wchar_t className[MAX_PATH] = {};
444 GetClassName(hwnd, className, MAX_PATH);
446 return "Cannot capture windows without a class name";
447
448 if (!IsWindowVisible(hwnd))
449 return "Cannot capture invisible windows";
450
451 if (GetAncestor(hwnd, GA_ROOT) != hwnd)
452 return "Can only capture root windows";
453
454 const LONG_PTR style = GetWindowLongPtr(hwnd, GWL_STYLE);
455 if (style & WS_DISABLED)
456 return "Cannot capture disabled windows";
457
458 const LONG_PTR exStyle = GetWindowLongPtr(hwnd, GWL_EXSTYLE);
459 if (exStyle & WS_EX_TOOLWINDOW)
460 return "No tooltips";
461
462 DWORD cloaked = FALSE;
463 const HRESULT hr = DwmGetWindowAttribute(hwnd, DWMWA_CLOAKED, &cloaked, sizeof(cloaked));
464 if (SUCCEEDED(hr) && cloaked == DWM_CLOAKED_SHELL)
465 return "Cannot capture cloaked windows";
466
467 return {};
468}
469
471{
472 if (static_cast<bool>(m_grabber) == active)
473 return false;
474
475 if (m_grabber) {
476 m_grabber.reset();
477 return true;
478 }
479
480 const auto window = source<WindowSource>();
482
483 const auto hwnd = reinterpret_cast<HWND>(handle ? handle->id : 0);
484 if (const QString error = isCapturableWindow(hwnd); !error.isEmpty()) {
486 return false;
487 }
488
489 m_grabber = std::make_unique<Grabber>(*this, hwnd);
490 m_grabber->start();
491
492 return true;
493}
494
496{
497 return GraphicsCaptureSession::IsSupported();
498}
499
501{
502 if (m_grabber)
503 return m_grabber->frameFormat();
504 return {};
505}
506
508
509#include "qffmpegwindowcapture_uwp.moc"
AVFCameraSession * m_session
IOBluetoothDevice * device
The QAbstractVideoBuffer class is an abstraction for video data. \inmodule QtMultimedia.
static const QCapturableWindowPrivate * handle(const QCapturableWindow &window)
\inmodule QtMultimedia
void errorUpdated(QPlatformSurfaceCapture::Error error, const QString &description)
void addFrameCallback(Object &object, Method method)
void updateError(QPlatformSurfaceCapture::Error error, const QString &description={})
Grabber(QFFmpegWindowCaptureUwp &capture, HWND hwnd)
bool setActiveInternal(bool active) override
~QFFmpegWindowCaptureUwp() override
QVideoFrameFormat frameFormat() const override
static QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *member, Qt::ConnectionType=Qt::AutoConnection)
\threadsafe
Definition qobject.cpp:2960
void updateError(Error error, const QString &errorString)
void newVideoFrame(const QVideoFrame &)
\inmodule QtCore
Definition qsize.h:25
\macro QT_RESTRICTED_CAST_FROM_ASCII
Definition qstring.h:129
static QString fromLatin1(QByteArrayView ba)
This is an overloaded member function, provided for convenience. It differs from the above function o...
Definition qstring.cpp:5871
static QString fromWCharArray(const wchar_t *string, qsizetype size=-1)
Definition qstring.h:1309
static QString fromStdWString(const std::wstring &s)
Returns a copy of the str string.
Definition qstring.h:1458
static void sleep(unsigned long)
The QVideoFrameFormat class specifies the stream format of a video presentation surface.
void setStreamFrameRate(qreal rate)
Sets the frame rate of a video stream in frames per second.
void setFrameSize(const QSize &size)
Sets the size of frames in a video stream to size.
The QVideoFrame class represents a frame of video data.
Definition qvideoframe.h:27
MapMode
Enumerates how a video buffer's data is mapped to system memory.
Definition qvideoframe.h:37
EGLContext ctx
QMap< QString, QString > map
[6]
rect
[4]
Combined button and popup list for selecting options.
Q_MULTIMEDIA_EXPORT QString errorString(HRESULT hr)
auto wait_for(Async const &async, Windows::Foundation::TimeSpan const &timeout)
static constexpr qreal DefaultScreenCaptureFrameRate
static QString isCapturableWindow(HWND hwnd)
#define Q_LOGGING_CATEGORY(name,...)
#define qCWarning(category,...)
#define qCDebug(category,...)
GLuint64 GLenum void * handle
GLenum mode
GLuint * monitors
GLenum GLuint GLintptr GLsizeiptr size
[1]
GLenum GLuint GLenum GLsizei length
GLenum GLenum GLsizei count
GLbitfield GLuint64 timeout
[4]
GLenum GLuint texture
GLuint GLsizei const GLchar * message
#define MAX_PATH
QLatin1StringView QLatin1String
Definition qstringfwd.h:31
#define Q_OBJECT
unsigned int quint32
Definition qtypes.h:50
double qreal
Definition qtypes.h:187
QT_BEGIN_NAMESPACE typedef uchar * output
long HRESULT
const char className[16]
[1]
Definition qwizard.cpp:100
QGraphicsItem * item
QItemEditorFactory * factory
aWidget window() -> setWindowTitle("New Window Title")
[2]
QFrame frame
[0]