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
28ObjectRemoveObserver::ObjectRemoveObserver(ObjectSerial objectSerial)
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, [this](std::optional<SpaObjectAudioFormat> const &) {
129 startCompressionTimer();
130 });
131 };
132
133 if (mediaClass == "Audio/Source" || mediaClass == "Audio/Source/Virtual") {
134 addPendingNode(m_pendingRecords.m_sources);
135 return;
136 }
137 if (mediaClass == "Audio/Sink" || mediaClass == "Audio/Sink/Virtual") {
138 addPendingNode(m_pendingRecords.m_sinks);
139 return;
140 }
141
142 break;
143 }
144 default:
145 return;
146 }
147}
148
150{
152
153 std::optional<ObjectSerial> serial = findObjectSerial(id);
154
155 if (!serial)
156 return; // we didn't track the object.
157
158 qCDebug(lcPipewireDeviceMonitor) << "removing object" << *serial;
159
160 std::vector<SharedObjectRemoveObserver> removalObserversForObject;
161 {
162 QWriteLocker lock{ &m_objectDictMutex };
163
164 for (const auto &observer : m_objectRemoveObserver) {
165 if (observer->serial() == serial)
166 removalObserversForObject.push_back(observer);
167 }
168 q20::erase_if(m_objectRemoveObserver, [&](const SharedObjectRemoveObserver &element) {
169 return element->serial() == serial;
170 });
171
172 m_objectSerialDict.erase(id);
173 m_serialObjectDict.erase(*serial);
174 }
175
176 for (const SharedObjectRemoveObserver &element : removalObserversForObject)
177 emit element->objectRemoved();
178
179 {
180 std::lock_guard guard{ m_pendingRecordsMutex };
181
182 m_pendingRecords.removeRecordsForObject(*serial);
183 m_pendingRecords.m_removals.push_back(*serial);
184 }
185
186 startCompressionTimer();
187}
188
196
204
205void QAudioDeviceMonitor::audioDevicesChanged(bool verifyThreading)
206{
207 // Note: we don't want to assert here if we're called from the QtPipeWire::QAudioDevices()
208 // constructor, as that might run on a worker thread (which pushed the instance to the app
209 // thread)
210 if (verifyThreading)
211 Q_ASSERT(this->thread()->isCurrentThread());
212
213 PendingRecords pendingRecords = [&] {
214 std::lock_guard guard{ m_pendingRecordsMutex };
215 PendingRecords resolvedRecords;
216
217 std::swap(m_pendingRecords.m_removals, resolvedRecords.m_removals);
218 std::swap(m_pendingRecords.m_defaultSource, resolvedRecords.m_defaultSource);
219 std::swap(m_pendingRecords.m_defaultSink, resolvedRecords.m_defaultSink);
220
221 // we may still have unresolved records, which wait on their format, but we only want to
222 // handle the fully resolved elements
223 auto takeFullyResolvedRecords = [](std::list<PendingNodeRecord> &toResolve,
224 std::list<PendingNodeRecord> &resolved) {
225 auto it = toResolve.begin();
226 while (it != toResolve.end()) {
227 if (it->formatFuture.isFinished()) {
228 auto next = std::next(it);
229 resolved.splice(resolved.end(), toResolve, it);
230 it = next;
231 } else {
232 it++;
233 }
234 }
235 };
236 takeFullyResolvedRecords(m_pendingRecords.m_sources, resolvedRecords.m_sources);
237 takeFullyResolvedRecords(m_pendingRecords.m_sinks, resolvedRecords.m_sinks);
238
239 return resolvedRecords;
240 }();
241
242 auto getNodeName =
243 [](std::variant<QByteArray, NoDefaultDeviceType> arg) -> std::optional<QByteArray> {
244 if (std::holds_alternative<NoDefaultDeviceType>(arg))
245 return std::nullopt;
246
247 return std::get<QByteArray>(arg);
248 };
249
250 bool defaultSourceChanged = pendingRecords.m_defaultSource.has_value();
251 if (defaultSourceChanged)
252 m_defaultSourceName = getNodeName(*pendingRecords.m_defaultSource);
253
254 bool defaultSinkChanged = pendingRecords.m_defaultSink.has_value();
255 if (defaultSinkChanged)
256 m_defaultSinkName = getNodeName(*pendingRecords.m_defaultSink);
257
258 if (!pendingRecords.m_sources.empty() || !pendingRecords.m_removals.empty()
259 || defaultSourceChanged)
260 updateSources(std::move(pendingRecords.m_sources), pendingRecords.m_removals);
261
262 if (!pendingRecords.m_sinks.empty() || !pendingRecords.m_removals.empty() || defaultSinkChanged)
263 updateSinks(std::move(pendingRecords.m_sinks), pendingRecords.m_removals);
264}
265
266void QAudioDeviceMonitor::PendingRecords::removeRecordsForObject(ObjectSerial id)
267{
268 for (std::list<PendingNodeRecord> *recordList : { &m_sources, &m_sinks }) {
269 recordList->remove_if([&](const PendingNodeRecord &record) {
270 return record.serial == id || record.deviceSerial == id;
271 });
272 }
273}
274
275template <QAudioDeviceMonitor::Direction Mode>
276std::optional<ObjectSerial>
277QAudioDeviceMonitor::findNodeSerialForNodeName(std::string_view nodeName) const
278{
279 // find node by name
280 QReadLocker guard(&m_mutex);
281
282 QSpan records = Mode == Direction::sink ? QSpan{ m_sinks } : QSpan{ m_sources };
283 auto it = std::find_if(records.begin(), records.end(), [&](const NodeRecord &sink) {
284 return getNodeName(sink.properties) == nodeName;
285 });
286
287 if (it == records.end())
288 return std::nullopt;
289 return it->serial;
290}
291
292std::optional<ObjectSerial> QAudioDeviceMonitor::findSinkNodeSerial(std::string_view nodeName) const
293{
294 return findNodeSerialForNodeName<Direction::sink>(nodeName);
295}
296
297std::optional<ObjectSerial>
298QAudioDeviceMonitor::findSourceNodeSerial(std::string_view nodeName) const
299{
300 return findNodeSerialForNodeName<Direction::source>(nodeName);
301}
302
303template <QAudioDeviceMonitor::Direction Mode>
304void QAudioDeviceMonitor::updateSourcesOrSinks(std::list<PendingNodeRecord> addedNodes,
305 QSpan<const ObjectSerial> removedObjects)
306{
307 QWriteLocker guard(&m_mutex);
308
309 std::vector<NodeRecord> &sinksOrSources = Mode == Direction::sink ? m_sinks : m_sources;
310
311 if (!removedObjects.empty()) {
312 for (ObjectSerial id : removedObjects) {
313 q20::erase_if(sinksOrSources, [&](const auto &record) {
314 return record.serial == id || record.deviceSerial == id;
315 });
316 }
317 }
318
319 for (PendingNodeRecord &record : addedNodes) {
320 QList<std::optional<SpaObjectAudioFormat>> results = record.formatFuture.results();
321 results.removeIf([](std::optional<SpaObjectAudioFormat> const &arg) {
322 return !arg.has_value();
323 });
324
325 results.removeIf([](std::optional<SpaObjectAudioFormat> const &arg) {
326 const bool isIEC61937EncapsulatedDevice = std::visit([](const auto &format) {
327 if constexpr (std::is_same_v<std::decay_t<decltype(format)>,
328 spa_audio_iec958_codec>) {
329 // we only support PCM devices
330 return format != SPA_AUDIO_IEC958_CODEC_PCM;
331 } else
332 return false;
333 }, arg->sampleTypes);
334 return isIEC61937EncapsulatedDevice;
335 });
336
337 // sort to list non-iec958 devices first
338 std::sort(results.begin(), results.end(),
339 [](std::optional<SpaObjectAudioFormat> const &lhs,
340 std::optional<SpaObjectAudioFormat> const &rhs) {
341 auto lhs_has_iec958 = std::holds_alternative<spa_audio_iec958_codec>(lhs->sampleTypes);
342 auto rhs_has_iec958 = std::holds_alternative<spa_audio_iec958_codec>(rhs->sampleTypes);
343 return lhs_has_iec958 < rhs_has_iec958;
344 });
345
346 if (results.size() > 1) {
347 qCDebug(lcPipewireDeviceMonitor)
348 << "Multiple formats supported by node, prefer non-iec958: format"
349 << record.serial;
350 }
351
352 if (!results.empty()) {
353 sinksOrSources.push_back(NodeRecord{
354 record.serial,
355 record.deviceSerial,
356 std::move(record.properties),
357 std::move(*results[0]),
358 });
359 } else {
360 qCDebug(lcPipewireDeviceMonitor)
361 << "Could not resolve audio format for" << record.serial;
362 }
363 }
364
365 QList<QAudioDevice> oldDeviceList =
366 Mode == Direction::sink ? m_sinkDeviceList : m_sourceDeviceList;
367
368 const std::optional<QByteArray> &defaultSinkOrSourceNodeNameBA =
369 Mode == Direction::sink ? m_defaultSinkName : m_defaultSourceName;
370
371 // revert once QTBUG-134902 is fixed
372 const auto defaultSinkOrSourceNodeName = [&]() -> std::optional<std::string_view> {
373 if (defaultSinkOrSourceNodeNameBA)
374 return std::string_view{
375 defaultSinkOrSourceNodeNameBA->data(),
376 std::size_t(defaultSinkOrSourceNodeNameBA->size()),
377 };
378 return std::nullopt;
379 }();
380
381 QList<QAudioDevice> newDeviceList;
382
383 // we brute-force re-create the device list ... not smart and it can certainly be improved
384 for (NodeRecord &sinkOrSource : sinksOrSources) {
385 std::optional<ObjectSerial> deviceSerial = sinkOrSource.deviceSerial;
386 std::optional<std::string_view> nodeName = getNodeName(sinkOrSource.properties);
387 bool isDefault = (defaultSinkOrSourceNodeName == nodeName);
388
389 std::optional<QByteArray> sysFsPath = [&]() -> std::optional<QByteArray> {
390 if (!deviceSerial)
391 return std::nullopt;
392
393 auto deviceIt = m_devices.find(*deviceSerial);
394 if (deviceIt == m_devices.end()) {
395 qCDebug(lcPipewireDeviceMonitor) << "No device for device id" << *deviceSerial;
396 return std::nullopt;
397 }
398
399 std::optional<std::string_view> deviceSysfsPath =
400 getDeviceSysfsPath(deviceIt->second.properties);
401
402 if (!deviceSysfsPath)
403 return std::nullopt;
404
405 return QByteArray{
406 *deviceSysfsPath,
407 };
408 }();
409
410 auto devicePrivate = std::make_unique<QPipewireAudioDevicePrivate>(
411 sinkOrSource.properties, sysFsPath, sinkOrSource.format, QAudioDevice::Mode::Output,
412 isDefault);
413
414 QAudioDevice device = QAudioDevicePrivate::createQAudioDevice(std::move(devicePrivate));
415
416 newDeviceList.push_back(device);
417
418 qCDebug(lcPipewireDeviceMonitor) << "adding device" << sysFsPath;
419 }
420
421 // sort by description
422 std::sort(newDeviceList.begin(), newDeviceList.end(),
423 [](const QAudioDevice &lhs, const QAudioDevice &rhs) {
424 return lhs.description() < rhs.description();
425 });
426
427 guard.unlock();
428
429 bool deviceListsEqual = ranges::equal(oldDeviceList, newDeviceList,
430 [](const QAudioDevice &lhs, const QAudioDevice &rhs) {
431 return (lhs.id() == rhs.id()) && (lhs.isDefault() == rhs.isDefault());
432 });
433
434 if (!deviceListsEqual) {
435 qCDebug(lcPipewireDeviceMonitor) << "updated device list";
436
437 if constexpr (Mode == Direction::sink) {
438 m_sinkDeviceList = newDeviceList;
439 emit audioSinksChanged(m_sinkDeviceList);
440 } else {
441 m_sourceDeviceList = newDeviceList;
442 emit audioSourcesChanged(m_sourceDeviceList);
443 }
444 }
445}
446
447void QAudioDeviceMonitor::updateSinks(std::list<PendingNodeRecord> addedNodes,
448 QSpan<const ObjectSerial> removedObjects)
449{
450 updateSourcesOrSinks<Direction::sink>(std::move(addedNodes), removedObjects);
451}
452
453void QAudioDeviceMonitor::updateSources(std::list<PendingNodeRecord> addedNodes,
454 QSpan<const ObjectSerial> removedObjects)
455{
456 updateSourcesOrSinks<Direction::source>(std::move(addedNodes), removedObjects);
457}
458
459std::optional<ObjectSerial> QAudioDeviceMonitor::findDeviceSerial(std::string_view deviceName) const
460{
461 QReadLocker guard(&m_mutex);
462 auto it = std::find_if(m_devices.begin(), m_devices.end(), [&](auto const &entry) {
463 return getDeviceName(entry.second.properties) == deviceName;
464 });
465 if (it == m_devices.end())
466 return std::nullopt;
467 return it->first;
468}
469
471{
472 QReadLocker lock{ &m_objectDictMutex };
473
474 auto it = m_serialObjectDict.find(serial);
475 if (it != m_serialObjectDict.end())
476 return it->second;
477 return std::nullopt;
478}
479
481{
482 QReadLocker lock{ &m_objectDictMutex };
483
484 auto it = m_objectSerialDict.find(id);
485 if (it != m_objectSerialDict.end())
486 return it->second;
487 return std::nullopt;
488}
489
491{
492 QWriteLocker lock{ &m_objectDictMutex };
493
494 if (m_serialObjectDict.find(observer->serial()) == m_serialObjectDict.end())
495 return false; // don't register observer if the object has already been removed
496
497 m_objectRemoveObserver.push_back(std::move(observer));
498 return true;
499}
500
502{
503 QWriteLocker lock{ &m_objectDictMutex };
504
505 q20::erase(m_objectRemoveObserver, observer);
506}
507
509{
510 // force initial device enumeration
512
513 // sync with format futures
514 for (;;) {
516
517 std::lock_guard pendingRecordLock{
518 m_pendingRecordsMutex,
519 };
520
521 for (ObjectSerial removed : m_pendingRecords.m_removals)
522 m_pendingRecords.removeRecordsForObject(removed);
523
524 auto allFormatsResolved = [](const std::list<PendingNodeRecord> &list) {
525 return std::all_of(list.begin(), list.end(), [](const PendingNodeRecord &record) {
526 return record.formatFuture.isFinished();
527 });
528 };
529
530 if (allFormatsResolved(m_pendingRecords.m_sources)
531 && allFormatsResolved(m_pendingRecords.m_sinks))
532 break;
533 }
534
535 // now all formats have been resolved and we can update the device list
536 audioDevicesChanged(verifyThreading);
537
538 QReadLocker lock{ &m_mutex };
539 return {
540 .sources = m_sourceDeviceList,
541 .sinks = m_sinkDeviceList,
542 };
543}
544
545void QAudioDeviceMonitor::startCompressionTimer()
546{
547 QMetaObject::invokeMethod(this, [this] {
548 if (m_compressionTimer.isActive())
549 return;
550 m_compressionTimer.start();
551 });
552}
553
554QAudioDeviceMonitor::PendingNodeRecord::PendingNodeRecord(ObjectId object, ObjectSerial serial,
555 std::optional<ObjectSerial> deviceSerial,
556 PwPropertyDict properties):
557 serial{
558 serial,
559 },
560 deviceSerial{
561 deviceSerial,
562 },
563 properties{
564 std::move(properties),
565 }
566{
568
569 auto promise = std::make_shared<QPromise<std::optional<SpaObjectAudioFormat>>>();
570 formatFuture = promise->future();
571 promise->start();
572
573 auto onParam = [promise](int /*seq*/, uint32_t /*id*/, uint32_t /*index*/, uint32_t /*next*/,
574 const struct spa_pod *param) mutable {
575 std::optional<SpaObjectAudioFormat> format = SpaObjectAudioFormat::parse(param);
576 promise->addResult(format);
577 };
578
579 QAudioContextManager::withEventLoopLock([&] {
581 PwNodeHandle nodeProxy = context->bindNode(object);
582
583 enumFormatListener = std::make_unique<NodeEventListener>(std::move(nodeProxy),
584 NodeEventListener::NodeHandler{
585 {},
586 std::move(onParam),
587 });
588
589 enumFormatListener->enumParams(SPA_PARAM_EnumFormat);
590
591 // we potentially receive multiple calls to pw_core_events->param if devices support
592 // multiple formats. e.g. hdmi devices potentially report "raw" pcm and iec958. so we sync
593 // with the pipewire server, to act as barrier.
594 enumFormatDoneListener = std::make_unique<CoreEventDoneListener>();
595 enumFormatDoneListener->asyncWait(context->coreConnection().get(), [promise] {
596 promise->finish();
597 });
598 });
599}
600
601} // namespace QtPipeWire
602
603QT_END_NAMESPACE
604
605#include "moc_qpipewire_audiodevicemonitor_p.cpp"
void unregisterObserver(const SharedObjectRemoveObserver &)
std::optional< ObjectSerial > findSourceNodeSerial(std::string_view nodeName) const
DeviceLists getDeviceLists(bool verifyThreading=true)
std::optional< ObjectId > findObjectId(ObjectSerial)
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< ObjectSerial > findObjectSerial(ObjectId)
bool registerObserver(SharedObjectRemoveObserver)
Combined button and popup list for selecting options.
Q_STATIC_LOGGING_CATEGORY(lcPipewireRegistry, "qt.multimedia.pipewire.registry")
StrongIdType< uint32_t, ObjectIdTag > ObjectId
StrongIdType< uint64_t, ObjectSerialTag > ObjectSerial
std::shared_ptr< ObjectRemoveObserver > SharedObjectRemoveObserver