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