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