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_audiodevicemonitor.cpp
Go to the documentation of this file.
1// Copyright (C) 2025 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/qmultimedia_ranges_p.h>
7#include <QtMultimedia/private/qpipewire_audiocontextmanager_p.h>
8#include <QtMultimedia/private/qpipewire_audiodevice_p.h>
9#include <QtMultimedia/private/qpipewire_registry_support_p.h>
10#include <QtCore/private/qcoreapplication_p.h>
11#include <QtCore/private/qflatmap_p.h>
12#include <QtCore/private/qthread_p.h>
13#include <QtCore/q20vector.h>
14#include <QtCore/qcoreapplication.h>
15#include <QtCore/qdebug.h>
16#include <QtCore/qloggingcategory.h>
17
18#include <mutex>
19
20QT_BEGIN_NAMESPACE
21namespace ranges = QtMultimediaPrivate::ranges;
22
23namespace QtPipeWire {
24
25using namespace QtMultimediaPrivate;
26
27Q_STATIC_LOGGING_CATEGORY(lcPipewireDeviceMonitor, "qt.multimedia.pipewire.devicemonitor")
28
30 : m_observedSerial(objectSerial)
31{
32}
33
38
40{
41 if (!QThread::isMainThread())
42 // ensure that device monitor runs on application thread
43 moveToThread(qApp->thread());
44
45 constexpr auto compressionTime = std::chrono::milliseconds(50);
46
47 m_compressionTimer.setTimerType(Qt::TimerType::CoarseTimer);
48 m_compressionTimer.setInterval(compressionTime);
49 m_compressionTimer.setSingleShot(true);
50
51 m_compressionTimer.callOnTimeout(this, [this] {
52 audioDevicesChanged();
53 });
54
55 m_compressionTimerThread.setObjectName("PWDevMon");
56 m_compressionTimerThread.setServiceLevel(QThread::QualityOfService::Eco);
57 m_compressionTimerThread.setStackSize(1024 * 1024); // 1MB should be enough
58 m_compressionTimerThread.start();
59 m_compressionTimerThread.setPriority(QThread::Priority::LowPriority);
60 m_compressionTimer.moveToThread(&m_compressionTimerThread);
61
62 qAddPostRoutine([] {
63 QAudioDeviceMonitor &monitor = QAudioContextManager::deviceMonitor();
64 QMetaObject::invokeMethod(&monitor.m_compressionTimer, [&] {
65 monitor.m_compressionTimer.stop();
66 monitor.m_compressionTimer.moveToThread(qApp->thread());
67 }, Qt::BlockingQueuedConnection);
68 });
69}
70
72{
73 if (m_compressionTimer.thread() != thread()) {
74 QMetaObject::invokeMethod(&m_compressionTimer, [this] {
75 m_compressionTimer.stop();
76 m_compressionTimer.moveToThread(thread());
77 }, Qt::BlockingQueuedConnection);
78 }
79
80 m_compressionTimerThread.quit();
81 m_compressionTimerThread.wait();
82}
83
84void QAudioDeviceMonitor::objectAdded(ObjectId id, uint32_t /*permissions*/,
85 PipewireRegistryType objectType, uint32_t /*version*/,
86 const spa_dict &propDict)
87{
88 Q_ASSERT(QAudioContextManager::isInPwThreadLoop());
89
90 Q_ASSERT(objectType == PipewireRegistryType::Device
91 || objectType == PipewireRegistryType::Node);
92
93 PwPropertyDict props = toPropertyDict(propDict);
94 std::optional<std::string_view> mediaClass = getMediaClass(props);
95 if (!mediaClass)
96 return;
97
98 std::optional<ObjectSerial> serial = getObjectSerial(props);
99 Q_ASSERT(serial);
100 {
101 QWriteLocker lock{ &m_objectDictMutex };
102 m_objectSerialDict.emplace(id, *serial);
103 m_serialObjectDict.emplace(*serial, id);
104 }
105
106 switch (objectType) {
107 case PipewireRegistryType::Device: {
108 if (mediaClass != "Audio/Device")
109 return;
110
111 // we can store devices immediately
112 qCDebug(lcPipewireDeviceMonitor)
113 << "added device" << *serial << getDeviceDescription(props).value_or("");
114
115 QWriteLocker lock{ &m_mutex };
116 m_devices.emplace(*serial, DeviceRecord{ *serial, std::move(props) });
117
118 return;
119 }
120 case PipewireRegistryType::Node: {
121 // for nodes we need to enumerate the formats
122
123 auto addPendingNode = [&](std::list<PendingNodeRecord> &pendingRecords) {
124 std::optional<std::string_view> nodeName = getNodeName(props);
125 if (!nodeName) {
126 qCWarning(lcPipewireDeviceMonitor) << "node without name (ignoring):" << props;
127 return;
128 }
129
130 if (nodeName == "auto_null") {
131 // pipewire will create a dummy output in case theres' no physical output. We want
132 // to filter that out
133 qCWarning(lcPipewireDeviceMonitor) << "Ignoring dummy output:" << props;
134 return;
135 }
136
137 // Note: virtual devices have neither deviceId, nor deviceSerial. Physical devices have both
138 std::optional<ObjectId> deviceId = getDeviceId(props);
139 std::optional<ObjectSerial> deviceSerial =
140 deviceId ? findObjectSerial(*deviceId) : std::nullopt;
141
142 if (deviceId && !deviceSerial) {
143 qCInfo(lcPipewireDeviceMonitor) << "Cannot add node: device removed";
144 return;
145 }
146
147 std::lock_guard guard{ m_pendingRecordsMutex };
148
149 qCDebug(lcPipewireDeviceMonitor) << "added node for device" << serial << deviceSerial;
150
151 // enumerating the audio format is asynchronous: we enumerate the formats asynchronously
152 // and wait for the result before updating the device list
153 pendingRecords.emplace_back(id, *serial, deviceSerial, std::move(props));
154 pendingRecords.back().formatFuture.then(
155 &m_compressionTimer,
156 [this, weakResults = std::weak_ptr{ pendingRecords.back().formatResults }](
157 std::vector<SpaObjectAudioFormat> formats) {
158 // we do not handle the formats immediately, but coalesce multiple format updates, reduces the number
159 // of changes to the device list
160 if (auto ptr = weakResults.lock()) {
161 *ptr = std::move(formats);
162 startCompressionTimer();
163 }
164 });
165 };
166
167 if (mediaClass == "Audio/Source" || mediaClass == "Audio/Source/Virtual") {
168 addPendingNode(m_pendingRecords.m_sources);
169 return;
170 }
171 if (mediaClass == "Audio/Sink" || mediaClass == "Audio/Sink/Virtual") {
172 addPendingNode(m_pendingRecords.m_sinks);
173 return;
174 }
175
176 break;
177 }
178 default:
179 return;
180 }
181}
182
184{
185 Q_ASSERT(QAudioContextManager::isInPwThreadLoop());
186
187 std::optional<ObjectSerial> serial = findObjectSerial(id);
188
189 if (!serial)
190 return; // we didn't track the object.
191
192 qCDebug(lcPipewireDeviceMonitor) << "removing object" << *serial;
193
194 std::vector<SharedObjectRemoveObserver> removalObserversForObject;
195 {
196 QWriteLocker lock{ &m_objectDictMutex };
197
198 for (const auto &observer : m_objectRemoveObserver) {
199 if (observer->serial() == serial)
200 removalObserversForObject.push_back(observer);
201 }
202 q20::erase_if(m_objectRemoveObserver, [&](const SharedObjectRemoveObserver &element) {
203 return element->serial() == serial;
204 });
205
206 m_objectSerialDict.erase(id);
207 m_serialObjectDict.erase(*serial);
208 }
209
210 for (const SharedObjectRemoveObserver &element : removalObserversForObject)
211 emit element->objectRemoved();
212
213 {
214 std::lock_guard guard{ m_pendingRecordsMutex };
215
216 m_pendingRecords.removeRecordsForObject(*serial);
217 m_pendingRecords.m_removals.push_back(*serial);
218 }
219
220 startCompressionTimer();
221}
222
230
238
239void QAudioDeviceMonitor::audioDevicesChanged(bool verifyThreading)
240{
241 // Note: we don't want to assert here if we're called from the QtPipeWire::QAudioDevices()
242 // constructor, as that might run on a worker thread (which pushed the instance to the app
243 // thread)
244 if (verifyThreading)
245 Q_ASSERT(this->thread()->isCurrentThread());
246
247 PendingRecords pendingRecords = [&] {
248 std::lock_guard guard{ m_pendingRecordsMutex };
249 PendingRecords resolvedRecords;
250
251 std::swap(m_pendingRecords.m_removals, resolvedRecords.m_removals);
252 std::swap(m_pendingRecords.m_defaultSource, resolvedRecords.m_defaultSource);
253 std::swap(m_pendingRecords.m_defaultSink, resolvedRecords.m_defaultSink);
254
255 // we may still have unresolved records, which wait on their format, but we only want to
256 // handle the fully resolved elements
257 auto takeFullyResolvedRecords = [](std::list<PendingNodeRecord> &toResolve,
258 std::list<PendingNodeRecord> &resolved) {
259 auto it = toResolve.begin();
260 while (it != toResolve.end()) {
261 // we do not only need the future to be resolved, but the continuation needs to
262 // have run (just checking for the future being ready is not sufficient)
263 const bool isFullyResolved = it->formatResults->has_value();
264 if (isFullyResolved) {
265 auto next = std::next(it);
266 resolved.splice(resolved.end(), toResolve, it);
267 it = next;
268 } else {
269 it++;
270 }
271 }
272 };
273 takeFullyResolvedRecords(m_pendingRecords.m_sources, resolvedRecords.m_sources);
274 takeFullyResolvedRecords(m_pendingRecords.m_sinks, resolvedRecords.m_sinks);
275
276 return resolvedRecords;
277 }();
278
279 auto getNodeName =
280 [](std::variant<QByteArray, NoDefaultDeviceType> arg) -> std::optional<QByteArray> {
281 if (std::holds_alternative<NoDefaultDeviceType>(arg))
282 return std::nullopt;
283
284 return std::get<QByteArray>(arg);
285 };
286
287 bool defaultSourceChanged = pendingRecords.m_defaultSource.has_value();
288 if (defaultSourceChanged)
289 m_defaultSourceName = getNodeName(*pendingRecords.m_defaultSource);
290
291 bool defaultSinkChanged = pendingRecords.m_defaultSink.has_value();
292 if (defaultSinkChanged)
293 m_defaultSinkName = getNodeName(*pendingRecords.m_defaultSink);
294
295 if (!pendingRecords.m_sources.empty() || !pendingRecords.m_removals.empty()
296 || defaultSourceChanged)
297 updateSources(std::move(pendingRecords.m_sources), pendingRecords.m_removals);
298
299 if (!pendingRecords.m_sinks.empty() || !pendingRecords.m_removals.empty() || defaultSinkChanged)
300 updateSinks(std::move(pendingRecords.m_sinks), pendingRecords.m_removals);
301}
302
303void QAudioDeviceMonitor::PendingRecords::removeRecordsForObject(ObjectSerial id)
304{
305 for (std::list<PendingNodeRecord> *recordList : { &m_sources, &m_sinks }) {
306 recordList->remove_if([&](const PendingNodeRecord &record) {
307 return record.serial == id || record.deviceSerial == id;
308 });
309 }
310}
311
312template <QAudioDeviceMonitor::Direction Mode>
313std::optional<ObjectSerial>
314QAudioDeviceMonitor::findNodeSerialForNodeName(std::string_view nodeName) const
315{
316 // find node by name
317 QReadLocker guard(&m_mutex);
318
319 QSpan records = Mode == Direction::sink ? QSpan{ m_sinks } : QSpan{ m_sources };
320 auto it = ranges::find_if(records, [&](const NodeRecord &sink) {
321 return getNodeName(sink.properties) == nodeName;
322 });
323
324 if (it == records.end())
325 return std::nullopt;
326 return it->serial;
327}
328
330{
331 return findNodeSerialForNodeName<Direction::sink>(nodeName);
332}
333
335QAudioDeviceMonitor::findSourceNodeSerial(std::string_view nodeName) const
336{
337 return findNodeSerialForNodeName<Direction::source>(nodeName);
338}
339
340template <QAudioDeviceMonitor::Direction Mode>
341void QAudioDeviceMonitor::updateSourcesOrSinks(std::list<PendingNodeRecord> addedNodes,
342 QSpan<const ObjectSerial> removedObjects)
343{
344 QWriteLocker guard(&m_mutex);
345
346 std::vector<NodeRecord> &sinksOrSources = Mode == Direction::sink ? m_sinks : m_sources;
347
348 if (!removedObjects.empty()) {
349 for (ObjectSerial id : removedObjects) {
350 q20::erase_if(sinksOrSources, [&](const auto &record) {
351 return record.serial == id || record.deviceSerial == id;
352 });
353 }
354 }
355
356 for (PendingNodeRecord &record : addedNodes) {
357 Q_ASSERT(record.formatResults && record.formatResults->has_value());
358 std::vector<SpaObjectAudioFormat> &results = **record.formatResults;
359
360 q20::erase_if(results, [](SpaObjectAudioFormat const &arg) {
361 const bool isIEC61937EncapsulatedDevice = std::visit([](const auto &format) {
362 if constexpr (std::is_same_v<std::decay_t<decltype(format)>,
363 spa_audio_iec958_codec>) {
364 // we only support PCM devices
365 return format != SPA_AUDIO_IEC958_CODEC_PCM;
366 } else
367 return false;
368 }, arg.sampleTypes);
369 return isIEC61937EncapsulatedDevice;
370 });
371
372 // sort to list non-iec958 devices first
373 ranges::sort(results, [](SpaObjectAudioFormat const &lhs, SpaObjectAudioFormat const &rhs) {
374 auto lhs_has_iec958 = std::holds_alternative<spa_audio_iec958_codec>(lhs.sampleTypes);
375 auto rhs_has_iec958 = std::holds_alternative<spa_audio_iec958_codec>(rhs.sampleTypes);
376 return lhs_has_iec958 < rhs_has_iec958;
377 });
378
379 if (results.size() > 1) {
380 qCDebug(lcPipewireDeviceMonitor)
381 << "Multiple formats supported by node, prefer non-iec958: format"
382 << record.serial;
383 }
384
385 if (!results.empty()) {
386 sinksOrSources.push_back(NodeRecord{
387 record.serial,
388 record.deviceSerial,
389 std::move(record.properties),
390 std::move(results.front()),
391 });
392 } else {
393 qCDebug(lcPipewireDeviceMonitor)
394 << "Could not resolve audio format for" << record.serial;
395 }
396 }
397
398 QList<QAudioDevice> oldDeviceList =
399 Mode == Direction::sink ? m_sinkDeviceList : m_sourceDeviceList;
400
401 const std::optional<QByteArray> &defaultSinkOrSourceNodeNameBA =
402 Mode == Direction::sink ? m_defaultSinkName : m_defaultSourceName;
403
404 // revert once QTBUG-134902 is fixed
405 const auto defaultSinkOrSourceNodeName = [&]() -> std::optional<std::string_view> {
406 if (defaultSinkOrSourceNodeNameBA)
407 return std::string_view{
408 defaultSinkOrSourceNodeNameBA->data(),
409 std::size_t(defaultSinkOrSourceNodeNameBA->size()),
410 };
411 return std::nullopt;
412 }();
413
414 QList<QAudioDevice> newDeviceList;
415
416 // we brute-force re-create the device list ... not smart and it can certainly be improved
417 for (NodeRecord &sinkOrSource : sinksOrSources) {
418 std::optional<std::string_view> nodeName = getNodeName(sinkOrSource.properties);
419 bool isDefault = (defaultSinkOrSourceNodeName == nodeName);
420
421 auto devicePrivate = std::make_unique<QPipewireAudioDevicePrivate>(
422 sinkOrSource.properties, sinkOrSource.format, QAudioDevice::Mode::Output,
423 isDefault);
424
425 QAudioDevice device = QAudioDevicePrivate::createQAudioDevice(std::move(devicePrivate));
426
427 newDeviceList.push_back(device);
428
429 qCDebug(lcPipewireDeviceMonitor) << "adding device" << nodeName;
430 }
431
432 // sort by description
433 ranges::sort(newDeviceList, [](const QAudioDevice &lhs, const QAudioDevice &rhs) {
434 return lhs.description() < rhs.description();
435 });
436
437 guard.unlock();
438
439 bool deviceListsEqual = ranges::equal(oldDeviceList, newDeviceList,
440 [](const QAudioDevice &lhs, const QAudioDevice &rhs) {
441 return (lhs.id() == rhs.id()) && (lhs.isDefault() == rhs.isDefault());
442 });
443
444 if (!deviceListsEqual) {
445 qCDebug(lcPipewireDeviceMonitor) << "updated device list";
446
447 if constexpr (Mode == Direction::sink) {
448 m_sinkDeviceList = newDeviceList;
449 emit audioSinksChanged(m_sinkDeviceList);
450 } else {
451 m_sourceDeviceList = newDeviceList;
452 emit audioSourcesChanged(m_sourceDeviceList);
453 }
454 }
455}
456
457void QAudioDeviceMonitor::updateSinks(std::list<PendingNodeRecord> addedNodes,
458 QSpan<const ObjectSerial> removedObjects)
459{
460 updateSourcesOrSinks<Direction::sink>(std::move(addedNodes), removedObjects);
461}
462
463void QAudioDeviceMonitor::updateSources(std::list<PendingNodeRecord> addedNodes,
464 QSpan<const ObjectSerial> removedObjects)
465{
466 updateSourcesOrSinks<Direction::source>(std::move(addedNodes), removedObjects);
467}
468
469std::optional<ObjectSerial> QAudioDeviceMonitor::findDeviceSerial(std::string_view deviceName) const
470{
471 QReadLocker guard(&m_mutex);
472 auto it = ranges::find_if(m_devices, [&](auto const &entry) {
473 return getDeviceName(entry.second.properties) == deviceName;
474 });
475 if (it == m_devices.end())
476 return std::nullopt;
477 return it->first;
478}
479
481{
482 QReadLocker lock{ &m_objectDictMutex };
483
484 auto it = m_serialObjectDict.find(serial);
485 if (it != m_serialObjectDict.end())
486 return it->second;
487 return std::nullopt;
488}
489
491{
492 QReadLocker lock{ &m_objectDictMutex };
493
494 auto it = m_objectSerialDict.find(id);
495 if (it != m_objectSerialDict.end())
496 return it->second;
497 return std::nullopt;
498}
499
500bool QAudioDeviceMonitor::registerObserver(SharedObjectRemoveObserver observer)
501{
502 QWriteLocker lock{ &m_objectDictMutex };
503
504 if (m_serialObjectDict.find(observer->serial()) == m_serialObjectDict.end())
505 return false; // don't register observer if the object has already been removed
506
507 m_objectRemoveObserver.push_back(std::move(observer));
508 return true;
509}
510
511void QAudioDeviceMonitor::unregisterObserver(const SharedObjectRemoveObserver &observer)
512{
513 QWriteLocker lock{ &m_objectDictMutex };
514
515 q20::erase(m_objectRemoveObserver, observer);
516}
517
519{
520 // force initial device enumeration
521 QAudioContextManager::instance()->syncRegistry();
522
523 // sync with format futures
524 for (;;) {
525 QAudioContextManager::instance()->syncRegistry();
526
527 std::lock_guard pendingRecordLock{
528 m_pendingRecordsMutex,
529 };
530
531 for (ObjectSerial removed : m_pendingRecords.m_removals)
532 m_pendingRecords.removeRecordsForObject(removed);
533
534 auto allFormatsResolved = [](const std::list<PendingNodeRecord> &list) {
535 return ranges::all_of(list, [](const PendingNodeRecord &record) {
536 return record.formatFuture.isFinished();
537 });
538 };
539
540 if (allFormatsResolved(m_pendingRecords.m_sources)
541 && allFormatsResolved(m_pendingRecords.m_sinks))
542 break;
543 }
544
545 // HACK: getDeviceLists is synchronous, so we need to wait for all the pending format have
546 // been populated force the continuations posted on &m_compressionTimer to be executed (compare
547 // QAudioDeviceMonitor::objectAdded)
548 // LATER: can we asynchronously resolve the format, similar to std::future<AudioDeviceFormat>?
549 QMetaObject::invokeMethod(&m_compressionTimer, [&] {
550 QCoreApplication::sendPostedEvents(&m_compressionTimer, QEvent::MetaCall);
551 audioDevicesChanged(verifyThreading);
552 m_compressionTimer.stop();
553 }, Qt::BlockingQueuedConnection);
554
555 QReadLocker lock{ &m_mutex };
556 return {
557 .sources = m_sourceDeviceList,
558 .sinks = m_sinkDeviceList,
559 };
560}
561
562void QAudioDeviceMonitor::startCompressionTimer()
563{
564 QMetaObject::invokeMethod(&m_compressionTimer, [this] {
565 if (m_compressionTimer.isActive())
566 return;
567 m_compressionTimer.start();
568 });
569}
570
571QAudioDeviceMonitor::PendingNodeRecord::PendingNodeRecord(ObjectId object, ObjectSerial serial,
572 std::optional<ObjectSerial> deviceSerial,
573 PwPropertyDict properties):
574 serial{
575 serial,
576 },
577 deviceSerial{
578 deviceSerial,
579 },
580 properties{
581 std::move(properties),
582 },
583 formatResults{
584 std::make_shared<std::optional<std::vector<SpaObjectAudioFormat>>>()
585 }
586{
587 Q_ASSERT(QAudioContextManager::isInPwThreadLoop());
588
589 auto promise = std::make_shared<QPromise<std::vector<SpaObjectAudioFormat>>>();
590 formatFuture = promise->future();
591
592 auto shared_results = std::make_shared<std::vector<SpaObjectAudioFormat>>();
593
594 auto onParam = [shared_results](int /*seq*/, uint32_t /*id*/, uint32_t /*index*/,
595 uint32_t /*next*/, const struct spa_pod *param) mutable {
596 std::optional<SpaObjectAudioFormat> format = SpaObjectAudioFormat::parse(param);
597 if (format)
598 shared_results->emplace_back(*format);
599 };
600
601 QAudioContextManager::withEventLoopLock([&] {
602 QAudioContextManager *context = QAudioContextManager::instance();
603 PwNodeHandle nodeProxy = context->bindNode(object);
604
605 enumFormatListener = std::make_unique<NodeEventListener>(std::move(nodeProxy),
606 NodeEventListener::NodeHandler{
607 {},
608 std::move(onParam),
609 });
610
611 enumFormatListener->enumParams(SPA_PARAM_EnumFormat);
612
613 // we potentially receive multiple calls to pw_core_events->param if devices support
614 // multiple formats. e.g. hdmi devices potentially report "raw" pcm and iec958. so we sync
615 // with the pipewire server, to act as barrier.
616 enumFormatDoneListener = std::make_unique<CoreEventDoneListener>();
617 enumFormatDoneListener->asyncWait(context->coreConnection().get(),
618 [promise, shared_results] {
619 promise->start();
620 promise->emplaceResult(std::move(*shared_results));
621 promise->finish();
622 });
623 });
624}
625
626} // namespace QtPipeWire
627
628QT_END_NAMESPACE
629
630#include "moc_qpipewire_audiodevicemonitor_p.cpp"
std::optional< ObjectSerial > findObjectSerial(ObjectId) const
void unregisterObserver(const SharedObjectRemoveObserver &)
std::optional< ObjectSerial > findSourceNodeSerial(std::string_view nodeName) const
DeviceLists getDeviceLists(bool verifyThreading=true)
std::optional< ObjectSerial > findSinkNodeSerial(std::string_view nodeName) const
void objectAdded(ObjectId, uint32_t permissions, PipewireRegistryType, uint32_t version, const spa_dict &props)
std::optional< ObjectId > findObjectId(ObjectSerial) const
bool registerObserver(SharedObjectRemoveObserver)
Q_STATIC_LOGGING_CATEGORY(lcPipewireAudioSink, "qt.multimedia.pipewire.audiosink")
StrongIdType< uint64_t, ObjectSerialTag > ObjectSerial