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

source code of qtmultimedia/src/plugins/multimedia/ffmpeg/qx11surfacecapture.cpp