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
qpipewire_screencapturehelper.cpp
Go to the documentation of this file.
1// Copyright (C) 2024 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
5
6#include <QtMultimedia/private/qcapturablewindow_p.h>
7#include <QtMultimedia/private/qmemoryvideobuffer_p.h>
8#include <QtMultimedia/private/qpipewire_instance_p.h>
9#include <QtMultimedia/private/qvideoframe_p.h>
10#include <QtMultimedia/private/qvideoframeconversionhelper_p.h>
11#include <QtMultimedia/qabstractvideobuffer.h>
12#include <QtGui/private/qdesktopunixservices_p.h>
13#include <QtGui/private/qguiapplication_p.h>
14#include <QtGui/qguiapplication.h>
15#include <QtGui/qpa/qplatformintegration.h>
16#include <QtGui/qscreen.h>
17#include <QtGui/qwindow.h>
18#include <QtCore/private/qcore_unix_p.h>
19#include <QtCore/qdebug.h>
20#include <QtCore/qfileinfo.h>
21#include <QtCore/qloggingcategory.h>
22#include <QtCore/qmutex.h>
23#include <QtCore/qrandom.h>
24#include <QtCore/qurlquery.h>
25#include <QtCore/quuid.h>
26#include <QtCore/qvariantmap.h>
27#include <QtDBus/qdbusconnection.h>
28#include <QtDBus/qdbusinterface.h>
29#include <QtDBus/qdbusmessage.h>
30#include <QtDBus/qdbuspendingcall.h>
31#include <QtDBus/qdbuspendingreply.h>
32#include <QtDBus/qdbusreply.h>
33#include <QtDBus/qdbusunixfiledescriptor.h>
34
35#include <fcntl.h>
36
37// pipewire's macros tend to emit unused value warnings
38QT_WARNING_PUSH
39QT_WARNING_DISABLE_CLANG("-Wunused-value")
40
41QT_BEGIN_NAMESPACE
42
43using namespace Qt::StringLiterals;
44
45Q_STATIC_LOGGING_CATEGORY(qLcPipeWireCapture, "qt.multimedia.pipewire.capture");
46Q_STATIC_LOGGING_CATEGORY(qLcPipeWireCaptureMore, "qt.multimedia.pipewire.capture.more");
47
48namespace QtPipeWire {
49
51{
53 QDBusConnection bus = QDBusConnection::sessionBus();
54 auto *interface = new QDBusInterface(u"org.freedesktop.portal.Desktop"_s,
55 u"/org/freedesktop/portal/desktop"_s,
56 u"org.freedesktop.DBus.Properties"_s, bus, qGuiApp);
57
58 QList<QVariant> args;
59 args << u"org.freedesktop.portal.ScreenCast"_s << u"version"_s;
60
61 QDBusMessage reply = interface->callWithArgumentList(QDBus::Block, u"Get"_s, args);
62 qCDebug(qLcPipeWireCapture) << "v1=" << reply.type()
63 << "v2=" << reply.arguments().size()
64 << "v3=" << reply.arguments().at(0).toUInt();
65 if (reply.type() == QDBusMessage::ReplyMessage
66 && reply.arguments().size() == 1
67 // && reply.arguments().at(0).toUInt() >= 2
68 ) {
70 }
71 qCDebug(qLcPipeWireCapture) << Q_FUNC_INFO << "hasScreenCastPortal=" << hasScreenCastPortal;
72 }
73
74 bool hasScreenCastPortal = false;
75};
76
77Q_GLOBAL_STATIC(PipeWireCaptureGlobalState, globalState)
78
79
87
92
94{
95 return std::move(m_currentFrame);
96}
97
99{
100 QSurfaceCaptureGrabber::initializeGrabbingContext();
101 if (m_state == NoState)
102 createInterface();
103}
104
106{
107 if (m_state != NoState)
108 destroy();
109 QSurfaceCaptureGrabber::finalizeGrabbingContext();
110}
111
113{
114 return m_videoFrameFormat;
115}
116
118{
119 if (!globalState)
120 return false;
121
122 return globalState->hasScreenCastPortal;
123}
124
125void QPipeWireCaptureHelper::gotRequestResponse(uint result, const QVariantMap &map)
126{
127 Q_UNUSED(map);
128 qCDebug(qLcPipeWireCapture) << Q_FUNC_INFO << "result=" << result << "map=" << map;
129 if (result != 0) {
130 m_operationState = NoOperation;
131 qWarning() << "Failed to capture screen via pipewire, perhaps because user cancelled the operation.";
132 m_requestToken = -1;
133 return;
134 }
135
136 switch (m_operationState) {
137 case CreateSession:
138 selectSources(map[u"session_handle"_s].toString());
139 break;
140 case SelectSources:
141 startStream();
142 break;
143 case StartStream:
144 updateStreams(map[u"streams"_s].value<QDBusArgument>());
145 openPipeWireRemote();
146 m_operationState = NoOperation;
147 m_state = Streaming;
148 break;
149 case OpenPipeWireRemote:
150 m_operationState = NoOperation;
151 break;
152 default:
153 break;
154 }
155}
156
158{
159 return QRandomGenerator::global()->bounded(1, 25600);
160}
161
162QString QPipeWireCaptureHelper::getRequestToken()
163{
164 if (m_requestToken <= 0)
165 m_requestToken = generateRequestToken();
166 return u"u%1%2"_s.arg(m_requestTokenPrefix).arg(m_requestToken);
167}
168
169void QPipeWireCaptureHelper::createInterface()
170{
171 if (!globalState)
172 return;
173 if (!globalState->hasScreenCastPortal)
174 return;
175
176 m_operationState = NoOperation;
177
178 if (!m_screenCastInterface) {
179 m_screenCastInterface = std::make_unique<QDBusInterface>(
180 u"org.freedesktop.portal.Desktop"_s, u"/org/freedesktop/portal/desktop"_s,
181 u"org.freedesktop.portal.ScreenCast"_s, QDBusConnection::sessionBus());
182 bool ok = m_screenCastInterface->connection().connect(
183 u"org.freedesktop.portal.Desktop"_s, u""_s, u"org.freedesktop.portal.Request"_s,
184 u"Response"_s, this, SLOT(gotRequestResponse(uint,QVariantMap)));
185
186 if (!ok) {
187 updateError(
188 QPlatformSurfaceCapture::Error::InternalError,
189 u"Failed to connect to org.freedesktop.portal.ScreenCast dbus interface."_s);
190 return;
191 }
192 }
193 createSession();
194}
195
196void QPipeWireCaptureHelper::createSession()
197{
198 if (!m_screenCastInterface)
199 return;
200
201 QVariantMap options{
202 //{u"handle_token"_s , getRequestToken()},
203 { u"session_handle_token"_s, getRequestToken() },
204 };
205 QDBusMessage reply = m_screenCastInterface->call(u"CreateSession"_s, options);
206 if (!reply.errorMessage().isEmpty()) {
207 updateError(QPlatformSurfaceCapture::Error::InternalError,
208 u"Failed to create session for org.freedesktop.portal.ScreenCast. Error: "_s
209 + reply.errorName() + u": "_s + reply.errorMessage());
210 return;
211 }
212
213 m_operationState = CreateSession;
214}
215
216void QPipeWireCaptureHelper::selectSources(const QString &sessionHandle)
217{
218 if (!m_screenCastInterface)
219 return;
220
221 m_sessionHandle = sessionHandle;
222 QVariantMap options{
223 { u"handle_token"_s, getRequestToken() },
224 { u"types"_s, (uint)1 },
225 { u"multiple"_s, false },
226 { u"cursor_mode"_s, (uint)1 },
227 { u"persist_mode"_s, (uint)0 },
228 };
229 QDBusMessage reply = m_screenCastInterface->call(u"SelectSources"_s,
230 QDBusObjectPath(sessionHandle), options);
231 if (!reply.errorMessage().isEmpty()) {
232 updateError(QPlatformSurfaceCapture::Error::InternalError,
233 u"Failed to select sources for org.freedesktop.portal.ScreenCast. Error: "_s
234 + reply.errorName() + u": "_s + reply.errorMessage());
235 return;
236 }
237
238 m_operationState = SelectSources;
239}
240
241void QPipeWireCaptureHelper::startStream()
242{
243 if (!m_screenCastInterface)
244 return;
245
246 QVariantMap options{
247 { u"handle_token"_s, getRequestToken() },
248 };
249
250 auto *unixServices = dynamic_cast<QDesktopUnixServices *>(QGuiApplicationPrivate::platformIntegration()->services());
251 const QString parentWindow = QGuiApplication::focusWindow() && unixServices
252 ? unixServices->portalWindowIdentifier(QGuiApplication::focusWindow())
253 : QString();
254 QDBusMessage reply = m_screenCastInterface->call("Start"_L1, QDBusObjectPath(m_sessionHandle),
255 parentWindow, options);
256 if (!reply.errorMessage().isEmpty()) {
257 updateError(QPlatformSurfaceCapture::Error::InternalError,
258 u"Failed to start stream for org.freedesktop.portal.ScreenCast. Error: "_s
259 + reply.errorName() + u": "_s + reply.errorMessage());
260 return;
261 }
262
263 m_operationState = StartStream;
264}
265
266void QPipeWireCaptureHelper::updateStreams(const QDBusArgument &streamsInfo)
267{
268 m_streams.clear();
269
270 streamsInfo.beginStructure();
271 streamsInfo.beginArray();
272
273 while (!streamsInfo.atEnd()) {
274 quint32 nodeId = 0;
275 streamsInfo >> nodeId;
276 QMap<QString, QVariant> properties;
277 streamsInfo >> properties;
278
279 qint32 x = 0;
280 qint32 y = 0;
281 if (properties.contains(u"position"_s)) {
282 const auto position = properties[u"position"_s].value<QDBusArgument>();
283 position.beginStructure();
284 position >> x;
285 position >> y;
286 position.endStructure();
287 }
288
289 qint32 width = 0;
290 qint32 height = 0;
291 if (properties.contains(u"size"_s)) {
292 const auto size = properties[u"size"_s].value<QDBusArgument>();
293 size.beginStructure();
294 size >> width;
295 size >> height;
296 size.endStructure();
297 }
298
299 uint sourceType = 0;
300 if (properties.contains(u"source_type"_s))
301 sourceType = properties[u"source_type"_s].toUInt();
302
303 StreamInfo streamInfo;
304 streamInfo.nodeId = nodeId;
305 streamInfo.sourceType = sourceType;
306 streamInfo.rect = {x, y, width, height};
307 m_streams << streamInfo;
308 }
309
310 streamsInfo.endArray();
311 streamsInfo.endStructure();
312
313}
314
315void QPipeWireCaptureHelper::openPipeWireRemote()
316{
317 if (!m_screenCastInterface)
318 return;
319
320 QVariantMap options;
321 QDBusReply<QDBusUnixFileDescriptor> reply = m_screenCastInterface->call(
322 u"OpenPipeWireRemote"_s, QDBusObjectPath(m_sessionHandle), options);
323 if (!reply.isValid()) {
324 updateError(
325 QPlatformSurfaceCapture::Error::InternalError,
326 u"Failed to open pipewire remote for org.freedesktop.portal.ScreenCast. Error: name="_s
327 + reply.error().name() + u", message="_s + reply.error().message());
328 return;
329 }
330
331 m_pipewireFd = reply.value().fileDescriptor();
332 bool ok = open(m_pipewireFd);
333 qCDebug(qLcPipeWireCapture) << "open(" << m_pipewireFd << ") result=" << ok;
334 if (!ok) {
335 updateError(QPlatformSurfaceCapture::Error::InternalError,
336 u"Failed to open pipewire remote file descriptor"_s);
337 return;
338 }
339
340 m_operationState = OpenPipeWireRemote;
341}
342
343namespace {
344class LoopLocker
345{
346public:
347 LoopLocker(pw_thread_loop *threadLoop)
348 : m_threadLoop(threadLoop) {
349 lock();
350 }
351 ~LoopLocker() {
352 unlock();
353 }
354
355 void lock() {
356 if (m_threadLoop)
357 pw_thread_loop_lock(m_threadLoop);
358 }
359
360 void unlock() {
361 if (m_threadLoop) {
362 pw_thread_loop_unlock(m_threadLoop);
363 m_threadLoop = nullptr;
364 }
365 }
366
367private:
368 pw_thread_loop *m_threadLoop = nullptr;
369};
370} // namespace
371
372bool QPipeWireCaptureHelper::open(int pipewireFd)
373{
374 if (m_streams.isEmpty())
375 return false;
376
377 if (!globalState)
378 return false;
379
380 if (!m_instance)
381 m_instance = QPipeWireInstance::instance();
382
383 static const pw_core_events coreEvents = {
384 .version = PW_VERSION_CORE_EVENTS,
385 .info = [](void *data, const struct pw_core_info *info) {
386 Q_UNUSED(data)
387 Q_UNUSED(info)
388 },
389 .done = [](void *object, uint32_t id, int seq) {
390 reinterpret_cast<QPipeWireCaptureHelper *>(object)->onCoreEventDone(id, seq);
391 },
392 .ping = [](void *data, uint32_t id, int seq) {
393 Q_UNUSED(data)
394 Q_UNUSED(id)
395 Q_UNUSED(seq)
396 },
397 .error = [](void *data, uint32_t id, int seq, int res, const char *message) {
398 Q_UNUSED(data)
399 Q_UNUSED(id)
400 Q_UNUSED(seq)
401 Q_UNUSED(res)
402 Q_UNUSED(message)
403 },
404 .remove_id = [](void *data, uint32_t id) {
405 Q_UNUSED(data)
406 Q_UNUSED(id)
407 },
408 .bound_id = [](void *data, uint32_t id, uint32_t global_id) {
409 Q_UNUSED(data)
410 Q_UNUSED(id)
411 Q_UNUSED(global_id)
412 },
413 .add_mem = [](void *data, uint32_t id, uint32_t type, int fd, uint32_t flags) {
414 Q_UNUSED(data)
415 Q_UNUSED(id)
416 Q_UNUSED(type)
417 Q_UNUSED(fd)
418 Q_UNUSED(flags)
419 },
420 .remove_mem = [](void *data, uint32_t id) {
421 Q_UNUSED(data)
422 Q_UNUSED(id)
423 },
424#if defined(PW_CORE_EVENT_BOUND_PROPS)
425 .bound_props = [](void *data, uint32_t id, uint32_t global_id, const struct spa_dict *props) {
426 Q_UNUSED(data)
427 Q_UNUSED(id)
428 Q_UNUSED(global_id)
429 Q_UNUSED(props)
430 },
431#endif // PW_CORE_EVENT_BOUND_PROPS
432 };
433
434 static const pw_registry_events registryEvents = {
435 .version = PW_VERSION_REGISTRY_EVENTS,
436 .global = [](void *object, uint32_t id, uint32_t permissions, const char *type, uint32_t version, const spa_dict *props) {
437 reinterpret_cast<QPipeWireCaptureHelper *>(object)->onRegistryEventGlobal(id, permissions, type, version, props);
438 },
439 .global_remove = [](void *data, uint32_t id) {
440 Q_UNUSED(data)
441 Q_UNUSED(id)
442 },
443 };
444
445 m_threadLoop = PwThreadLoopHandle{
446 pw_thread_loop_new("qt-multimedia-pipewire-loop", nullptr),
447 };
448 if (!m_threadLoop) {
449 m_err = true;
450 updateError(QPlatformSurfaceCapture::Error::InternalError,
451 u"QPipeWireCaptureHelper failed at pw_thread_loop_new()."_s);
452 return false;
453 }
454
455 m_context = PwContextHandle{
456 pw_context_new(pw_thread_loop_get_loop(m_threadLoop.get()), nullptr, 0),
457 };
458 if (!m_context) {
459 m_err = true;
460 updateError(QPlatformSurfaceCapture::Error::InternalError,
461 u"QPipeWireCaptureHelper failed at pw_context_new()."_s);
462 return false;
463 }
464
465 m_core = PwCoreConnectionHandle{
466 pw_context_connect_fd(m_context.get(), fcntl(pipewireFd, F_DUPFD_CLOEXEC, 5), nullptr, 0),
467 };
468 if (!m_core) {
469 m_err = true;
470 updateError(QPlatformSurfaceCapture::Error::InternalError,
471 u"QPipeWireCaptureHelper failed at pw_context_connect_fd()."_s);
472 return false;
473 }
474
475 pw_core_add_listener(m_core.get(), &m_coreListener, &coreEvents, this);
476
477 m_registry = PwRegistryHandle{
478 pw_core_get_registry(m_core.get(), PW_VERSION_REGISTRY, 0),
479 };
480 if (!m_registry) {
481 m_err = true;
482 updateError(QPlatformSurfaceCapture::Error::InternalError,
483 u"QPipeWireCaptureHelper failed at pw_core_get_registry()."_s);
484 return false;
485 }
486 pw_registry_add_listener(m_registry.get(), &m_registryListener, &registryEvents, this);
487
488 updateCoreInitSeq();
489
490 if (pw_thread_loop_start(m_threadLoop.get()) != 0) {
491 m_err = true;
492 updateError(QPlatformSurfaceCapture::Error::InternalError,
493 u"QPipeWireCaptureHelper failed at pw_thread_loop_start()."_s);
494 return false;
495 }
496
497 LoopLocker locker(m_threadLoop.get());
498 while (!m_initDone) {
499 if (pw_thread_loop_timed_wait(m_threadLoop.get(), 2) != 0)
500 break;
501 }
502
503 return m_initDone && m_hasSource;
504}
505
506void QPipeWireCaptureHelper::updateCoreInitSeq()
507{
508 m_coreInitSeq = pw_core_sync(m_core.get(), PW_ID_CORE, m_coreInitSeq);
509}
510
511void QPipeWireCaptureHelper::onCoreEventDone(uint32_t id, int seq)
512{
513 if (id == PW_ID_CORE && seq == m_coreInitSeq) {
514 spa_hook_remove(&m_registryListener);
515 spa_hook_remove(&m_coreListener);
516
517 m_initDone = true;
518 pw_thread_loop_signal(m_threadLoop.get(), false);
519 }
520}
521
522void QPipeWireCaptureHelper::onRegistryEventGlobal(uint32_t id, uint32_t permissions, const char *type, uint32_t version, const spa_dict *props)
523{
524 Q_UNUSED(id)
525 Q_UNUSED(permissions)
526 Q_UNUSED(version)
527
528 if (qstrcmp(type, PW_TYPE_INTERFACE_Node) != 0)
529 return;
530
531 const auto* media_class = spa_dict_lookup(props, PW_KEY_MEDIA_CLASS);
532 if (!media_class)
533 return;
534
535 if (qstrcmp(media_class, "Stream/Output/Video") != 0
536 && qstrcmp(media_class, "Video/Source") != 0)
537 return;
538
539 m_hasSource = true;
540
541 updateCoreInitSeq();
542
543 recreateStream();
544}
545
546namespace {
547
548struct Rate {
549 qreal fps; // Used as stream frame rate
550 spa_fraction frac; // Used as pipewire frame rate
551};
552
553Rate rateFromFps(qreal fps)
554{
555 // NTSC rates get special handling if it seems like the user wants them
556 const std::initializer_list<Rate> ntscRates = {
557 { 23.976, SPA_FRACTION(24'000, 1'001) },
558 { 29.97, SPA_FRACTION(30'000, 1'001) },
559 { 59.94, SPA_FRACTION(60'000, 1'001) },
560 };
561 // NOTE: One could assume that a requested 60.f mapped to 60000/1001.
562 // TODO: There's also other rates (59.97, 49.99, etc..)
563
564 for (const Rate &rate : ntscRates) {
565 constexpr float precision = 0.01f;
566 if (qAbs(fps - rate.fps) < precision)
567 return rate;
568 }
569
570 // By default, round to a whole number fps. Rounding down is safest,
571 // but avoid 0/1, which isn't accepted as a maximum rate
572 quint32 roundedFps = qMax(qFloor(fps), 1);
573 return { qreal(roundedFps), SPA_FRACTION(roundedFps, 1) };
574}
575
576}
577
578void QPipeWireCaptureHelper::recreateStream()
579{
580 static const pw_stream_events streamEvents = {
581 .version = PW_VERSION_STREAM_EVENTS,
582 .destroy = [](void *data) {
583 Q_UNUSED(data)
584 },
585 .state_changed = [](void *data, pw_stream_state old, pw_stream_state state, const char *error) {
586 reinterpret_cast<QPipeWireCaptureHelper *>(data)->onStateChanged(old, state, error);
587 },
588 .control_info = [](void *data, uint32_t id, const struct pw_stream_control *control) {
589 Q_UNUSED(data)
590 Q_UNUSED(id)
591 Q_UNUSED(control)
592 },
593 .io_changed = [](void *data, uint32_t id, void *area, uint32_t size) {
594 Q_UNUSED(data)
595 Q_UNUSED(id)
596 Q_UNUSED(area)
597 Q_UNUSED(size)
598 },
599 .param_changed = [](void *data, uint32_t id, const struct spa_pod *param) {
600 reinterpret_cast<QPipeWireCaptureHelper *>(data)->onParamChanged(id, param);
601 },
602 .add_buffer = [](void *data, struct pw_buffer *buffer) {
603 Q_UNUSED(data)
604 Q_UNUSED(buffer)
605 },
606 .remove_buffer = [](void *data, struct pw_buffer *buffer) {
607 Q_UNUSED(data)
608 Q_UNUSED(buffer)
609 },
610 .process = [](void *data) {
611 reinterpret_cast<QPipeWireCaptureHelper *>(data)->onProcess();
612 },
613 .drained = [](void *data) {
614 Q_UNUSED(data)
615 },
616#if PW_VERSION_STREAM_EVENTS >= 1
617 .command = [](void *data, const struct spa_command *command) {
618 Q_UNUSED(data)
619 Q_UNUSED(command)
620 },
621#endif
622#if PW_VERSION_STREAM_EVENTS >= 2
623 .trigger_done = [](void *data) {
624 Q_UNUSED(data)
625 },
626#endif
627 };
628
629 destroyStream(true);
630
631 auto streamInfo = m_streams[0];
632 struct std::array<spa_dict_item, 4> items;
633 struct spa_dict info;
634 items[0] = SPA_DICT_ITEM_INIT(PW_KEY_MEDIA_TYPE, "Video");
635 items[1] = SPA_DICT_ITEM_INIT(PW_KEY_MEDIA_CATEGORY, "Capture");
636 items[2] = SPA_DICT_ITEM_INIT(PW_KEY_MEDIA_ROLE, "Screen");
637 info = SPA_DICT_INIT(items.data(), 3);
638 auto *props = pw_properties_new_dict(&info);
639
640 LoopLocker locker(m_threadLoop.get());
641
642 m_stream = PwStreamHandle{
643 pw_stream_new(m_core.get(), "video-capture", props),
644 };
645 if (!m_stream) {
646 m_err = true;
647 locker.unlock();
648 updateError(QPlatformSurfaceCapture::Error::InternalError,
649 u"QPipeWireCaptureHelper failed at pw_stream_new()."_s);
650 return;
651 }
652
653 m_streamListener = {};
654 pw_stream_add_listener(m_stream.get(), &m_streamListener, &streamEvents, this);
655
656 QT_WARNING_PUSH
657 // QTBUG-129587: libpipewire=1.2.5 warning
658 QT_WARNING_DISABLE_GCC("-Wmissing-field-initializers")
659 QT_WARNING_DISABLE_CLANG("-Wmissing-field-initializers")
660
661 std::array<uint8_t, 4096> buffer;
662 struct spa_pod_builder builder = SPA_POD_BUILDER_INIT(buffer.data(), sizeof(buffer));
663 struct spa_rectangle defsize = SPA_RECTANGLE(quint32(streamInfo.rect.width()), quint32(streamInfo.rect.height()));
664 struct spa_rectangle maxsize = SPA_RECTANGLE(4096, 4096);
665 struct spa_rectangle minsize = SPA_RECTANGLE(1,1);
666
667 // Considering the framerate as always variable rate, but with our target set as maximum.
668 struct spa_fraction maxrate = SPA_FRACTION(1000, 1);
669 struct spa_fraction minrate = SPA_FRACTION(0, 1);
670 auto rate = rateFromFps(frameRate());
671
672 std::array<const struct spa_pod *, 1> params{ static_cast<const spa_pod*>(spa_pod_builder_add_object(
673 &builder,
674 SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat,
675 SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_video),
676 SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw),
677 SPA_FORMAT_VIDEO_format, SPA_POD_CHOICE_ENUM_Id(6,
678 SPA_VIDEO_FORMAT_RGB,
679 SPA_VIDEO_FORMAT_BGR,
680 SPA_VIDEO_FORMAT_RGBA,
681 SPA_VIDEO_FORMAT_BGRA,
682 SPA_VIDEO_FORMAT_RGBx,
683 SPA_VIDEO_FORMAT_BGRx),
684 SPA_FORMAT_VIDEO_size, SPA_POD_CHOICE_RANGE_Rectangle(
685 &defsize, &minsize, &maxsize),
686 SPA_FORMAT_VIDEO_framerate, SPA_POD_CHOICE_RANGE_Fraction(
687 &rate.frac, &minrate, &maxrate),
688 SPA_FORMAT_VIDEO_maxFramerate, SPA_POD_Fraction(&rate.frac))
689 )};
690 QT_WARNING_POP
691
692 const int connectErr = pw_stream_connect(
693 m_stream.get(), PW_DIRECTION_INPUT, streamInfo.nodeId,
694 static_cast<pw_stream_flags>(PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS),
695 params.data(), 1);
696 if (connectErr != 0) {
697 m_err = true;
698 locker.unlock();
699 updateError(QPlatformSurfaceCapture::Error::InternalError,
700 u"QPipeWireCaptureHelper failed at pw_stream_connect()."_s);
701 return;
702 }
703}
704void QPipeWireCaptureHelper::destroyStream(bool forceDrain)
705{
706 if (!m_stream)
707 return;
708
709 if (forceDrain) {
710 LoopLocker locker(m_threadLoop.get());
711 while (!m_streamPaused && !m_silence && !m_err) {
712 if (pw_thread_loop_timed_wait(m_threadLoop.get(), 1) != 0)
713 break;
714 }
715 }
716
717 LoopLocker locker(m_threadLoop.get());
718 m_ignoreStateChange = true;
719 pw_stream_disconnect(m_stream.get());
720 m_stream = {};
721 m_ignoreStateChange = false;
722
723 m_stream = nullptr;
724 m_requestToken = -1;
725}
726
727void QPipeWireCaptureHelper::signalLoop(bool onProcessDone, bool err)
728{
729 if (err)
730 m_err = true;
731 if (onProcessDone)
732 m_processed = true;
733 pw_thread_loop_signal(m_threadLoop.get(), false);
734}
735
736void QPipeWireCaptureHelper::onStateChanged(pw_stream_state old, pw_stream_state state, const char *error)
737{
738 Q_UNUSED(old)
739 Q_UNUSED(error)
740
741 if (m_ignoreStateChange)
742 return;
743
744 switch (state)
745 {
746 case PW_STREAM_STATE_UNCONNECTED:
747 signalLoop(false, true);
748 break;
749 case PW_STREAM_STATE_PAUSED:
750 m_streamPaused = true;
751 signalLoop(false, false);
752 break;
753 case PW_STREAM_STATE_STREAMING:
754 m_streamPaused = false;
755 signalLoop(false, false);
756 break;
757 default:
758 break;
759 }
760}
761void QPipeWireCaptureHelper::onProcess()
762{
763 struct pw_buffer *pwBuffer = pw_stream_dequeue_buffer(m_stream.get());
764 if (!pwBuffer) {
765 updateError(QPlatformSurfaceCapture::Error::InternalError,
766 u"Out of buffers in pipewire stream dequeue."_s);
767 return;
768 }
769
770 struct spa_buffer *buf = pwBuffer->buffer;
771 void *sdata = buf->datas[0].data;
772 if (!sdata)
773 return;
774
775 int sstride = buf->datas[0].chunk->stride;
776 if (sstride == 0)
777 sstride = int(buf->datas[0].chunk->size / m_size.height());
778 qsizetype size = buf->datas[0].chunk->size;
779
780 if (m_videoFrameFormat.frameSize() != m_size || m_videoFrameFormat.pixelFormat() != m_pixelFormat)
781 m_videoFrameFormat = QVideoFrameFormat(m_size, m_pixelFormat);
782
783 m_currentFrame = QVideoFramePrivate::createFrame(
784 std::make_unique<QMemoryVideoBuffer>(QByteArray(static_cast<const char *>(sdata), size), sstride),
785 m_videoFrameFormat);
786 qCDebug(qLcPipeWireCaptureMore) << "got a frame of size " << buf->datas[0].chunk->size;
787
788 pw_stream_queue_buffer(m_stream.get(), pwBuffer);
789
790 signalLoop(true, false);
791}
792
793void QPipeWireCaptureHelper::destroy()
794{
795 if (!globalState)
796 return;
797 m_state = Stopping;
798 destroyStream(false);
799
800 pw_thread_loop_stop(m_threadLoop.get());
801
802 m_registry = {};
803 m_core = {};
804 m_context = {};
805 m_threadLoop = {};
806
807 m_state = NoState;
808}
809
810void QPipeWireCaptureHelper::onParamChanged(uint32_t id, const struct spa_pod *param)
811{
812 if (param == nullptr || id != SPA_PARAM_Format)
813 return;
814
815 if (spa_format_parse(param,
816 &m_format.media_type,
817 &m_format.media_subtype) < 0)
818 return;
819
820 if (m_format.media_type != SPA_MEDIA_TYPE_video
821 || m_format.media_subtype != SPA_MEDIA_SUBTYPE_raw)
822 return;
823
824 if (spa_format_video_raw_parse(param, &m_format.info.raw) < 0)
825 return;
826
827 qCDebug(qLcPipeWireCapture) << "got video format:";
828 qCDebug(qLcPipeWireCapture) << " format: " << m_format.info.raw.format
829 << " (" << spa_debug_type_find_name(spa_type_video_format, m_format.info.raw.format) << ")";
830 qCDebug(qLcPipeWireCapture) << " size: " << m_format.info.raw.size.width
831 << " x " << m_format.info.raw.size.height;
832 qCDebug(qLcPipeWireCapture) << " framerate: " << m_format.info.raw.framerate.num
833 << " / " << m_format.info.raw.framerate.denom;
834
835 m_size = QSize(static_cast<int>(m_format.info.raw.size.width),
836 static_cast<int>(m_format.info.raw.size.height));
837 m_pixelFormat = QPipeWireCaptureHelper::toQtPixelFormat(m_format.info.raw.format);
838 qCDebug(qLcPipeWireCapture) << "m_pixelFormat=" << m_pixelFormat;
839}
840
841// align with qt_videoFormatLookup in src/plugins/multimedia/gstreamer/common/qgst.cpp
842// https://docs.pipewire.org/group__spa__param.html#gacb274daea0abcce261955323e7d0b1aa
843// Most of the formats are identical to their GStreamer equivalent.
844QVideoFrameFormat::PixelFormat QPipeWireCaptureHelper::toQtPixelFormat(spa_video_format spaVideoFormat)
845{
846 switch (spaVideoFormat) {
847 default:
848 break;
849 case SPA_VIDEO_FORMAT_I420:
850 return QVideoFrameFormat::Format_YUV420P;
851 case SPA_VIDEO_FORMAT_Y42B:
852 return QVideoFrameFormat::Format_YUV422P;
853 case SPA_VIDEO_FORMAT_YV12:
854 return QVideoFrameFormat::Format_YV12;
855 case SPA_VIDEO_FORMAT_UYVY:
856 return QVideoFrameFormat::Format_UYVY;
857 case SPA_VIDEO_FORMAT_YUY2:
858 return QVideoFrameFormat::Format_YUYV;
859 case SPA_VIDEO_FORMAT_NV12:
860 return QVideoFrameFormat::Format_NV12;
861 case SPA_VIDEO_FORMAT_NV21:
862 return QVideoFrameFormat::Format_NV21;
863 case SPA_VIDEO_FORMAT_AYUV:
864 return QVideoFrameFormat::Format_AYUV;
865 case SPA_VIDEO_FORMAT_GRAY8:
866 return QVideoFrameFormat::Format_Y8;
867 case SPA_VIDEO_FORMAT_xRGB:
868 return QVideoFrameFormat::Format_XRGB8888;
869 case SPA_VIDEO_FORMAT_xBGR:
870 return QVideoFrameFormat::Format_XBGR8888;
871 case SPA_VIDEO_FORMAT_RGBx:
872 return QVideoFrameFormat::Format_RGBX8888;
873 case SPA_VIDEO_FORMAT_BGRx:
874 return QVideoFrameFormat::Format_BGRX8888;
875 case SPA_VIDEO_FORMAT_ARGB:
876 return QVideoFrameFormat::Format_ARGB8888;
877 case SPA_VIDEO_FORMAT_ABGR:
878 return QVideoFrameFormat::Format_ABGR8888;
879 case SPA_VIDEO_FORMAT_RGBA:
880 return QVideoFrameFormat::Format_RGBA8888;
881 case SPA_VIDEO_FORMAT_BGRA:
882 return QVideoFrameFormat::Format_BGRA8888;
883#if Q_BYTE_ORDER == Q_LITTLE_ENDIAN
884 case SPA_VIDEO_FORMAT_GRAY16_LE:
885 return QVideoFrameFormat::Format_Y16;
886 case SPA_VIDEO_FORMAT_P010_10LE:
887 return QVideoFrameFormat::Format_P010;
888#else
889 case SPA_VIDEO_FORMAT_GRAY16_BE:
890 return QVideoFrameFormat::Format_Y16;
891 case SPA_VIDEO_FORMAT_P010_10BE:
892 return QVideoFrameFormat::Format_P010;
893#endif
894 }
895
896 return QVideoFrameFormat::Format_Invalid;
897}
898
899spa_video_format QPipeWireCaptureHelper::toSpaVideoFormat(QVideoFrameFormat::PixelFormat pixelFormat)
900{
901 switch (pixelFormat) {
902 default:
903 break;
904 case QVideoFrameFormat::Format_YUV420P:
905 return SPA_VIDEO_FORMAT_I420;
906 case QVideoFrameFormat::Format_YUV422P:
907 return SPA_VIDEO_FORMAT_Y42B;
908 case QVideoFrameFormat::Format_YV12:
909 return SPA_VIDEO_FORMAT_YV12;
910 case QVideoFrameFormat::Format_UYVY:
911 return SPA_VIDEO_FORMAT_UYVY;
912 case QVideoFrameFormat::Format_YUYV:
913 return SPA_VIDEO_FORMAT_YUY2;
914 case QVideoFrameFormat::Format_NV12:
915 return SPA_VIDEO_FORMAT_NV12;
916 case QVideoFrameFormat::Format_NV21:
917 return SPA_VIDEO_FORMAT_NV21;
918 case QVideoFrameFormat::Format_AYUV:
919 return SPA_VIDEO_FORMAT_AYUV;
920 case QVideoFrameFormat::Format_Y8:
921 return SPA_VIDEO_FORMAT_GRAY8;
922 case QVideoFrameFormat::Format_XRGB8888:
923 return SPA_VIDEO_FORMAT_xRGB;
924 case QVideoFrameFormat::Format_XBGR8888:
925 return SPA_VIDEO_FORMAT_xBGR;
926 case QVideoFrameFormat::Format_RGBX8888:
927 return SPA_VIDEO_FORMAT_RGBx;
928 case QVideoFrameFormat::Format_BGRX8888:
929 return SPA_VIDEO_FORMAT_BGRx;
930 case QVideoFrameFormat::Format_ARGB8888:
931 return SPA_VIDEO_FORMAT_ARGB;
932 case QVideoFrameFormat::Format_ABGR8888:
933 return SPA_VIDEO_FORMAT_ABGR;
934 case QVideoFrameFormat::Format_RGBA8888:
935 return SPA_VIDEO_FORMAT_RGBA;
936 case QVideoFrameFormat::Format_BGRA8888:
937 return SPA_VIDEO_FORMAT_BGRA;
938#if Q_BYTE_ORDER == Q_LITTLE_ENDIAN
939 case QVideoFrameFormat::Format_Y16:
940 return SPA_VIDEO_FORMAT_GRAY16_LE;
941 case QVideoFrameFormat::Format_P010:
942 return SPA_VIDEO_FORMAT_P010_10LE;
943#else
944 case QVideoFrameFormat::Format_Y16:
945 return SPA_VIDEO_FORMAT_GRAY16_BE;
946 case QVideoFrameFormat::Format_P010:
947 return SPA_VIDEO_FORMAT_P010_10BE;
948#endif
949 }
950
951 return SPA_VIDEO_FORMAT_UNKNOWN;
952}
953
954} // namespace QtPipeWire
955
956QT_END_NAMESPACE
957
958QT_WARNING_POP
QT_BEGIN_NAMESPACE Q_STATIC_LOGGING_CATEGORY(lcSynthesizedIterableAccess, "qt.iterable.synthesized", QtWarningMsg)