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