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