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