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
qx11surfacecapture.cpp
Go to the documentation of this file.
1// Copyright (C) 2023 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 <qvideoframe.h>
8#include <qscreen.h>
9#include <qwindow.h>
10#include <qdebug.h>
11#include <qguiapplication.h>
12#include <qloggingcategory.h>
13#include <qminmax.h>
14
15#include "private/qcapturablewindow_p.h"
16#include "private/qmemoryvideobuffer_p.h"
17#include "private/qvideoframeconversionhelper_p.h"
18#include "private/qvideoframe_p.h"
19
20#include <X11/Xlib.h>
21#include <sys/shm.h>
22#include <X11/extensions/XShm.h>
23#include <X11/Xutil.h>
24#include <X11/extensions/Xrandr.h>
25
26#include <optional>
27#include <utility>
28
29QT_BEGIN_NAMESPACE
30
31Q_STATIC_LOGGING_CATEGORY(qLcX11SurfaceCapture, "qt.multimedia.ffmpeg.qx11surfacecapture");
32
33namespace {
34
35void destroyXImage(XImage* image) {
36 XDestroyImage(image); // macro
37}
38
39template <typename T, typename D>
40std::unique_ptr<T, D> makeXUptr(T* ptr, D deleter) {
41 return std::unique_ptr<T, D>(ptr, deleter);
42}
43
44int screenNumberByName(Display *display, const QString &name)
45{
46 int size = 0;
47 auto monitors = makeXUptr(
48 XRRGetMonitors(display, XDefaultRootWindow(display), true, &size),
49 &XRRFreeMonitors);
50 const auto end = monitors.get() + size;
51 auto found = std::find_if(monitors.get(), end, [&](const XRRMonitorInfo &info) {
52 auto atomName = makeXUptr(XGetAtomName(display, info.name), &XFree);
53 return atomName && name == QString::fromUtf8(atomName.get());
54 });
55
56 return found == end ? -1 : std::distance(monitors.get(), found);
57}
58
59QVideoFrameFormat::PixelFormat xImagePixelFormat(const XImage &image)
60{
61 if (image.bits_per_pixel != 32) return QVideoFrameFormat::Format_Invalid;
62
63 if (image.red_mask == 0xff0000 &&
64 image.green_mask == 0xff00 &&
65 image.blue_mask == 0xff)
66 return QVideoFrameFormat::Format_BGRX8888;
67
68 if (image.red_mask == 0xff00 &&
69 image.green_mask == 0xff0000 &&
70 image.blue_mask == 0xff000000)
71 return QVideoFrameFormat::Format_XBGR8888;
72
73 if (image.blue_mask == 0xff0000 &&
74 image.green_mask == 0xff00 &&
75 image.red_mask == 0xff)
76 return QVideoFrameFormat::Format_RGBX8888;
77
78 if (image.red_mask == 0xff00 &&
79 image.green_mask == 0xff0000 &&
80 image.blue_mask == 0xff000000)
81 return QVideoFrameFormat::Format_XRGB8888;
82
83 return QVideoFrameFormat::Format_Invalid;
84}
85
86} // namespace
87
88class QX11SurfaceCapture::Grabber : public QFFmpegSurfaceCaptureGrabber
89{
90public:
91 static std::unique_ptr<Grabber> create(QX11SurfaceCapture &capture, QScreen *screen)
92 {
93 std::unique_ptr<Grabber> result(new Grabber(capture));
94 return result->init(screen) ? std::move(result) : nullptr;
95 }
96
97 static std::unique_ptr<Grabber> create(QX11SurfaceCapture &capture, WId wid)
98 {
99 std::unique_ptr<Grabber> result(new Grabber(capture));
100 return result->init(wid) ? std::move(result) : nullptr;
101 }
102
104 {
105 stop();
106
107 detachShm();
108 }
109
110 const QVideoFrameFormat &format() const { return m_format; }
111
112private:
113 Grabber(QX11SurfaceCapture &capture) : QFFmpegSurfaceCaptureGrabber()
114 {
115 addFrameCallback(&capture, &QX11SurfaceCapture::newVideoFrame);
116 connect(this, &Grabber::errorUpdated, &capture, &QX11SurfaceCapture::updateError);
117
118 m_userFrameRate = capture.frameRate();
119 }
120
121 bool createDisplay()
122 {
123 if (!m_display)
124 m_display.reset(XOpenDisplay(nullptr));
125
126 if (!m_display)
127 updateError(QPlatformSurfaceCapture::InternalError,
128 QStringLiteral("Cannot open X11 display"));
129
130 return m_display != nullptr;
131 }
132
133 bool init(WId wid)
134 {
135 const auto defaultRate = QGuiApplication::primaryScreen()
136 ? qMin(QGuiApplication::primaryScreen()->refreshRate(),
137 DefaultScreenCaptureFrameRate)
138 : DefaultScreenCaptureFrameRate;
139 setFrameRate(m_userFrameRate.value_or(defaultRate));
140
141 return createDisplay() && initWithXID(static_cast<XID>(wid));
142 }
143
144 bool init(QScreen *screen)
145 {
146 if (!screen) {
147 updateError(QPlatformSurfaceCapture::NotFound, QStringLiteral("Screen Not Found"));
148 return false;
149 }
150
151 if (!createDisplay())
152 return false;
153
154 auto screenNumber = screenNumberByName(m_display.get(), screen->name());
155
156 if (screenNumber < 0)
157 return false;
158
159 setFrameRate(m_userFrameRate.value_or(
160 qMin(screen->refreshRate(), DefaultScreenCaptureFrameRate)));
161
162 return initWithXID(RootWindow(m_display.get(), screenNumber));
163 }
164
165 bool initWithXID(XID xid)
166 {
167 m_xid = xid;
168
169 if (update()) {
170 start();
171 return true;
172 }
173
174 return false;
175 }
176
177 void detachShm()
178 {
179 if (std::exchange(m_attached, false)) {
180 XShmDetach(m_display.get(), &m_shmInfo);
181 shmdt(m_shmInfo.shmaddr);
182 shmctl(m_shmInfo.shmid, IPC_RMID, nullptr);
183 }
184 }
185
186 void attachShm()
187 {
188 Q_ASSERT(!m_attached);
189
190 m_shmInfo.shmid =
191 shmget(IPC_PRIVATE, m_xImage->bytes_per_line * m_xImage->height, IPC_CREAT | 0777);
192
193 if (m_shmInfo.shmid == -1)
194 return;
195
196 m_shmInfo.readOnly = false;
197 m_shmInfo.shmaddr = m_xImage->data = (char *)shmat(m_shmInfo.shmid, nullptr, 0);
198
199 m_attached = XShmAttach(m_display.get(), &m_shmInfo);
200 }
201
202 bool update()
203 {
204 Window win = m_xid;
205 XWindowAttributes wndattr;
206 int xPos = 0;
207 int yPos = 0;
208 int width = 0;
209 int height = 0;
210 int depth = 0;
211 Visual *visual = nullptr;
212 while (true) {
213 if (!XGetWindowAttributes(m_display.get(), win, &wndattr)) {
214 updateError(QPlatformSurfaceCapture::CaptureFailed,
215 QStringLiteral("Cannot get window attributes"));
216 return false;
217 }
218 if (win == m_xid) {
219 if (wndattr.map_state != IsViewable) {
220 updateError(QPlatformSurfaceCapture::CaptureFailed,
221 QStringLiteral("Window is not viewable"));
222 return false;
223 }
224 width = wndattr.width;
225 height = wndattr.height;
226 depth = wndattr.depth;
227 visual = wndattr.visual;
228 }
229 if (win == wndattr.root)
230 break;
231 xPos += wndattr.x;
232 yPos += wndattr.y;
233
234 Window root, *children;
235 unsigned int nchildren;
236 if (!XQueryTree(m_display.get(), win, &root, &win, &children, &nchildren)) {
237 updateError(QPlatformSurfaceCapture::CaptureFailed,
238 QStringLiteral("Cannot get parent window"));
239 return false;
240 }
241 if (children) XFree(children);
242 }
243
244 m_xOffset = qMax(0, -xPos);
245 m_yOffset = qMax(0, -yPos);
246 width = qMin(width, wndattr.width - xPos) - m_xOffset;
247 height = qMin(height, wndattr.height - yPos) - m_yOffset;
248 if (width <= 0 || height <= 0) {
249 updateError(QPlatformSurfaceCapture::CaptureFailed,
250 QStringLiteral("Window is completely out of the screen borders"));
251 return false;
252 }
253
254 // check window params for the root window as well since
255 // it potentially can be changed (e.g. on VM with resizing)
256 if (!m_xImage || width != m_xImage->width || height != m_xImage->height
257 || depth != m_xImage->depth || visual->visualid != m_visualID) {
258
259 qCDebug(qLcX11SurfaceCapture) << "recreate ximage: " << width << height
260 << depth << visual->visualid;
261
262 detachShm();
263 m_xImage.reset();
264
265 m_visualID = wndattr.visual->visualid;
266 m_xImage.reset(XShmCreateImage(m_display.get(), visual, depth, ZPixmap,
267 nullptr, &m_shmInfo, width, height));
268
269 if (!m_xImage) {
270 updateError(QPlatformSurfaceCapture::CaptureFailed,
271 QStringLiteral("Cannot create image"));
272 return false;
273 }
274
275 const auto pixelFormat = xImagePixelFormat(*m_xImage);
276
277 // TODO: probably, add a converter instead
278 if (pixelFormat == QVideoFrameFormat::Format_Invalid) {
279 updateError(QPlatformSurfaceCapture::CaptureFailed,
280 QStringLiteral("Not handled pixel format, bpp=")
281 + QString::number(m_xImage->bits_per_pixel));
282 return false;
283 }
284
285 attachShm();
286
287 if (!m_attached) {
288 updateError(QPlatformSurfaceCapture::CaptureFailed,
289 QStringLiteral("Cannot attach shared memory"));
290 return false;
291 }
292
293 QVideoFrameFormat format(QSize(m_xImage->width, m_xImage->height), pixelFormat);
294 format.setStreamFrameRate(frameRate());
295 m_format = format;
296 }
297
298 return m_attached;
299 }
300
301protected:
303 {
304 if (!grabXImage())
305 return {};
306
307 QByteArray data(m_xImage->bytes_per_line * m_xImage->height, Qt::Uninitialized);
308
309 const auto pixelSrc = reinterpret_cast<const uint32_t *>(m_xImage->data);
310 const auto pixelDst = reinterpret_cast<uint32_t *>(data.data());
311 const auto pixelCount = data.size() / 4;
312 const auto xImageAlphaVaries = false; // In known cases it doesn't vary - it's 0xff or 0xff
313
314 qCopyPixelsWithAlphaMask(pixelDst, pixelSrc, pixelCount, m_format.pixelFormat(),
315 xImageAlphaVaries);
316
317 auto buffer = std::make_unique<QMemoryVideoBuffer>(data, m_xImage->bytes_per_line);
318 return QVideoFramePrivate::createFrame(std::move(buffer), m_format);
319 }
320
321private:
322 bool grabXImage()
323 {
324 for (int i = 0; i < 10; i++) {
325 if (!update())
326 return false;
327 if (XShmGetImage(m_display.get(), m_xid, m_xImage.get(),
328 m_xOffset, m_yOffset, AllPlanes))
329 return true;
330 }
331
332 updateError(QPlatformSurfaceCapture::CaptureFailed,
333 QStringLiteral(
334 "Cannot get ximage; the window geometry may be undergoing change"));
335 return false;
336 }
337
338 std::optional<QPlatformSurfaceCapture::Error> m_prevGrabberError;
339 XID m_xid = None;
340 int m_xOffset = 0;
341 int m_yOffset = 0;
342 std::unique_ptr<Display, decltype(&XCloseDisplay)> m_display{ nullptr, &XCloseDisplay };
343 std::unique_ptr<XImage, decltype(&destroyXImage)> m_xImage{ nullptr, &destroyXImage };
344 XShmSegmentInfo m_shmInfo{};
345 bool m_attached = false;
346 VisualID m_visualID = None;
347 QVideoFrameFormat m_format;
348 std::optional<qreal> m_userFrameRate;
349};
350
351QX11SurfaceCapture::QX11SurfaceCapture(Source initialSource)
352 : QPlatformSurfaceCapture(std::move(initialSource))
353{
354 // For debug
355 // XSetErrorHandler([](Display *, XErrorEvent * e) {
356 // qDebug() << "error handler" << e->error_code;
357 // return 0;
358 // });
359}
360
361QX11SurfaceCapture::~QX11SurfaceCapture() = default;
362
363QVideoFrameFormat QX11SurfaceCapture::frameFormat() const
364{
365 return m_grabber ? m_grabber->format() : QVideoFrameFormat{};
366}
367
368bool QX11SurfaceCapture::setActiveInternal(bool active)
369{
370 qCDebug(qLcX11SurfaceCapture) << "set active" << active;
371
372 if (m_grabber)
373 m_grabber.reset();
374 else
375 std::visit([this](const auto &source) {
376 activate(source);
377 }, source());
378
379 return static_cast<bool>(m_grabber) == active;
380}
381
382void QX11SurfaceCapture::activate(ScreenSource screen)
383{
384 if (checkScreenWithError(screen))
385 m_grabber = Grabber::create(*this, screen);
386}
387
388void QX11SurfaceCapture::activate(const WindowSource &window)
389{
390 auto handle = QCapturableWindowPrivate::handle(window);
391 m_grabber = Grabber::create(*this, handle ? handle->id : 0);
392}
393
394bool QX11SurfaceCapture::isSupported()
395{
396 return qgetenv("XDG_SESSION_TYPE").compare(QLatin1String("x11"), Qt::CaseInsensitive) == 0;
397}
398
399QT_END_NAMESPACE
static std::unique_ptr< Grabber > create(QX11SurfaceCapture &capture, QScreen *screen)
const QVideoFrameFormat & format() const
QVideoFrame grabFrame() override
static std::unique_ptr< Grabber > create(QX11SurfaceCapture &capture, WId wid)
#define qCDebug(category,...)
#define Q_STATIC_LOGGING_CATEGORY(name,...)