Changeset View
Changeset View
Standalone View
Standalone View
framebuffers/pipewire/pw_framebuffer.cpp
- This file was added.
1 | /* This file is part of the KDE project | ||||
---|---|---|---|---|---|
2 | Copyright (C) 2018 Oleg Chernovskiy <kanedias@xaker.ru> | ||||
3 | Copyright (C) 2018 Jan Grulich <jgrulich@redhat.com> | ||||
4 | | ||||
5 | This program is free software; you can redistribute it and/or | ||||
6 | modify it under the terms of the GNU General Public | ||||
7 | License as published by the Free Software Foundation; either | ||||
8 | version 3 of the License, or (at your option) any later version. | ||||
9 | */ | ||||
10 | | ||||
11 | // system | ||||
12 | #include <sys/mman.h> | ||||
13 | #include <cstring> | ||||
14 | | ||||
15 | // Qt | ||||
16 | #include <QX11Info> | ||||
17 | #include <QCoreApplication> | ||||
18 | #include <QGuiApplication> | ||||
19 | #include <QScreen> | ||||
20 | #include <QSocketNotifier> | ||||
21 | #include <QDebug> | ||||
22 | #if (QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)) | ||||
23 | #include <QRandomGenerator> | ||||
24 | #endif | ||||
25 | | ||||
26 | // pipewire | ||||
27 | #include <pipewire/version.h> | ||||
28 | | ||||
29 | #if !PW_CHECK_VERSION(0, 2, 9) | ||||
30 | #include <spa/support/type-map.h> | ||||
31 | #include <spa/param/format-utils.h> | ||||
32 | #include <spa/param/video/format-utils.h> | ||||
33 | #include <spa/param/video/raw-utils.h> | ||||
34 | #endif | ||||
35 | #include <spa/param/video/format-utils.h> | ||||
36 | #include <spa/param/props.h> | ||||
37 | | ||||
38 | #include <pipewire/factory.h> | ||||
39 | #include <pipewire/pipewire.h> | ||||
40 | #include <pipewire/remote.h> | ||||
41 | #include <pipewire/stream.h> | ||||
42 | | ||||
43 | #include <limits.h> | ||||
44 | | ||||
45 | #include "pw_framebuffer.h" | ||||
46 | #include "xdp_dbus_screencast_interface.h" | ||||
47 | #include "xdp_dbus_remotedesktop_interface.h" | ||||
48 | | ||||
49 | static const uint MIN_SUPPORTED_XDP_KDE_SC_VERSION = 1; | ||||
50 | | ||||
51 | Q_DECLARE_METATYPE(PWFrameBuffer::Stream); | ||||
52 | Q_DECLARE_METATYPE(PWFrameBuffer::Streams); | ||||
53 | | ||||
54 | const QDBusArgument &operator >> (const QDBusArgument &arg, PWFrameBuffer::Stream &stream) | ||||
55 | { | ||||
56 | arg.beginStructure(); | ||||
57 | arg >> stream.nodeId; | ||||
58 | | ||||
59 | arg.beginMap(); | ||||
60 | while (!arg.atEnd()) { | ||||
61 | QString key; | ||||
62 | QVariant map; | ||||
63 | arg.beginMapEntry(); | ||||
64 | arg >> key >> map; | ||||
65 | arg.endMapEntry(); | ||||
66 | stream.map.insert(key, map); | ||||
67 | } | ||||
68 | arg.endMap(); | ||||
69 | arg.endStructure(); | ||||
70 | | ||||
71 | return arg; | ||||
72 | } | ||||
73 | | ||||
74 | #if !PW_CHECK_VERSION(0, 2, 9) | ||||
75 | /** | ||||
76 | * @brief The PwType class - helper class to contain pointers to raw C pipewire media mappings | ||||
77 | */ | ||||
78 | class PwType { | ||||
79 | public: | ||||
80 | spa_type_media_type media_type; | ||||
81 | spa_type_media_subtype media_subtype; | ||||
82 | spa_type_format_video format_video; | ||||
83 | spa_type_video_format video_format; | ||||
84 | }; | ||||
85 | #endif | ||||
86 | | ||||
87 | /** | ||||
88 | * @brief The PWFrameBuffer::Private class - private counterpart of PWFramebuffer class. This is the entity where | ||||
89 | * whole logic resides, for more info search for "d-pointer pattern" information. | ||||
90 | */ | ||||
91 | class PWFrameBuffer::Private { | ||||
92 | public: | ||||
93 | Private(PWFrameBuffer *q); | ||||
94 | ~Private(); | ||||
95 | | ||||
96 | private: | ||||
97 | friend class PWFrameBuffer; | ||||
98 | | ||||
99 | static void onStateChanged(void *data, pw_remote_state old, pw_remote_state state, const char *error); | ||||
100 | static void onStreamStateChanged(void *data, pw_stream_state old, pw_stream_state state, const char *error_message); | ||||
101 | static void onStreamFormatChanged(void *data, const struct spa_pod *format); | ||||
102 | static void onStreamProcess(void *data); | ||||
103 | | ||||
104 | void initDbus(); | ||||
105 | void initPw(); | ||||
106 | #if !PW_CHECK_VERSION(0, 2, 9) | ||||
107 | void initializePwTypes(); | ||||
108 | #endif | ||||
109 | | ||||
110 | // dbus handling | ||||
111 | void handleSessionCreated(quint32 &code, QVariantMap &results); | ||||
112 | void handleDevicesSelected(quint32 &code, QVariantMap &results); | ||||
113 | void handleSourcesSelected(quint32 &code, QVariantMap &results); | ||||
114 | void handleRemoteDesktopStarted(quint32 &code, QVariantMap &results); | ||||
115 | | ||||
116 | // pw handling | ||||
117 | void createReceivingStream(); | ||||
118 | void handleFrame(pw_buffer *pwBuffer); | ||||
119 | | ||||
120 | // link to public interface | ||||
121 | PWFrameBuffer *q; | ||||
122 | | ||||
123 | // pipewire stuff | ||||
124 | #if PW_CHECK_VERSION(0, 2, 9) | ||||
125 | struct pw_core *pwCore = nullptr; | ||||
126 | struct pw_loop *pwLoop = nullptr; | ||||
127 | struct pw_stream *pwStream = nullptr; | ||||
128 | struct pw_remote *pwRemote = nullptr; | ||||
129 | struct pw_thread_loop *pwMainLoop = nullptr; | ||||
130 | #else | ||||
131 | pw_core *pwCore = nullptr; | ||||
132 | pw_loop *pwLoop = nullptr; | ||||
133 | pw_stream *pwStream = nullptr; | ||||
134 | pw_remote *pwRemote = nullptr; | ||||
135 | pw_thread_loop *pwMainLoop = nullptr; | ||||
136 | pw_type *pwCoreType = nullptr; | ||||
137 | PwType *pwType = nullptr; | ||||
138 | #endif | ||||
139 | | ||||
140 | uint pwStreamNodeId = 0; | ||||
141 | | ||||
142 | // event handlers | ||||
143 | pw_remote_events pwRemoteEvents = {}; | ||||
144 | pw_stream_events pwStreamEvents = {}; | ||||
145 | | ||||
146 | // wayland-like listeners | ||||
147 | // ...of events that happen in pipewire server | ||||
148 | spa_hook remoteListener = {}; | ||||
149 | // ...of events that happen with the stream we consume | ||||
150 | spa_hook streamListener = {}; | ||||
151 | | ||||
152 | // negotiated video format | ||||
153 | spa_video_info_raw *videoFormat = nullptr; | ||||
154 | | ||||
155 | // requests a session from XDG Desktop Portal | ||||
156 | // auto-generated and compiled from xdp_dbus_interface.xml file | ||||
157 | QScopedPointer<OrgFreedesktopPortalScreenCastInterface> dbusXdpScreenCastService; | ||||
158 | QScopedPointer<OrgFreedesktopPortalRemoteDesktopInterface> dbusXdpRemoteDesktopService; | ||||
159 | | ||||
160 | // XDP screencast session handle | ||||
161 | QDBusObjectPath sessionPath; | ||||
162 | // Pipewire file descriptor | ||||
163 | QDBusUnixFileDescriptor pipewireFd; | ||||
164 | | ||||
165 | // screen geometry holder | ||||
166 | struct { | ||||
167 | quint32 width; | ||||
168 | quint32 height; | ||||
169 | } screenGeometry; | ||||
170 | | ||||
171 | // Allowed devices | ||||
172 | uint devices; | ||||
173 | | ||||
174 | // sanity indicator | ||||
175 | bool isValid = true; | ||||
176 | }; | ||||
177 | | ||||
178 | PWFrameBuffer::Private::Private(PWFrameBuffer *q) : q(q) | ||||
179 | { | ||||
180 | // initialize event handlers, remote end and stream-related | ||||
181 | pwRemoteEvents.version = PW_VERSION_REMOTE_EVENTS; | ||||
182 | pwRemoteEvents.state_changed = &onStateChanged; | ||||
183 | | ||||
184 | pwStreamEvents.version = PW_VERSION_STREAM_EVENTS; | ||||
185 | pwStreamEvents.state_changed = &onStreamStateChanged; | ||||
186 | pwStreamEvents.format_changed = &onStreamFormatChanged; | ||||
187 | pwStreamEvents.process = &onStreamProcess; | ||||
188 | } | ||||
189 | | ||||
190 | /** | ||||
191 | * @brief PWFrameBuffer::Private::initDbus - initialize D-Bus connectivity with XDG Desktop Portal. | ||||
192 | * Based on XDG_CURRENT_DESKTOP environment variable it will give us implementation that we need, | ||||
193 | * in case of KDE it is xdg-desktop-portal-kde binary. | ||||
194 | */ | ||||
195 | void PWFrameBuffer::Private::initDbus() | ||||
196 | { | ||||
197 | qInfo() << "Initializing D-Bus connectivity with XDG Desktop Portal"; | ||||
198 | dbusXdpScreenCastService.reset(new OrgFreedesktopPortalScreenCastInterface(QLatin1String("org.freedesktop.portal.Desktop"), | ||||
199 | QLatin1String("/org/freedesktop/portal/desktop"), | ||||
200 | QDBusConnection::sessionBus())); | ||||
201 | dbusXdpRemoteDesktopService.reset(new OrgFreedesktopPortalRemoteDesktopInterface(QLatin1String("org.freedesktop.portal.Desktop"), | ||||
202 | QLatin1String("/org/freedesktop/portal/desktop"), | ||||
203 | QDBusConnection::sessionBus())); | ||||
204 | auto version = dbusXdpScreenCastService->version(); | ||||
205 | if (version < MIN_SUPPORTED_XDP_KDE_SC_VERSION) { | ||||
206 | qWarning() << "Unsupported XDG Portal screencast interface version:" << version; | ||||
207 | isValid = false; | ||||
208 | return; | ||||
209 | } | ||||
210 | | ||||
211 | // create session | ||||
212 | auto sessionParameters = QVariantMap { | ||||
213 | #if (QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)) | ||||
214 | { QLatin1String("session_handle_token"), QStringLiteral("krfb%1").arg(QRandomGenerator::global()->generate()) }, | ||||
215 | { QLatin1String("handle_token"), QStringLiteral("krfb%1").arg(QRandomGenerator::global()->generate()) } | ||||
216 | #else | ||||
217 | { QLatin1String("session_handle_token"), QStringLiteral("krfb%1").arg(qrand()) }, | ||||
218 | { QLatin1String("handle_token"), QStringLiteral("krfb%1").arg(qrand()) } | ||||
219 | #endif | ||||
220 | }; | ||||
221 | auto sessionReply = dbusXdpRemoteDesktopService->CreateSession(sessionParameters); | ||||
222 | sessionReply.waitForFinished(); | ||||
223 | if (!sessionReply.isValid()) { | ||||
224 | qWarning("Couldn't initialize XDP-KDE screencast session"); | ||||
225 | isValid = false; | ||||
226 | return; | ||||
227 | } | ||||
228 | | ||||
229 | qInfo() << "DBus session created: " << sessionReply.value().path(); | ||||
230 | QDBusConnection::sessionBus().connect(QString(), | ||||
231 | sessionReply.value().path(), | ||||
232 | QLatin1String("org.freedesktop.portal.Request"), | ||||
233 | QLatin1String("Response"), | ||||
234 | this->q, | ||||
235 | SLOT(handleXdpSessionCreated(uint, QVariantMap))); | ||||
236 | } | ||||
237 | | ||||
238 | void PWFrameBuffer::handleXdpSessionCreated(quint32 code, QVariantMap results) | ||||
239 | { | ||||
240 | d->handleSessionCreated(code, results); | ||||
241 | } | ||||
242 | | ||||
243 | /** | ||||
244 | * @brief PWFrameBuffer::Private::handleSessionCreated - handle creation of ScreenCast session. | ||||
245 | * XDG Portal answers with session path if it was able to successfully create the screencast. | ||||
246 | * | ||||
247 | * @param code return code for dbus call. Zero is success, non-zero means error | ||||
248 | * @param results map with results of call. | ||||
249 | */ | ||||
250 | void PWFrameBuffer::Private::handleSessionCreated(quint32 &code, QVariantMap &results) | ||||
251 | { | ||||
252 | if (code != 0) { | ||||
253 | qWarning() << "Failed to create session: " << code; | ||||
254 | isValid = false; | ||||
255 | return; | ||||
256 | } | ||||
257 | | ||||
258 | sessionPath = QDBusObjectPath(results.value(QLatin1String("session_handle")).toString()); | ||||
259 | | ||||
260 | // select sources for the session | ||||
261 | auto selectionOptions = QVariantMap { | ||||
262 | // We have to specify it's an uint, otherwise xdg-desktop-portal will not forward it to backend implementation | ||||
263 | { QLatin1String("types"),QVariant::fromValue<uint>(7) }, // request all (KeyBoard, Pointer, TouchScreen) | ||||
264 | #if (QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)) | ||||
265 | { QLatin1String("handle_token"), QStringLiteral("krfb%1").arg(QRandomGenerator::global()->generate()) } | ||||
266 | #else | ||||
267 | { QLatin1String("handle_token"), QStringLiteral("krfb%1").arg(qrand()) } | ||||
268 | #endif | ||||
269 | }; | ||||
270 | auto selectorReply = dbusXdpRemoteDesktopService->SelectDevices(sessionPath, selectionOptions); | ||||
271 | selectorReply.waitForFinished(); | ||||
272 | if (!selectorReply.isValid()) { | ||||
273 | qWarning() << "Couldn't select devices for the remote-desktop session"; | ||||
274 | isValid = false; | ||||
275 | return; | ||||
276 | } | ||||
277 | QDBusConnection::sessionBus().connect(QString(), | ||||
278 | selectorReply.value().path(), | ||||
279 | QLatin1String("org.freedesktop.portal.Request"), | ||||
280 | QLatin1String("Response"), | ||||
281 | this->q, | ||||
282 | SLOT(handleXdpDevicesSelected(uint, QVariantMap))); | ||||
283 | } | ||||
284 | | ||||
285 | void PWFrameBuffer::handleXdpDevicesSelected(quint32 code, QVariantMap results) | ||||
286 | { | ||||
287 | d->handleDevicesSelected(code, results); | ||||
288 | } | ||||
289 | | ||||
290 | /** | ||||
291 | * @brief PWFrameBuffer::Private::handleDevicesCreated - handle selection of devices we want to use for remote desktop | ||||
292 | * | ||||
293 | * @param code return code for dbus call. Zero is success, non-zero means error | ||||
294 | * @param results map with results of call. | ||||
295 | */ | ||||
296 | void PWFrameBuffer::Private::handleDevicesSelected(quint32 &code, QVariantMap &results) | ||||
297 | { | ||||
298 | if (code != 0) { | ||||
299 | qWarning() << "Failed to select devices: " << code; | ||||
300 | isValid = false; | ||||
301 | return; | ||||
302 | } | ||||
303 | | ||||
304 | // select sources for the session | ||||
305 | auto selectionOptions = QVariantMap { | ||||
306 | { QLatin1String("types"), 1 }, // only MONITOR is supported | ||||
307 | { QLatin1String("multiple"), false }, | ||||
308 | #if (QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)) | ||||
309 | { QLatin1String("handle_token"), QStringLiteral("krfb%1").arg(QRandomGenerator::global()->generate()) } | ||||
310 | #else | ||||
311 | { QLatin1String("handle_token"), QStringLiteral("krfb%1").arg(qrand()) } | ||||
312 | #endif | ||||
313 | }; | ||||
314 | auto selectorReply = dbusXdpScreenCastService->SelectSources(sessionPath, selectionOptions); | ||||
315 | selectorReply.waitForFinished(); | ||||
316 | if (!selectorReply.isValid()) { | ||||
317 | qWarning() << "Couldn't select sources for the screen-casting session"; | ||||
318 | isValid = false; | ||||
319 | return; | ||||
320 | } | ||||
321 | QDBusConnection::sessionBus().connect(QString(), | ||||
322 | selectorReply.value().path(), | ||||
323 | QLatin1String("org.freedesktop.portal.Request"), | ||||
324 | QLatin1String("Response"), | ||||
325 | this->q, | ||||
326 | SLOT(handleXdpSourcesSelected(uint, QVariantMap))); | ||||
327 | } | ||||
328 | | ||||
329 | void PWFrameBuffer::handleXdpSourcesSelected(quint32 code, QVariantMap results) | ||||
330 | { | ||||
331 | d->handleSourcesSelected(code, results); | ||||
332 | } | ||||
333 | | ||||
334 | /** | ||||
335 | * @brief PWFrameBuffer::Private::handleSourcesSelected - handle Screencast sources selection. | ||||
336 | * XDG Portal shows a dialog at this point which allows you to select monitor from the list. | ||||
337 | * This function is called after you make a selection. | ||||
338 | * | ||||
339 | * @param code return code for dbus call. Zero is success, non-zero means error | ||||
340 | * @param results map with results of call. | ||||
341 | */ | ||||
342 | void PWFrameBuffer::Private::handleSourcesSelected(quint32 &code, QVariantMap &) | ||||
343 | { | ||||
344 | if (code != 0) { | ||||
345 | qWarning() << "Failed to select sources: " << code; | ||||
346 | isValid = false; | ||||
347 | return; | ||||
348 | } | ||||
349 | | ||||
350 | // start session | ||||
351 | auto startParameters = QVariantMap { | ||||
352 | #if (QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)) | ||||
353 | { QLatin1String("handle_token"), QStringLiteral("krfb%1").arg(QRandomGenerator::global()->generate()) } | ||||
354 | #else | ||||
355 | { QLatin1String("handle_token"), QStringLiteral("krfb%1").arg(qrand()) } | ||||
356 | #endif | ||||
357 | }; | ||||
358 | auto startReply = dbusXdpRemoteDesktopService->Start(sessionPath, QString(), startParameters); | ||||
359 | startReply.waitForFinished(); | ||||
360 | QDBusConnection::sessionBus().connect(QString(), | ||||
361 | startReply.value().path(), | ||||
362 | QLatin1String("org.freedesktop.portal.Request"), | ||||
363 | QLatin1String("Response"), | ||||
364 | this->q, | ||||
365 | SLOT(handleXdpRemoteDesktopStarted(uint, QVariantMap))); | ||||
366 | } | ||||
367 | | ||||
368 | | ||||
369 | void PWFrameBuffer::handleXdpRemoteDesktopStarted(quint32 code, QVariantMap results) | ||||
370 | { | ||||
371 | d->handleRemoteDesktopStarted(code, results); | ||||
372 | } | ||||
373 | | ||||
374 | /** | ||||
375 | * @brief PWFrameBuffer::Private::handleScreencastStarted - handle Screencast start. | ||||
376 | * At this point there shall be ready pipewire stream to consume. | ||||
377 | * | ||||
378 | * @param code return code for dbus call. Zero is success, non-zero means error | ||||
379 | * @param results map with results of call. | ||||
380 | */ | ||||
381 | void PWFrameBuffer::Private::handleRemoteDesktopStarted(quint32 &code, QVariantMap &results) | ||||
382 | { | ||||
383 | if (code != 0) { | ||||
384 | qWarning() << "Failed to start screencast: " << code; | ||||
385 | isValid = false; | ||||
386 | return; | ||||
387 | } | ||||
388 | | ||||
389 | // there should be only one stream | ||||
390 | Streams streams = qdbus_cast<Streams>(results.value(QLatin1String("streams"))); | ||||
391 | if (streams.isEmpty()) { | ||||
392 | // maybe we should check deeper with qdbus_cast but this suffices for now | ||||
393 | qWarning() << "Failed to get screencast streams"; | ||||
394 | isValid = false; | ||||
395 | return; | ||||
396 | } | ||||
397 | | ||||
398 | auto streamReply = dbusXdpScreenCastService->OpenPipeWireRemote(sessionPath, QVariantMap()); | ||||
399 | streamReply.waitForFinished(); | ||||
400 | if (!streamReply.isValid()) { | ||||
401 | qWarning() << "Couldn't open pipewire remote for the screen-casting session"; | ||||
402 | isValid = false; | ||||
403 | return; | ||||
404 | } | ||||
405 | | ||||
406 | pipewireFd = streamReply.value(); | ||||
407 | if (!pipewireFd.isValid()) { | ||||
408 | qWarning() << "Couldn't get pipewire connection file descriptor"; | ||||
409 | isValid = false; | ||||
410 | return; | ||||
411 | } | ||||
412 | | ||||
413 | QSize streamResolution = qdbus_cast<QSize>(streams.first().map.value(QLatin1String("size"))); | ||||
414 | screenGeometry.width = streamResolution.width(); | ||||
415 | screenGeometry.height = streamResolution.height(); | ||||
416 | | ||||
417 | devices = results.value(QLatin1String("types")).toUInt(); | ||||
418 | | ||||
419 | pwStreamNodeId = streams.first().nodeId; | ||||
420 | | ||||
421 | // Reallocate our buffer with actual needed size | ||||
422 | q->fb = static_cast<char*>(malloc(screenGeometry.width * screenGeometry.height * 4)); | ||||
423 | | ||||
424 | if (!q->fb) { | ||||
425 | qWarning() << "Failed to allocate buffer"; | ||||
426 | isValid = false; | ||||
427 | return; | ||||
428 | } | ||||
429 | | ||||
430 | Q_EMIT q->frameBufferChanged(); | ||||
431 | | ||||
432 | initPw(); | ||||
433 | } | ||||
434 | | ||||
435 | /** | ||||
436 | * @brief PWFrameBuffer::Private::initPw - initialize Pipewire socket connectivity. | ||||
437 | * pipewireFd should be pointing to existing file descriptor that was passed by D-Bus at this point. | ||||
438 | */ | ||||
439 | void PWFrameBuffer::Private::initPw() { | ||||
440 | qInfo() << "Initializing Pipewire connectivity"; | ||||
441 | | ||||
442 | // init pipewire (required) | ||||
443 | pw_init(nullptr, nullptr); // args are not used anyways | ||||
444 | | ||||
445 | // initialize our source | ||||
446 | pwLoop = pw_loop_new(nullptr); | ||||
447 | pwMainLoop = pw_thread_loop_new(pwLoop, "pipewire-main-loop"); | ||||
448 | | ||||
449 | // create PipeWire core object (required) | ||||
450 | pwCore = pw_core_new(pwLoop, nullptr); | ||||
451 | #if !PW_CHECK_VERSION(0, 2, 9) | ||||
452 | pwCoreType = pw_core_get_type(pwCore); | ||||
453 | | ||||
454 | // init type maps | ||||
455 | initializePwTypes(); | ||||
456 | #endif | ||||
457 | | ||||
458 | // pw_remote should be initialized before type maps or connection error will happen | ||||
459 | pwRemote = pw_remote_new(pwCore, nullptr, 0); | ||||
460 | | ||||
461 | // init PipeWire remote, add listener to handle events | ||||
462 | pw_remote_add_listener(pwRemote, &remoteListener, &pwRemoteEvents, this); | ||||
463 | pw_remote_connect_fd(pwRemote, pipewireFd.fileDescriptor()); | ||||
464 | | ||||
465 | if (pw_thread_loop_start(pwMainLoop) < 0) { | ||||
466 | qWarning() << "Failed to start main PipeWire loop"; | ||||
467 | isValid = false; | ||||
468 | } | ||||
469 | } | ||||
470 | | ||||
471 | #if !PW_CHECK_VERSION(0, 2, 9) | ||||
472 | /** | ||||
473 | * @brief PWFrameBuffer::Private::initializePwTypes - helper method to initialize and map all needed | ||||
474 | * Pipewire types from core to type structure. | ||||
475 | */ | ||||
476 | void PWFrameBuffer::Private::initializePwTypes() | ||||
477 | { | ||||
478 | // raw C-like PipeWire type map | ||||
479 | spa_type_map *map = pwCoreType->map; | ||||
480 | pwType = new PwType(); | ||||
481 | | ||||
482 | spa_type_media_type_map(map, &pwType->media_type); | ||||
483 | spa_type_media_subtype_map(map, &pwType->media_subtype); | ||||
484 | spa_type_format_video_map(map, &pwType->format_video); | ||||
485 | spa_type_video_format_map(map, &pwType->video_format); | ||||
486 | } | ||||
487 | #endif | ||||
488 | | ||||
489 | /** | ||||
490 | * @brief PWFrameBuffer::Private::onStateChanged - global state tracking for pipewire connection | ||||
491 | * @param data pointer that you have set in pw_remote_add_listener call's last argument | ||||
492 | * @param state new state that connection has changed to | ||||
493 | * @param error optional error message, is set to non-null if state is error | ||||
494 | */ | ||||
495 | void PWFrameBuffer::Private::onStateChanged(void *data, pw_remote_state /*old*/, pw_remote_state state, const char *error) | ||||
496 | { | ||||
497 | qInfo() << "remote state: " << pw_remote_state_as_string(state); | ||||
498 | | ||||
499 | PWFrameBuffer::Private *d = static_cast<PWFrameBuffer::Private*>(data); | ||||
500 | | ||||
501 | switch (state) { | ||||
502 | case PW_REMOTE_STATE_ERROR: | ||||
503 | qWarning() << "remote error: " << error; | ||||
504 | break; | ||||
505 | case PW_REMOTE_STATE_CONNECTED: | ||||
506 | d->createReceivingStream(); | ||||
507 | break; | ||||
508 | default: | ||||
509 | qInfo() << "remote state: " << pw_remote_state_as_string(state); | ||||
510 | break; | ||||
511 | } | ||||
512 | } | ||||
513 | | ||||
514 | /** | ||||
515 | * @brief PWFrameBuffer::Private::onStreamStateChanged - called whenever stream state changes on pipewire server | ||||
516 | * @param data pointer that you have set in pw_stream_add_listener call's last argument | ||||
517 | * @param state new state that stream has changed to | ||||
518 | * @param error_message optional error message, is set to non-null if state is error | ||||
519 | */ | ||||
520 | void PWFrameBuffer::Private::onStreamStateChanged(void *data, pw_stream_state /*old*/, pw_stream_state state, const char *error_message) | ||||
521 | { | ||||
522 | qInfo() << "Stream state changed: " << pw_stream_state_as_string(state); | ||||
523 | | ||||
524 | auto *d = static_cast<PWFrameBuffer::Private *>(data); | ||||
525 | | ||||
526 | switch (state) { | ||||
527 | case PW_STREAM_STATE_ERROR: | ||||
528 | qWarning() << "pipewire stream error: " << error_message; | ||||
529 | break; | ||||
530 | case PW_STREAM_STATE_CONFIGURE: | ||||
531 | pw_stream_set_active(d->pwStream, true); | ||||
532 | break; | ||||
533 | default: | ||||
534 | break; | ||||
535 | } | ||||
536 | } | ||||
537 | | ||||
538 | /** | ||||
539 | * @brief PWFrameBuffer::Private::onStreamFormatChanged - being executed after stream is set to active | ||||
540 | * and after setup has been requested to connect to it. The actual video format is being negotiated here. | ||||
541 | * @param data pointer that you have set in pw_stream_add_listener call's last argument | ||||
542 | * @param format format that's being proposed | ||||
543 | */ | ||||
544 | #if defined(PW_API_PRE_0_2_0) | ||||
545 | void PWFrameBuffer::Private::onStreamFormatChanged(void *data, struct spa_pod *format) | ||||
546 | #else | ||||
547 | void PWFrameBuffer::Private::onStreamFormatChanged(void *data, const struct spa_pod *format) | ||||
548 | #endif // defined(PW_API_PRE_0_2_0) | ||||
549 | { | ||||
550 | qInfo() << "Stream format changed"; | ||||
551 | auto *d = static_cast<PWFrameBuffer::Private *>(data); | ||||
552 | | ||||
553 | const int bpp = 4; | ||||
554 | | ||||
555 | if (!format) { | ||||
556 | pw_stream_finish_format(d->pwStream, 0, nullptr, 0); | ||||
557 | return; | ||||
558 | } | ||||
559 | | ||||
560 | d->videoFormat = new spa_video_info_raw(); | ||||
561 | #if PW_CHECK_VERSION(0, 2, 9) | ||||
562 | spa_format_video_raw_parse(format, d->videoFormat); | ||||
563 | #else | ||||
564 | spa_format_video_raw_parse(format, d->videoFormat, &d->pwType->format_video); | ||||
565 | #endif | ||||
566 | auto width = d->videoFormat->size.width; | ||||
567 | auto height = d->videoFormat->size.height; | ||||
568 | auto stride = SPA_ROUND_UP_N(width * bpp, 4); | ||||
569 | auto size = height * stride; | ||||
570 | | ||||
571 | uint8_t buffer[1024]; | ||||
572 | auto builder = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer)); | ||||
573 | | ||||
574 | // setup buffers and meta header for new format | ||||
575 | const struct spa_pod *params[2]; | ||||
576 | | ||||
577 | #if PW_CHECK_VERSION(0, 2, 9) | ||||
578 | params[0] = reinterpret_cast<spa_pod *>(spa_pod_builder_add_object(&builder, | ||||
579 | SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers, | ||||
580 | ":", SPA_PARAM_BUFFERS_size, "i", size, | ||||
581 | ":", SPA_PARAM_BUFFERS_stride, "i", stride, | ||||
582 | ":", SPA_PARAM_BUFFERS_buffers, "?ri", SPA_CHOICE_RANGE(8, 1, 32), | ||||
583 | ":", SPA_PARAM_BUFFERS_align, "i", 16)); | ||||
584 | params[1] = reinterpret_cast<spa_pod *>(spa_pod_builder_add_object(&builder, | ||||
585 | SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta, | ||||
586 | ":", SPA_PARAM_META_type, "I", SPA_META_Header, | ||||
587 | ":", SPA_PARAM_META_size, "i", sizeof(struct spa_meta_header))); | ||||
588 | #else | ||||
589 | params[0] = reinterpret_cast<spa_pod *>(spa_pod_builder_object(&builder, | ||||
590 | d->pwCoreType->param.idBuffers, d->pwCoreType->param_buffers.Buffers, | ||||
591 | ":", d->pwCoreType->param_buffers.size, "i", size, | ||||
592 | ":", d->pwCoreType->param_buffers.stride, "i", stride, | ||||
593 | ":", d->pwCoreType->param_buffers.buffers, "iru", 8, SPA_POD_PROP_MIN_MAX(1, 32), | ||||
594 | ":", d->pwCoreType->param_buffers.align, "i", 16)); | ||||
595 | params[1] = reinterpret_cast<spa_pod *>(spa_pod_builder_object(&builder, | ||||
596 | d->pwCoreType->param.idMeta, d->pwCoreType->param_meta.Meta, | ||||
597 | ":", d->pwCoreType->param_meta.type, "I", d->pwCoreType->meta.Header, | ||||
598 | ":", d->pwCoreType->param_meta.size, "i", sizeof(struct spa_meta_header))); | ||||
599 | #endif | ||||
600 | | ||||
601 | pw_stream_finish_format(d->pwStream, 0, params, 2); | ||||
602 | } | ||||
603 | | ||||
604 | /** | ||||
605 | * @brief PWFrameBuffer::Private::onNewBuffer - called when new buffer is available in pipewire stream | ||||
606 | * @param data pointer that you have set in pw_stream_add_listener call's last argument | ||||
607 | * @param id | ||||
608 | */ | ||||
609 | void PWFrameBuffer::Private::onStreamProcess(void *data) | ||||
610 | { | ||||
611 | auto *d = static_cast<PWFrameBuffer::Private *>(data); | ||||
612 | | ||||
613 | pw_buffer *buf; | ||||
614 | if (!(buf = pw_stream_dequeue_buffer(d->pwStream))) { | ||||
615 | return; | ||||
616 | } | ||||
617 | | ||||
618 | d->handleFrame(buf); | ||||
619 | | ||||
620 | pw_stream_queue_buffer(d->pwStream, buf); | ||||
621 | } | ||||
622 | | ||||
623 | void PWFrameBuffer::Private::handleFrame(pw_buffer *pwBuffer) | ||||
624 | { | ||||
625 | auto *spaBuffer = pwBuffer->buffer; | ||||
626 | void *src = nullptr; | ||||
627 | | ||||
628 | src = spaBuffer->datas[0].data; | ||||
629 | if (!src) { | ||||
630 | return; | ||||
631 | } | ||||
632 | | ||||
633 | quint32 maxSize = spaBuffer->datas[0].maxsize; | ||||
634 | qint32 srcStride = spaBuffer->datas[0].chunk->stride; | ||||
635 | if (srcStride != q->paddedWidth()) { | ||||
636 | qWarning() << "Got buffer with stride different from screen stride" << srcStride << "!=" << q->paddedWidth(); | ||||
637 | return; | ||||
638 | } | ||||
639 | | ||||
640 | q->tiles.append(QRect(0, 0, q->width(), q->height())); | ||||
641 | std::memcpy(q->fb, src, maxSize); | ||||
642 | } | ||||
643 | | ||||
644 | /** | ||||
645 | * @brief PWFrameBuffer::Private::createReceivingStream - create a stream that will consume Pipewire buffers | ||||
646 | * and copy the framebuffer to the existing image that we track. The state of the stream and configuration | ||||
647 | * are later handled by the corresponding listener. | ||||
648 | */ | ||||
649 | void PWFrameBuffer::Private::createReceivingStream() | ||||
650 | { | ||||
651 | spa_rectangle pwMinScreenBounds = SPA_RECTANGLE(1, 1); | ||||
652 | spa_rectangle pwMaxScreenBounds = SPA_RECTANGLE(screenGeometry.width, screenGeometry.height); | ||||
653 | | ||||
654 | spa_fraction pwFramerateMin = SPA_FRACTION(0, 1); | ||||
655 | spa_fraction pwFramerateMax = SPA_FRACTION(60, 1); | ||||
656 | | ||||
657 | auto reuseProps = pw_properties_new("pipewire.client.reuse", "1", nullptr); // null marks end of varargs | ||||
658 | pwStream = pw_stream_new(pwRemote, "krfb-fb-consume-stream", reuseProps); | ||||
659 | | ||||
660 | uint8_t buffer[1024] = {}; | ||||
661 | const spa_pod *params[1]; | ||||
662 | auto builder = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer)); | ||||
663 | | ||||
664 | #if PW_CHECK_VERSION(0, 2, 9) | ||||
665 | params[0] = reinterpret_cast<spa_pod *>(spa_pod_builder_add_object(&builder, | ||||
666 | SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat, | ||||
667 | ":", SPA_FORMAT_mediaType, "I", SPA_MEDIA_TYPE_video, | ||||
668 | ":", SPA_FORMAT_mediaSubtype, "I", SPA_MEDIA_SUBTYPE_raw, | ||||
669 | ":", SPA_FORMAT_VIDEO_format, "I", SPA_VIDEO_FORMAT_RGBx, | ||||
670 | ":", SPA_FORMAT_VIDEO_size, "?rR", SPA_CHOICE_RANGE(&pwMaxScreenBounds, &pwMinScreenBounds, &pwMaxScreenBounds), | ||||
671 | ":", SPA_FORMAT_VIDEO_framerate, "F", &pwFramerateMin, | ||||
672 | ":", SPA_FORMAT_VIDEO_maxFramerate, "?rF", SPA_CHOICE_RANGE(&pwFramerateMax, &pwFramerateMin, &pwFramerateMax))); | ||||
673 | #else | ||||
674 | params[0] = reinterpret_cast<spa_pod *>(spa_pod_builder_object(&builder, | ||||
675 | pwCoreType->param.idEnumFormat, pwCoreType->spa_format, | ||||
676 | "I", pwType->media_type.video, | ||||
677 | "I", pwType->media_subtype.raw, | ||||
678 | ":", pwType->format_video.format, "I", pwType->video_format.RGBx, | ||||
679 | ":", pwType->format_video.size, "Rru", &pwMaxScreenBounds, SPA_POD_PROP_MIN_MAX(&pwMinScreenBounds, &pwMaxScreenBounds), | ||||
680 | ":", pwType->format_video.framerate, "F", &pwFramerateMin, | ||||
681 | ":", pwType->format_video.max_framerate, "Fru", &pwFramerateMax, 2, &pwFramerateMin, &pwFramerateMax)); | ||||
682 | #endif | ||||
683 | | ||||
684 | pw_stream_add_listener(pwStream, &streamListener, &pwStreamEvents, this); | ||||
685 | auto flags = static_cast<pw_stream_flags>(PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_INACTIVE | PW_STREAM_FLAG_MAP_BUFFERS); | ||||
686 | #if PW_CHECK_VERSION(0, 2, 9) | ||||
687 | if (pw_stream_connect(pwStream, PW_DIRECTION_INPUT, pwStreamNodeId, flags, params, 1) != 0) { | ||||
688 | #else | ||||
689 | if (pw_stream_connect(pwStream, PW_DIRECTION_INPUT, nullptr, flags, params, 1) != 0) { | ||||
690 | #endif | ||||
691 | qWarning() << "Could not connect receiving stream"; | ||||
692 | isValid = false; | ||||
693 | } | ||||
694 | } | ||||
695 | | ||||
696 | PWFrameBuffer::Private::~Private() | ||||
697 | { | ||||
698 | if (pwMainLoop) { | ||||
699 | pw_thread_loop_stop(pwMainLoop); | ||||
700 | } | ||||
701 | | ||||
702 | #if !PW_CHECK_VERSION(0, 2, 9) | ||||
703 | if (pwType) { | ||||
704 | delete pwType; | ||||
705 | } | ||||
706 | #endif | ||||
707 | | ||||
708 | if (pwStream) { | ||||
709 | pw_stream_destroy(pwStream); | ||||
710 | } | ||||
711 | | ||||
712 | if (pwRemote) { | ||||
713 | pw_remote_destroy(pwRemote); | ||||
714 | } | ||||
715 | | ||||
716 | if (pwCore) | ||||
717 | pw_core_destroy(pwCore); | ||||
718 | | ||||
719 | if (pwMainLoop) { | ||||
720 | pw_thread_loop_destroy(pwMainLoop); | ||||
721 | } | ||||
722 | | ||||
723 | if (pwLoop) { | ||||
724 | pw_loop_destroy(pwLoop); | ||||
725 | } | ||||
726 | } | ||||
727 | | ||||
728 | PWFrameBuffer::PWFrameBuffer(WId winid, QObject *parent) | ||||
729 | : FrameBuffer (winid, parent), | ||||
730 | d(new Private(this)) | ||||
731 | { | ||||
732 | // D-Bus is most important in init chain, no toys for us if something is wrong with XDP | ||||
733 | // PipeWire connectivity is initialized after D-Bus session is started | ||||
734 | d->initDbus(); | ||||
735 | | ||||
736 | // FIXME: for now use some initial size, later on we will reallocate this with the actual size we get from portal | ||||
737 | d->screenGeometry.width = 800; | ||||
738 | d->screenGeometry.height = 600; | ||||
739 | fb = nullptr; | ||||
740 | } | ||||
741 | | ||||
742 | PWFrameBuffer::~PWFrameBuffer() | ||||
743 | { | ||||
744 | free(fb); | ||||
745 | fb = nullptr; | ||||
746 | } | ||||
747 | | ||||
748 | int PWFrameBuffer::depth() | ||||
749 | { | ||||
750 | return 32; | ||||
751 | } | ||||
752 | | ||||
753 | int PWFrameBuffer::height() | ||||
754 | { | ||||
755 | return static_cast<qint32>(d->screenGeometry.height); | ||||
756 | } | ||||
757 | | ||||
758 | int PWFrameBuffer::width() | ||||
759 | { | ||||
760 | return static_cast<qint32>(d->screenGeometry.width); | ||||
761 | } | ||||
762 | | ||||
763 | int PWFrameBuffer::paddedWidth() | ||||
764 | { | ||||
765 | return width() * 4; | ||||
766 | } | ||||
767 | | ||||
768 | void PWFrameBuffer::getServerFormat(rfbPixelFormat &format) | ||||
769 | { | ||||
770 | format.bitsPerPixel = 32; | ||||
771 | format.depth = 32; | ||||
772 | format.trueColour = true; | ||||
773 | format.bigEndian = false; | ||||
774 | } | ||||
775 | | ||||
776 | void PWFrameBuffer::startMonitor() | ||||
777 | { | ||||
778 | | ||||
779 | } | ||||
780 | | ||||
781 | void PWFrameBuffer::stopMonitor() | ||||
782 | { | ||||
783 | | ||||
784 | } | ||||
785 | | ||||
786 | QVariant PWFrameBuffer::customProperty(const QString &property) const | ||||
787 | { | ||||
788 | if (property == QLatin1String("stream_node_id")) { | ||||
789 | return QVariant::fromValue<uint>(d->pwStreamNodeId); | ||||
790 | } if (property == QLatin1String("session_handle")) { | ||||
791 | return QVariant::fromValue<QDBusObjectPath>(d->sessionPath); | ||||
792 | } | ||||
793 | | ||||
794 | return FrameBuffer::customProperty(property); | ||||
795 | } | ||||
796 | | ||||
797 | bool PWFrameBuffer::isValid() const | ||||
798 | { | ||||
799 | return d->isValid; | ||||
800 | } |