10#include <QtCore/qcoreapplication.h>
11#include <QtCore/qdebug.h>
12#include <QtCore/qloggingcategory.h>
13#include <QtCore/private/qflatmap_p.h>
15#include <QtMultimedia/private/qmultimedia_ranges_p.h>
29 : m_observedSerial(objectSerial)
35 return m_observedSerial;
40 if (!QThread::isMainThread()) {
42 moveToThread(qApp->thread());
43 m_compressionTimer.moveToThread(qApp->thread());
46 constexpr auto compressionTime = std::chrono::milliseconds(50);
48 m_compressionTimer.setTimerType(Qt::TimerType::CoarseTimer);
49 m_compressionTimer.setInterval(compressionTime);
50 m_compressionTimer.setSingleShot(
true);
52 m_compressionTimer.callOnTimeout(
this, [
this] {
53 audioDevicesChanged();
59 const spa_dict &propDict)
66 PwPropertyDict props = toPropertyDict(propDict);
67 std::optional<std::string_view> mediaClass = getMediaClass(props);
74 QWriteLocker lock{ &m_objectDictMutex };
75 m_objectSerialDict.emplace(id, *serial);
76 m_serialObjectDict.emplace(*serial, id);
81 if (mediaClass !=
"Audio/Device")
85 qCDebug(lcPipewireDeviceMonitor)
86 <<
"added device" << *serial << getDeviceDescription(props).value_or(
"");
88 QWriteLocker lock{ &m_mutex };
89 m_devices.emplace(*serial, DeviceRecord{ *serial, std::move(props) });
96 auto addPendingNode = [&](std::list<PendingNodeRecord> &pendingRecords) {
97 std::optional<
ObjectId> deviceId = getDeviceId(props);
101 qCDebug(lcPipewireDeviceMonitor) <<
"no device ID in node (ignoring):" << props;
105 std::optional<
ObjectSerial> deviceSerial = findObjectSerial(*deviceId);
107 qCInfo(lcPipewireDeviceMonitor) <<
"Cannot add node: device removed";
111 std::lock_guard guard{ m_pendingRecordsMutex };
113 qCDebug(lcPipewireDeviceMonitor)
114 <<
"added node for device " << *serial << *deviceSerial;
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();
125 if (mediaClass ==
"Audio/Source")
126 addPendingNode(m_pendingRecords.m_sources);
127 else if (mediaClass ==
"Audio/Sink")
128 addPendingNode(m_pendingRecords.m_sinks);
145 qCDebug(lcPipewireDeviceMonitor) <<
"removing object" << *serial;
147 std::vector<SharedObjectRemoveObserver> removalObserversForObject;
149 QWriteLocker lock{ &m_objectDictMutex };
151 for (
const auto &observer : m_objectRemoveObserver) {
152 if (observer->serial() == serial)
153 removalObserversForObject.push_back(observer);
155 q20::erase_if(m_objectRemoveObserver, [&](
const SharedObjectRemoveObserver &element) {
156 return element->serial() == serial;
159 m_objectSerialDict.erase(id);
160 m_serialObjectDict.erase(*serial);
163 for (
const SharedObjectRemoveObserver &element : removalObserversForObject)
164 emit element->objectRemoved();
167 std::lock_guard guard{ m_pendingRecordsMutex };
169 m_pendingRecords.removeRecordsForObject(*serial);
170 m_pendingRecords.m_removals.push_back(*serial);
173 startCompressionTimer();
198 Q_ASSERT(
this->thread()->isCurrentThread());
200 PendingRecords pendingRecords = [&] {
201 std::lock_guard guard{ m_pendingRecordsMutex };
202 PendingRecords resolvedRecords;
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);
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);
223 takeFullyResolvedRecords(m_pendingRecords.m_sources, resolvedRecords.m_sources);
224 takeFullyResolvedRecords(m_pendingRecords.m_sinks, resolvedRecords.m_sinks);
226 return resolvedRecords;
230 [](std::variant<QByteArray, NoDefaultDeviceType> arg) -> std::optional<QByteArray> {
231 if (std::holds_alternative<NoDefaultDeviceType>(arg))
234 return std::get<QByteArray>(arg);
237 bool defaultSourceChanged = pendingRecords.m_defaultSource.has_value();
238 if (defaultSourceChanged)
239 m_defaultSourceName = getNodeName(*pendingRecords.m_defaultSource);
241 bool defaultSinkChanged = pendingRecords.m_defaultSink.has_value();
242 if (defaultSinkChanged)
243 m_defaultSinkName = getNodeName(*pendingRecords.m_defaultSink);
245 if (!pendingRecords.m_sources.empty() || !pendingRecords.m_removals.empty()
246 || defaultSourceChanged)
247 updateSources(std::move(pendingRecords.m_sources), pendingRecords.m_removals);
249 if (!pendingRecords.m_sinks.empty() || !pendingRecords.m_removals.empty() || defaultSinkChanged)
250 updateSinks(std::move(pendingRecords.m_sinks), pendingRecords.m_removals);
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;
267 QReadLocker guard(&m_mutex);
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;
274 if (it == records.end())
281 return findNodeSerialForNodeName<Direction::sink>(nodeName);
287 return findNodeSerialForNodeName<Direction::source>(nodeName);
292 QSpan<
const ObjectSerial> removedObjects)
294 QWriteLocker guard(&m_mutex);
296 std::vector<NodeRecord> &sinksOrSources = Mode == Direction::sink ? m_sinks : m_sources;
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;
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();
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>) {
317 return format != SPA_AUDIO_IEC958_CODEC_PCM;
320 }, arg->sampleTypes);
321 return isIEC61937EncapsulatedDevice;
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;
333 if (results.size() > 1) {
334 qCDebug(lcPipewireDeviceMonitor)
335 <<
"Multiple formats supported by node, prefer non-iec958: format"
339 if (!results.empty()) {
340 sinksOrSources.push_back(NodeRecord{
343 std::move(record.properties),
344 std::move(*results[0]),
347 qDebug(lcPipewireDeviceMonitor)
348 <<
"Could not resolve audio format for" << record.serial;
352 QList<QAudioDevice> oldDeviceList =
353 Mode == Direction::sink ? m_sinkDeviceList : m_sourceDeviceList;
355 const std::optional<QByteArray> &defaultSinkOrSourceNodeNameBA =
356 Mode == Direction::sink ? m_defaultSinkName : m_defaultSourceName;
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()),
368 QList<QAudioDevice> newDeviceList;
371 for (NodeRecord &sinkOrSource : sinksOrSources) {
372 ObjectSerial deviceSerial = sinkOrSource.deviceSerial;
374 auto deviceIt = m_devices.find(deviceSerial);
375 if (deviceIt == m_devices.end()) {
376 qDebug(lcPipewireDeviceMonitor) <<
"No device for device id" << deviceSerial;
380 std::optional<std::string_view> nodeName = getNodeName(sinkOrSource.properties);
381 bool isDefault = (defaultSinkOrSourceNodeName == nodeName);
383 auto devicePrivate = std::make_unique<QPipewireAudioDevicePrivate>(
384 sinkOrSource.properties, deviceIt->second.properties, sinkOrSource.format,
385 QAudioDevice::Mode::Output, isDefault);
387 QAudioDevice device = QAudioDevicePrivate::createQAudioDevice(std::move(devicePrivate));
389 newDeviceList.push_back(device);
391 qDebug(lcPipewireDeviceMonitor) <<
"adding device" << deviceIt->second.properties;
395 std::sort(newDeviceList.begin(), newDeviceList.end(),
396 [](
const QAudioDevice &lhs,
const QAudioDevice &rhs) {
397 return lhs.description() < rhs.description();
402 bool deviceListsEqual = ranges::equal(oldDeviceList, newDeviceList,
403 [](
const QAudioDevice &lhs,
const QAudioDevice &rhs) {
404 return (lhs.id() == rhs.id()) && (lhs.isDefault() == rhs.isDefault());
407 if (!deviceListsEqual) {
408 qDebug(lcPipewireDeviceMonitor) <<
"updated device list";
410 if constexpr (Mode == Direction::sink) {
411 m_sinkDeviceList = newDeviceList;
412 emit audioSinksChanged(m_sinkDeviceList);
414 m_sourceDeviceList = newDeviceList;
415 emit audioSourcesChanged(m_sourceDeviceList);
421 QSpan<
const ObjectSerial> removedObjects)
423 updateSourcesOrSinks<Direction::sink>(
std::move(addedNodes), removedObjects);
427 QSpan<
const ObjectSerial> removedObjects)
429 updateSourcesOrSinks<Direction::source>(
std::move(addedNodes), removedObjects);
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;
438 if (it == m_devices.end())
445 QReadLocker lock{ &m_objectDictMutex };
447 auto it = m_serialObjectDict.find(serial);
448 if (it != m_serialObjectDict.end())
455 QReadLocker lock{ &m_objectDictMutex };
457 auto it = m_objectSerialDict.find(id);
458 if (it != m_objectSerialDict.end())
465 QWriteLocker lock{ &m_objectDictMutex };
467 if (m_serialObjectDict.find(observer->serial()) == m_serialObjectDict.end())
470 m_objectRemoveObserver.push_back(std::move(observer));
476 QWriteLocker lock{ &m_objectDictMutex };
478 q20::erase(m_objectRemoveObserver, observer);
490 std::lock_guard pendingRecordLock{
491 m_pendingRecordsMutex,
494 for (ObjectSerial removed : m_pendingRecords.m_removals)
495 m_pendingRecords.removeRecordsForObject(removed);
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();
503 if (allFormatsResolved(m_pendingRecords.m_sources)
504 && allFormatsResolved(m_pendingRecords.m_sinks))
509 audioDevicesChanged(verifyThreading);
511 QReadLocker lock{ &m_mutex };
513 .sources = m_sourceDeviceList,
514 .sinks = m_sinkDeviceList,
520 QMetaObject::invokeMethod(
this, [
this] {
521 if (m_compressionTimer.isActive())
523 m_compressionTimer.start();
529 PwPropertyDict properties):
537 std::move(properties),
542 auto promise = std::make_shared<QPromise<std::optional<SpaObjectAudioFormat>>>();
543 formatFuture = promise->future();
546 auto onParam = [promise](
int , uint32_t , uint32_t , uint32_t ,
547 const struct spa_pod *param)
mutable {
549 promise->addResult(format);
554 PwNodeHandle nodeProxy = context->bindNode(object);
556 enumFormatListener = std::make_unique<NodeEventListener>(std::move(nodeProxy),
557 NodeEventListener::NodeHandler{
562 enumFormatListener->enumParams(SPA_PARAM_EnumFormat);
567 enumFormatDoneListener = std::make_unique<CoreEventDoneListener>();
568 enumFormatDoneListener->asyncWait(context->coreConnection().get(), [promise] {
578#include "moc_qpipewire_audiodevicemonitor_p.cpp"
ObjectSerial serial() const
static QAudioContextManager * instance()
static bool isInPwThreadLoop()
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 objectRemoved(ObjectId)
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