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 | | ||||
4 | This program is free software; you can redistribute it and/or | ||||
5 | modify it under the terms of the GNU General Public | ||||
6 | License as published by the Free Software Foundation; either | ||||
7 | version 3 of the License, or (at your option) any later version. | ||||
8 | */ | ||||
9 | | ||||
10 | // system | ||||
11 | #include <sys/mman.h> | ||||
12 | #include <cstring> | ||||
13 | // Qt | ||||
14 | #include <QX11Info> | ||||
15 | #include <QCoreApplication> | ||||
16 | #include <QGuiApplication> | ||||
17 | #include <QScreen> | ||||
18 | #include <QSocketNotifier> | ||||
19 | #include <QDebug> | ||||
20 | #include <KRandom> | ||||
21 | // pipewire | ||||
22 | #include <pipewire/pipewire.h> | ||||
23 | #include <pipewire/global.h> | ||||
24 | #include <spa/param/video/format-utils.h> | ||||
25 | #include <spa/param/format-utils.h> | ||||
26 | #include <spa/param/props.h> | ||||
27 | #include <spa/lib/debug.h> | ||||
28 | #include <limits.h> | ||||
29 | | ||||
30 | #include "pw_framebuffer.h" | ||||
31 | #include "xdp_dbus_interface.h" | ||||
32 | | ||||
33 | static const uint MIN_SUPPORTED_XDP_KDE_SC_VERSION = 1; | ||||
34 | static const QDBusObjectPath KRFB_TOKEN(QLatin1String("/krfb-pw-plugin")); | ||||
35 | | ||||
36 | /** | ||||
37 | * @brief The PwType class - helper class to contain pointers to raw C pipewire media mappings | ||||
38 | */ | ||||
39 | class PwType { | ||||
40 | public: | ||||
41 | spa_type_media_type media_type; | ||||
42 | spa_type_media_subtype media_subtype; | ||||
43 | spa_type_format_video format_video; | ||||
44 | spa_type_video_format video_format; | ||||
45 | }; | ||||
46 | | ||||
47 | /** | ||||
48 | * @brief The PWFrameBuffer::Private class - private counterpart of PWFramebuffer class. This is the entity where | ||||
49 | * whole logic resides, for more info search for "d-pointer pattern" information. | ||||
50 | */ | ||||
51 | class PWFrameBuffer::Private { | ||||
52 | public: | ||||
53 | Private(PWFrameBuffer *q); | ||||
54 | ~Private(); | ||||
55 | | ||||
56 | private: | ||||
57 | friend class PWFrameBuffer; | ||||
58 | | ||||
59 | static void onStateChanged(void *data, pw_remote_state old, pw_remote_state state, const char *error); | ||||
60 | static void onStreamStateChanged(void *data, pw_stream_state old, pw_stream_state state, const char *error_message); | ||||
61 | static void onStreamFormatChanged(void *data, struct spa_pod *format); | ||||
62 | static void onNewBuffer(void *data, uint32_t id); | ||||
63 | | ||||
64 | void initWayland(); | ||||
65 | void initDbus(); | ||||
66 | void initPw(); | ||||
67 | void initializePwTypes(); | ||||
68 | | ||||
69 | // dbus handling | ||||
70 | void handleSessionCreated(quint32 &code, QVariantMap &results); | ||||
71 | void handleSourcesSelected(quint32 &code, QVariantMap &results); | ||||
72 | void handleScreencastStarted(quint32 &code, QVariantMap &results); | ||||
73 | | ||||
74 | // pw handling | ||||
75 | void processPwEvents(); | ||||
76 | void createReceivingStream(); | ||||
77 | void handleFrame(spa_buffer *buf); | ||||
78 | | ||||
79 | // link to public interface | ||||
80 | PWFrameBuffer *q; | ||||
81 | | ||||
82 | // pipewire stuff | ||||
83 | pw_core *pwCore = nullptr; | ||||
84 | pw_type *pwCoreType = nullptr; | ||||
85 | pw_remote *pwRemote = nullptr; | ||||
86 | pw_stream *pwStream = nullptr; | ||||
87 | pw_loop *pwLoop = nullptr; | ||||
88 | QScopedPointer<PwType> pwType; | ||||
89 | | ||||
90 | // event handlers | ||||
91 | pw_remote_events pwRemoteEvents = {}; | ||||
92 | pw_stream_events pwStreamEvents = {}; | ||||
93 | | ||||
94 | // wayland-like listeners | ||||
95 | // ...of events that happen in pipewire server | ||||
96 | spa_hook remoteListener = {}; | ||||
97 | // ...of events that happen with the stream we consume | ||||
98 | spa_hook streamListener = {}; | ||||
99 | | ||||
100 | // negotiated video format | ||||
101 | QScopedPointer<spa_video_info_raw> videoFormat; | ||||
102 | | ||||
103 | // listens on pipewire socket | ||||
104 | QScopedPointer<QSocketNotifier> socketNotifier; | ||||
105 | | ||||
106 | // requests a session from XDG Desktop Portal | ||||
107 | // auto-generated and compiled from xdp_dbus_interface.xml file | ||||
108 | QScopedPointer<OrgFreedesktopPortalScreenCastInterface> dbusXdpService; | ||||
109 | | ||||
110 | // XDP screencast session handle | ||||
111 | QDBusObjectPath sessionPath; | ||||
112 | // Pipewire file descriptor | ||||
113 | QDBusUnixFileDescriptor pipewireFd; | ||||
114 | | ||||
115 | // counters for dbus exchange | ||||
116 | quint32 requestCounter = 0; | ||||
117 | quint32 sessionCounter = 0; | ||||
118 | | ||||
119 | // screen geometry holder | ||||
120 | struct { | ||||
121 | quint32 width; | ||||
122 | quint32 height; | ||||
123 | } screenGeometry; | ||||
124 | | ||||
125 | // real image with allocated memory which poses as a destination when we get a buffer from pipewire | ||||
126 | // and as source when we pass the frame back to protocol | ||||
127 | QImage fbImage; | ||||
128 | | ||||
129 | // sanity indicator | ||||
130 | bool isValid = true; | ||||
131 | }; | ||||
132 | | ||||
133 | PWFrameBuffer::Private::Private(PWFrameBuffer *q) : q(q) | ||||
134 | { | ||||
135 | // initialize event handlers, remote end and stream-related | ||||
136 | pwRemoteEvents.version = PW_VERSION_REMOTE_EVENTS; | ||||
137 | pwRemoteEvents.state_changed = &onStateChanged; | ||||
138 | | ||||
139 | pwStreamEvents.version = PW_VERSION_STREAM_EVENTS; | ||||
140 | pwStreamEvents.state_changed = &onStreamStateChanged; | ||||
141 | pwStreamEvents.format_changed = &onStreamFormatChanged; | ||||
142 | pwStreamEvents.new_buffer = &onNewBuffer; | ||||
143 | } | ||||
144 | | ||||
145 | /** | ||||
146 | * @brief PWFrameBuffer::Private::initWayland - initializes screen info and Wayland connectivity. | ||||
147 | * For now just grabs first available screen and uses its dimensions for framebuffer. | ||||
148 | */ | ||||
149 | void PWFrameBuffer::Private::initWayland() | ||||
150 | { | ||||
151 | qInfo() << "Initializing screen info"; | ||||
152 | auto screen = qApp->screens().at(0); | ||||
153 | auto screenSize = screen->geometry(); | ||||
154 | screenGeometry.width = static_cast<quint32>(screenSize.width()); | ||||
155 | screenGeometry.height = static_cast<quint32>(screenSize.height()); | ||||
156 | fbImage = QImage(screenSize.width(), screenSize.height(), QImage::Format_RGBX8888); | ||||
157 | } | ||||
158 | | ||||
159 | /** | ||||
160 | * @brief PWFrameBuffer::Private::initDbus - initialize D-Bus connectivity with XDG Desktop Portal. | ||||
161 | * Based on XDG_CURRENT_DESKTOP environment variable it will give us implementation that we need, | ||||
162 | * in case of KDE it is xdg-desktop-portal-kde binary. | ||||
163 | */ | ||||
164 | void PWFrameBuffer::Private::initDbus() | ||||
165 | { | ||||
166 | qInfo() << "Initializing D-Bus connectivity with XDG Desktop Portal"; | ||||
167 | dbusXdpService.reset(new OrgFreedesktopPortalScreenCastInterface(QLatin1String("org.freedesktop.portal.Desktop"), | ||||
168 | QLatin1String("/org/freedesktop/portal/desktop"), | ||||
169 | QDBusConnection::sessionBus())); | ||||
170 | if (!dbusXdpService->isValid()) { | ||||
171 | qWarning("Can't find XDG Portal screencast interface"); | ||||
172 | isValid = false; | ||||
173 | return; | ||||
174 | } | ||||
175 | | ||||
176 | auto version = dbusXdpService->version(); | ||||
177 | if (version < MIN_SUPPORTED_XDP_KDE_SC_VERSION) { | ||||
178 | qWarning() << "Unsupported XDG Portal screencast interface version:" << version; | ||||
179 | isValid = false; | ||||
180 | return; | ||||
181 | } | ||||
182 | | ||||
183 | // create session | ||||
184 | auto sessionParameters = QVariantMap { | ||||
185 | { QLatin1String("session_handle_token"), QString::number(sessionCounter++) }, | ||||
186 | { QLatin1String("handle_token"), QString::number(requestCounter++) } | ||||
187 | }; | ||||
188 | auto sessionReply = dbusXdpService->CreateSession(sessionParameters); | ||||
189 | sessionReply.waitForFinished(); | ||||
190 | if (!sessionReply.isValid()) { | ||||
191 | qWarning("Couldn't initialize XDP-KDE screencast session"); | ||||
192 | isValid = false; | ||||
193 | return; | ||||
194 | } | ||||
195 | | ||||
196 | qInfo() << "DBus session created: " << sessionReply.value().path(); | ||||
197 | QDBusConnection::sessionBus().connect(QString(), | ||||
198 | sessionReply.value().path(), | ||||
199 | QLatin1String("org.freedesktop.portal.Request"), | ||||
200 | QLatin1String("Response"), | ||||
201 | this->q, | ||||
202 | SLOT(handleXdpSessionCreated(uint, QVariantMap))); | ||||
203 | } | ||||
204 | | ||||
205 | void PWFrameBuffer::handleXdpSessionCreated(quint32 code, QVariantMap results) | ||||
206 | { | ||||
207 | d->handleSessionCreated(code, results); | ||||
208 | } | ||||
209 | | ||||
210 | /** | ||||
211 | * @brief PWFrameBuffer::Private::handleSessionCreated - handle creation of ScreenCast session. | ||||
212 | * XDG Portal answers with session path if it was able to successfully create the screencast. | ||||
213 | * | ||||
214 | * @param code return code for dbus call. Zero is success, non-zero means error | ||||
215 | * @param results map with results of call. | ||||
216 | */ | ||||
217 | void PWFrameBuffer::Private::handleSessionCreated(quint32 &code, QVariantMap &results) | ||||
218 | { | ||||
219 | if (code != 0) { | ||||
220 | qWarning() << "Failed to create session: " << code; | ||||
221 | isValid = false; | ||||
222 | return; | ||||
223 | } | ||||
224 | | ||||
225 | sessionPath = QDBusObjectPath(results.value(QLatin1String("session_handle")).toString()); | ||||
226 | | ||||
227 | // select sources for the session | ||||
228 | auto selectionOptions = QVariantMap { | ||||
229 | { QLatin1String("types"), 1u }, // only MONITOR is supported | ||||
230 | { QLatin1String("multiple"), false }, | ||||
231 | { QLatin1String("handle_token"), QString::number(requestCounter++) } | ||||
232 | }; | ||||
233 | auto selectorReply = dbusXdpService->SelectSources(sessionPath, selectionOptions); | ||||
234 | selectorReply.waitForFinished(); | ||||
235 | if (!selectorReply.isValid()) { | ||||
236 | qWarning() << "Couldn't select sources for the screen-casting session"; | ||||
237 | isValid = false; | ||||
238 | return; | ||||
239 | } | ||||
240 | QDBusConnection::sessionBus().connect(QString(), | ||||
241 | selectorReply.value().path(), | ||||
242 | QLatin1String("org.freedesktop.portal.Request"), | ||||
243 | QLatin1String("Response"), | ||||
244 | this->q, | ||||
245 | SLOT(handleXdpSourcesSelected(uint, QVariantMap))); | ||||
246 | } | ||||
247 | | ||||
248 | void PWFrameBuffer::handleXdpSourcesSelected(quint32 code, QVariantMap results) | ||||
249 | { | ||||
250 | d->handleSourcesSelected(code, results); | ||||
251 | } | ||||
252 | | ||||
253 | /** | ||||
254 | * @brief PWFrameBuffer::Private::handleSourcesSelected - handle Screencast sources selection. | ||||
255 | * XDG Portal shows a dialog at this point which allows you to select monitor from the list. | ||||
256 | * This function is called after you make a selection. | ||||
257 | * | ||||
258 | * @param code return code for dbus call. Zero is success, non-zero means error | ||||
259 | * @param results map with results of call. | ||||
260 | */ | ||||
261 | void PWFrameBuffer::Private::handleSourcesSelected(quint32 &code, QVariantMap &) | ||||
262 | { | ||||
263 | if (code != 0) { | ||||
264 | qWarning() << "Failed to select sources: " << code; | ||||
265 | isValid = false; | ||||
266 | return; | ||||
267 | } | ||||
268 | | ||||
269 | // start session | ||||
270 | auto startParameters = QVariantMap { | ||||
271 | { QLatin1String("handle_token"), QString::number(requestCounter++) } | ||||
272 | }; | ||||
273 | auto startReply = dbusXdpService->Start(sessionPath, QString(), startParameters); | ||||
274 | startReply.waitForFinished(); | ||||
275 | QDBusConnection::sessionBus().connect(QString(), | ||||
276 | startReply.value().path(), | ||||
277 | QLatin1String("org.freedesktop.portal.Request"), | ||||
278 | QLatin1String("Response"), | ||||
279 | this->q, | ||||
280 | SLOT(handleXdpScreenCastStarted(uint, QVariantMap))); | ||||
281 | } | ||||
282 | | ||||
283 | | ||||
284 | void PWFrameBuffer::handleXdpScreenCastStarted(quint32 code, QVariantMap results) | ||||
285 | { | ||||
286 | d->handleScreencastStarted(code, results); | ||||
287 | } | ||||
288 | | ||||
289 | /** | ||||
290 | * @brief PWFrameBuffer::Private::handleScreencastStarted - handle Screencast start. | ||||
291 | * At this point there shall be ready pipewire stream to consume. | ||||
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::handleScreencastStarted(quint32 &code, QVariantMap &results) | ||||
297 | { | ||||
298 | if (code != 0) { | ||||
299 | qWarning() << "Failed to start screencast: " << code; | ||||
300 | isValid = false; | ||||
301 | return; | ||||
302 | } | ||||
303 | | ||||
304 | // there should be only one stream | ||||
305 | auto streams = results.value("streams"); | ||||
306 | if (streams.isNull()) { | ||||
307 | // maybe we should check deeper with qdbus_cast but this suffices for now | ||||
308 | qWarning() << "Failed to get screencast streams"; | ||||
309 | isValid = false; | ||||
310 | return; | ||||
311 | } | ||||
312 | | ||||
313 | auto streamReply = dbusXdpService->OpenPipeWireRemote(sessionPath, QVariantMap()); | ||||
314 | streamReply.waitForFinished(); | ||||
315 | if (!streamReply.isValid()) { | ||||
316 | qWarning() << "Couldn't open pipewire remote for the screen-casting session"; | ||||
317 | isValid = false; | ||||
318 | return; | ||||
319 | } | ||||
320 | | ||||
321 | pipewireFd = streamReply.value(); | ||||
322 | if (!pipewireFd.isValid()) { | ||||
323 | qWarning() << "Couldn't get pipewire connection file descriptor"; | ||||
324 | isValid = false; | ||||
325 | return; | ||||
326 | } | ||||
327 | initPw(); | ||||
328 | } | ||||
329 | | ||||
330 | /** | ||||
331 | * @brief PWFrameBuffer::Private::initPw - initialize Pipewire socket connectivity. | ||||
332 | * pipewireFd should be pointing to existing file descriptor that was passed by D-Bus at this point. | ||||
333 | */ | ||||
334 | void PWFrameBuffer::Private::initPw() { | ||||
335 | qInfo() << "Initializing Pipewire connectivity"; | ||||
336 | | ||||
337 | // init pipewire (required) | ||||
338 | pw_init(nullptr, nullptr); // args are not used anyways | ||||
339 | | ||||
340 | // initialize our source | ||||
341 | pwLoop = pw_loop_new(nullptr); | ||||
342 | socketNotifier.reset(new QSocketNotifier(pw_loop_get_fd(pwLoop), QSocketNotifier::Read)); | ||||
343 | QObject::connect(socketNotifier.data(), &QSocketNotifier::activated, this->q, &PWFrameBuffer::processPwEvents); | ||||
344 | | ||||
345 | // create PipeWire core object (required) | ||||
346 | pwCore = pw_core_new(pwLoop, nullptr); | ||||
347 | pwCoreType = pw_core_get_type(pwCore); | ||||
348 | | ||||
349 | // pw_remote should be initialized before type maps or connection error will happen | ||||
350 | pwRemote = pw_remote_new(pwCore, nullptr, 0); | ||||
351 | | ||||
352 | // init type maps | ||||
353 | initializePwTypes(); | ||||
354 | | ||||
355 | // init PipeWire remote, add listener to handle events | ||||
356 | pw_remote_add_listener(pwRemote, &remoteListener, &pwRemoteEvents, this); | ||||
357 | pw_remote_connect_fd(pwRemote, pipewireFd.fileDescriptor()); | ||||
358 | } | ||||
359 | | ||||
360 | /** | ||||
361 | * @brief PWFrameBuffer::Private::initializePwTypes - helper method to initialize and map all needed | ||||
362 | * Pipewire types from core to type structure. | ||||
363 | */ | ||||
364 | void PWFrameBuffer::Private::initializePwTypes() | ||||
365 | { | ||||
366 | // raw C-like PipeWire type map | ||||
367 | auto map = pwCoreType->map; | ||||
368 | | ||||
369 | pwType.reset(new PwType); | ||||
370 | spa_type_media_type_map(map, &pwType->media_type); | ||||
371 | spa_type_media_subtype_map(map, &pwType->media_subtype); | ||||
372 | spa_type_format_video_map(map, &pwType->format_video); | ||||
373 | spa_type_video_format_map(map, &pwType->video_format); | ||||
374 | | ||||
375 | // must be called after type system is mapped | ||||
376 | // calling it before causes unpredictable memory corruption and errors | ||||
377 | spa_debug_set_type_map(pwCoreType->map); | ||||
378 | } | ||||
379 | | ||||
380 | /** | ||||
381 | * @brief PWFrameBuffer::Private::onStateChanged - global state tracking for pipewire connection | ||||
382 | * @param data pointer that you have set in pw_remote_add_listener call's last argument | ||||
383 | * @param state new state that connection has changed to | ||||
384 | * @param error optional error message, is set to non-null if state is error | ||||
385 | */ | ||||
386 | void PWFrameBuffer::Private::onStateChanged(void *data, pw_remote_state /*old*/, pw_remote_state state, const char *error) | ||||
387 | { | ||||
388 | qInfo() << "remote state: " << pw_remote_state_as_string(state); | ||||
389 | | ||||
390 | PWFrameBuffer::Private *d = static_cast<PWFrameBuffer::Private*>(data); | ||||
391 | | ||||
392 | switch (state) { | ||||
393 | case PW_REMOTE_STATE_ERROR: | ||||
394 | qWarning() << "remote error: " << error; | ||||
395 | break; | ||||
396 | case PW_REMOTE_STATE_CONNECTED: | ||||
397 | d->createReceivingStream(); | ||||
398 | break; | ||||
399 | default: | ||||
400 | qInfo() << "remote state: " << pw_remote_state_as_string(state); | ||||
401 | break; | ||||
402 | } | ||||
403 | } | ||||
404 | | ||||
405 | /** | ||||
406 | * @brief PWFrameBuffer::Private::onStreamStateChanged - called whenever stream state changes on pipewire server | ||||
407 | * @param data pointer that you have set in pw_stream_add_listener call's last argument | ||||
408 | * @param state new state that stream has changed to | ||||
409 | * @param error_message optional error message, is set to non-null if state is error | ||||
410 | */ | ||||
411 | void PWFrameBuffer::Private::onStreamStateChanged(void *data, pw_stream_state /*old*/, pw_stream_state state, const char *error_message) | ||||
412 | { | ||||
413 | qInfo() << "Stream state changed: " << pw_stream_state_as_string(state); | ||||
414 | | ||||
415 | auto *d = static_cast<PWFrameBuffer::Private *>(data); | ||||
416 | | ||||
417 | switch (state) { | ||||
418 | case PW_STREAM_STATE_ERROR: | ||||
419 | qWarning() << "pipewire stream error: " << error_message; | ||||
420 | break; | ||||
421 | case PW_STREAM_STATE_CONFIGURE: | ||||
422 | pw_stream_set_active(d->pwStream, true); | ||||
423 | break; | ||||
424 | default: | ||||
425 | break; | ||||
426 | } | ||||
427 | } | ||||
428 | | ||||
429 | /** | ||||
430 | * @brief PWFrameBuffer::Private::onStreamFormatChanged - being executed after stream is set to active | ||||
431 | * and after setup has been requested to connect to it. The actual video format is being negotiated here. | ||||
432 | * @param data pointer that you have set in pw_stream_add_listener call's last argument | ||||
433 | * @param format format that's being proposed | ||||
434 | */ | ||||
435 | void PWFrameBuffer::Private::onStreamFormatChanged(void *data, struct spa_pod *format) | ||||
436 | { | ||||
437 | qInfo() << "Stream format changed"; | ||||
438 | auto *d = static_cast<PWFrameBuffer::Private *>(data); | ||||
439 | | ||||
440 | const int bpp = 4; | ||||
441 | | ||||
442 | if (!format) { | ||||
443 | pw_stream_finish_format(d->pwStream, 0, nullptr, 0); | ||||
444 | return; | ||||
445 | } | ||||
446 | | ||||
447 | d->videoFormat.reset(new spa_video_info_raw); | ||||
448 | spa_format_video_raw_parse(format, d->videoFormat.data(), &d->pwType->format_video); | ||||
449 | | ||||
450 | auto width = d->videoFormat->size.width; | ||||
451 | auto height = d->videoFormat->size.height; | ||||
452 | auto stride = SPA_ROUND_UP_N(width * bpp, 4); | ||||
453 | auto size = height * stride; | ||||
454 | | ||||
455 | uint8_t buffer[1024]; | ||||
456 | auto builder = spa_pod_builder {buffer, sizeof(buffer)}; | ||||
457 | | ||||
458 | // setup buffers and meta header for new format | ||||
459 | struct spa_pod *params[2]; | ||||
460 | params[0] = reinterpret_cast<spa_pod *>(spa_pod_builder_object(&builder, | ||||
461 | d->pwCoreType->param.idBuffers, d->pwCoreType->param_buffers.Buffers, | ||||
462 | ":", d->pwCoreType->param_buffers.size, "i", size, | ||||
463 | ":", d->pwCoreType->param_buffers.stride, "i", stride, | ||||
464 | ":", d->pwCoreType->param_buffers.buffers, "iru", 8, SPA_POD_PROP_MIN_MAX(1, 32), | ||||
465 | ":", d->pwCoreType->param_buffers.align, "i", 16)); | ||||
466 | params[1] = reinterpret_cast<spa_pod *>(spa_pod_builder_object(&builder, | ||||
467 | d->pwCoreType->param.idMeta, d->pwCoreType->param_meta.Meta, | ||||
468 | ":", d->pwCoreType->param_meta.type, "I", d->pwCoreType->meta.Header, | ||||
469 | ":", d->pwCoreType->param_meta.size, "i", sizeof(struct spa_meta_header))); | ||||
470 | | ||||
471 | pw_stream_finish_format(d->pwStream, 0, params, 2); | ||||
472 | } | ||||
473 | | ||||
474 | /** | ||||
475 | * @brief PWFrameBuffer::Private::onNewBuffer - called when new buffer is available in pipewire stream | ||||
476 | * @param data pointer that you have set in pw_stream_add_listener call's last argument | ||||
477 | * @param id | ||||
478 | */ | ||||
479 | void PWFrameBuffer::Private::onNewBuffer(void *data, uint32_t id) | ||||
480 | { | ||||
481 | qDebug() << "New buffer received" << id; | ||||
482 | auto *d = static_cast<PWFrameBuffer::Private *>(data); | ||||
483 | | ||||
484 | auto buf = pw_stream_peek_buffer(d->pwStream, id); | ||||
485 | d->handleFrame(buf); | ||||
486 | | ||||
487 | pw_stream_recycle_buffer(d->pwStream, id); | ||||
488 | } | ||||
489 | | ||||
490 | void PWFrameBuffer::Private::handleFrame(spa_buffer *buf) | ||||
491 | { | ||||
492 | auto mapLength = buf->datas[0].maxsize + buf->datas[0].mapoffset; | ||||
493 | | ||||
494 | void *mapped; // full length of mapped data | ||||
495 | void *src; // real pixel data in this buffer | ||||
496 | if (buf->datas[0].type == pwCoreType->data.MemFd || buf->datas[0].type == pwCoreType->data.DmaBuf) { | ||||
497 | mapped = mmap(nullptr, mapLength, PROT_READ, MAP_PRIVATE, buf->datas[0].fd, 0); | ||||
498 | src = SPA_MEMBER(mapped, buf->datas[0].mapoffset, void); | ||||
499 | } else if (buf->datas[0].type == pwCoreType->data.MemPtr) { | ||||
500 | mapped = nullptr; | ||||
501 | src = buf->datas[0].data; | ||||
502 | } else { | ||||
503 | qWarning() << "Got unsupported buffer type" << buf->datas[0].type; | ||||
504 | return; | ||||
505 | } | ||||
506 | | ||||
507 | qint32 srcStride = buf->datas[0].chunk->stride; | ||||
508 | if (srcStride != q->paddedWidth()) { | ||||
509 | qWarning() << "Got buffer with stride different from screen stride" << srcStride << "!=" << q->paddedWidth(); | ||||
510 | return; | ||||
511 | } | ||||
512 | | ||||
513 | fbImage.bits(); | ||||
514 | q->tiles.append(fbImage.rect()); | ||||
515 | std::memcpy(fbImage.bits(), src, buf->datas[0].maxsize); | ||||
516 | | ||||
517 | if (mapped) | ||||
518 | munmap(mapped, mapLength); | ||||
519 | } | ||||
520 | | ||||
521 | /** | ||||
522 | * @brief PWFrameBuffer::Private::processPwEvents - called when Pipewire socket notifies there's | ||||
523 | * data to process by remote interface. | ||||
524 | */ | ||||
525 | void PWFrameBuffer::Private::processPwEvents() { | ||||
526 | qDebug() << "Iterating over pipewire loop..."; | ||||
527 | | ||||
528 | int result = pw_loop_iterate(pwLoop, 0); | ||||
529 | if (result < 0) { | ||||
530 | qWarning() << "Failed to iterate over pipewire loop: " << spa_strerror(result); | ||||
531 | } | ||||
532 | } | ||||
533 | | ||||
534 | /** | ||||
535 | * @brief PWFrameBuffer::Private::createReceivingStream - create a stream that will consume Pipewire buffers | ||||
536 | * and copy the framebuffer to the existing image that we track. The state of the stream and configuration | ||||
537 | * are later handled by the corresponding listener. | ||||
538 | */ | ||||
539 | void PWFrameBuffer::Private::createReceivingStream() | ||||
540 | { | ||||
541 | auto pwScreenBounds = spa_rectangle {screenGeometry.width, screenGeometry.height}; | ||||
542 | | ||||
543 | auto pwFramerate = spa_fraction {25, 1}; | ||||
544 | auto pwFramerateMin = spa_fraction {0, 1}; | ||||
545 | auto pwFramerateMax = spa_fraction {60, 1}; | ||||
546 | | ||||
547 | auto reuseProps = pw_properties_new("pipewire.client.reuse", "1", nullptr); // null marks end of varargs | ||||
548 | pwStream = pw_stream_new(pwRemote, "krfb-fb-consume-stream", reuseProps); | ||||
549 | | ||||
550 | uint8_t buffer[1024] = {}; | ||||
551 | const spa_pod *params[1]; | ||||
552 | auto builder = spa_pod_builder{buffer, sizeof(buffer)}; | ||||
553 | params[0] = reinterpret_cast<spa_pod *>(spa_pod_builder_object(&builder, | ||||
554 | pwCoreType->param.idEnumFormat, pwCoreType->spa_format, | ||||
555 | "I", pwType->media_type.video, | ||||
556 | "I", pwType->media_subtype.raw, | ||||
557 | ":", pwType->format_video.format, "I", pwType->video_format.RGBx, | ||||
558 | ":", pwType->format_video.size, "R", &pwScreenBounds, | ||||
559 | ":", pwType->format_video.framerate, "F", &pwFramerateMin, | ||||
560 | ":", pwType->format_video.max_framerate, "Fr", &pwFramerate, 2, &pwFramerateMin, &pwFramerateMax)); | ||||
561 | spa_debug_pod(params[0], SPA_DEBUG_FLAG_FORMAT); | ||||
562 | | ||||
563 | pw_stream_add_listener(pwStream, &streamListener, &pwStreamEvents, this); | ||||
564 | auto flags = static_cast<pw_stream_flags>(PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_INACTIVE); | ||||
565 | if (pw_stream_connect(pwStream, PW_DIRECTION_INPUT, nullptr, flags, params, 1) != 0) { | ||||
566 | qWarning() << "Could not connect receiving stream"; | ||||
567 | isValid = false; | ||||
568 | } | ||||
569 | } | ||||
570 | | ||||
571 | PWFrameBuffer::Private::~Private() | ||||
572 | { | ||||
573 | if (pwStream) { | ||||
574 | pw_stream_disconnect(pwStream); | ||||
575 | pw_stream_destroy(pwStream); | ||||
576 | } | ||||
577 | if (pwRemote) { | ||||
578 | pw_remote_disconnect(pwRemote); | ||||
579 | } | ||||
580 | | ||||
581 | if (pwCore) | ||||
582 | pw_core_destroy(pwCore); | ||||
583 | | ||||
584 | if (pwLoop) { | ||||
585 | pw_loop_leave(pwLoop); | ||||
586 | pw_loop_destroy(pwLoop); | ||||
587 | } | ||||
588 | } | ||||
589 | | ||||
590 | PWFrameBuffer::PWFrameBuffer(WId winid, QObject *parent) | ||||
591 | : FrameBuffer (winid, parent), | ||||
592 | d(new Private(this)) | ||||
593 | { | ||||
594 | // D-Bus is most important in init chain, no toys for us if something is wrong with XDP | ||||
595 | // PipeWire connectivity is initialized after D-Bus session is started | ||||
596 | d->initDbus(); | ||||
597 | | ||||
598 | // connect to Wayland and PipeWire sockets | ||||
599 | d->initWayland(); | ||||
600 | | ||||
601 | // framebuffer from public interface will point directly to image data | ||||
602 | fb = reinterpret_cast<char *>(d->fbImage.bits()); | ||||
603 | } | ||||
604 | | ||||
605 | PWFrameBuffer::~PWFrameBuffer() | ||||
606 | { | ||||
607 | fb = nullptr; | ||||
608 | } | ||||
609 | | ||||
610 | void PWFrameBuffer::processPwEvents() | ||||
611 | { | ||||
612 | d->processPwEvents(); | ||||
613 | } | ||||
614 | | ||||
615 | int PWFrameBuffer::depth() | ||||
616 | { | ||||
617 | return 32; | ||||
618 | } | ||||
619 | | ||||
620 | int PWFrameBuffer::height() | ||||
621 | { | ||||
622 | return static_cast<qint32>(d->screenGeometry.height); | ||||
623 | } | ||||
624 | | ||||
625 | int PWFrameBuffer::width() | ||||
626 | { | ||||
627 | return static_cast<qint32>(d->screenGeometry.width); | ||||
628 | } | ||||
629 | | ||||
630 | void PWFrameBuffer::getServerFormat(rfbPixelFormat &format) | ||||
631 | { | ||||
632 | format.bitsPerPixel = 32; | ||||
633 | format.depth = 32; | ||||
634 | format.trueColour = true; | ||||
635 | format.bigEndian = false; | ||||
636 | } | ||||
637 | | ||||
638 | void PWFrameBuffer::startMonitor() | ||||
639 | { | ||||
640 | | ||||
641 | } | ||||
642 | | ||||
643 | void PWFrameBuffer::stopMonitor() | ||||
644 | { | ||||
645 | | ||||
646 | } | ||||
647 | | ||||
648 | bool PWFrameBuffer::isValid() const | ||||
649 | { | ||||
650 | return d->isValid; | ||||
651 | } |