Revision control
Copy as Markdown
// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
#ifndef QPIPEWIRE_AUDIODEVICEMONITOR_P_H
#define QPIPEWIRE_AUDIODEVICEMONITOR_P_H
//
// W A R N I N G
// -------------
//
// This file is not part of the Qt API. It exists purely as an
// implementation detail. This header file may change from version to
// version without notice, or even be removed.
//
// We mean it.
//
#include <QtMultimedia/private/qpipewire_async_support_p.h>
#include <QtMultimedia/private/qpipewire_propertydict_p.h>
#include <QtMultimedia/private/qpipewire_registry_support_p.h>
#include <QtMultimedia/private/qpipewire_spa_pod_support_p.h>
#include <QtMultimedia/qaudiodevice.h>
#include <QtCore/qchronotimer.h>
#include <QtCore/qfuture.h>
#include <QtCore/qreadwritelock.h>
#include <list>
#include <vector>
#include <pipewire/pipewire.h>
QT_BEGIN_NAMESPACE
namespace QtPipeWire {
class ObjectRemoveObserver : public QObject
{
Q_OBJECT
public:
explicit ObjectRemoveObserver(ObjectSerial objectSerial);
ObjectSerial serial() const;
Q_SIGNALS:
void objectRemoved();
private:
const ObjectSerial m_observedSerial;
};
using SharedObjectRemoveObserver = std::shared_ptr<ObjectRemoveObserver>;
// TODO: can we make use of COW here?
class QAudioDeviceMonitor : public QObject
{
Q_OBJECT
enum class Direction : uint8_t
{
sink,
source,
};
public:
QAudioDeviceMonitor();
~QAudioDeviceMonitor();
void objectAdded(ObjectId, uint32_t permissions, PipewireRegistryType, uint32_t version,
const spa_dict &props);
void objectRemoved(ObjectId);
struct NoDefaultDeviceType
{
};
static constexpr NoDefaultDeviceType NoDefaultDevice{};
void setDefaultAudioSink(std::variant<QByteArray, NoDefaultDeviceType>);
void setDefaultAudioSource(std::variant<QByteArray, NoDefaultDeviceType>);
std::optional<ObjectSerial> findSinkNodeSerial(std::string_view nodeName) const;
std::optional<ObjectSerial> findSourceNodeSerial(std::string_view nodeName) const;
// ObjectId/ObjectSerial mapping
std::optional<ObjectId> findObjectId(ObjectSerial) const;
std::optional<ObjectSerial> findObjectSerial(ObjectId) const;
[[nodiscard]] bool registerObserver(SharedObjectRemoveObserver);
void unregisterObserver(const SharedObjectRemoveObserver &);
void clearAllObservers();
// Obtaining device lists
struct DeviceLists
{
QList<QAudioDevice> sources;
QList<QAudioDevice> sinks;
};
DeviceLists getDeviceLists(bool verifyThreading = true);
Q_SIGNALS:
void audioSinksChanged(QList<QAudioDevice>);
void audioSourcesChanged(QList<QAudioDevice>);
private:
struct DeviceRecord
{
ObjectSerial serial;
PwPropertyDict properties;
};
struct PendingNodeRecord
{
PendingNodeRecord(ObjectId, ObjectSerial serial, std::optional<ObjectSerial> deviceSerial,
PwPropertyDict properties);
ObjectSerial serial;
std::optional<ObjectSerial> deviceSerial; // may be nullopt for virtual nodes
PwPropertyDict properties;
std::unique_ptr<NodeEventListener> enumFormatListener;
std::unique_ptr<CoreEventDoneListener> enumFormatDoneListener;
QFuture<std::vector<SpaObjectAudioFormat>> formatFuture;
// owned by he instance, updated in the formatFuture's continuation (we capture a weak reference in the
// continuation to avoid lifetime issues
const std::shared_ptr<std::optional<std::vector<SpaObjectAudioFormat>>> formatResults;
};
struct NodeRecord
{
ObjectSerial serial;
std::optional<ObjectSerial> deviceSerial; // may be nullopt for virtual nodes
PwPropertyDict properties;
SpaObjectAudioFormat format;
};
// discovered, but format not resolved
struct PendingRecords
{
std::list<PendingNodeRecord> m_sources;
std::list<PendingNodeRecord> m_sinks;
std::vector<ObjectSerial> m_removals;
std::optional<std::variant<QByteArray, NoDefaultDeviceType>> m_defaultSource;
std::optional<std::variant<QByteArray, NoDefaultDeviceType>> m_defaultSink;
void removeRecordsForObject(ObjectSerial);
};
QMutex m_pendingRecordsMutex;
PendingRecords m_pendingRecords QT_MM_GUARDED_BY(m_pendingRecordsMutex);
// discovered, format resolved. living on application thread
mutable QReadWriteLock m_mutex;
std::map<ObjectSerial, DeviceRecord> m_devices QT_MM_GUARDED_BY(m_mutex);
std::vector<NodeRecord> m_sources QT_MM_GUARDED_BY(m_mutex);
std::vector<NodeRecord> m_sinks QT_MM_GUARDED_BY(m_mutex);
std::optional<QByteArray> m_defaultSourceName;
std::optional<QByteArray> m_defaultSinkName;
QThread m_compressionTimerThread{ this };
QChronoTimer m_compressionTimer;
void startCompressionTimer();
// Device list updates
void audioDevicesChanged(bool verifyThreading = true);
void updateSources(std::list<PendingNodeRecord> addedNodes,
QSpan<const ObjectSerial> removedObjects);
void updateSinks(std::list<PendingNodeRecord> addedNodes,
QSpan<const ObjectSerial> removedObjects);
template <Direction>
void updateSourcesOrSinks(std::list<PendingNodeRecord>, QSpan<const ObjectSerial>);
QList<QAudioDevice> m_sourceDeviceList;
QList<QAudioDevice> m_sinkDeviceList;
std::optional<ObjectSerial> findDeviceSerial(std::string_view deviceName) const;
template <Direction>
std::optional<ObjectSerial> findNodeSerialForNodeName(std::string_view nodeName) const;
// ObjectId/ObjectSerial mapping
// CHECK: can we completely rely on only accessing ObjectId under the pipewire event loop lock?
mutable QReadWriteLock m_objectDictMutex;
std::map<ObjectId, ObjectSerial> m_objectSerialDict QT_MM_GUARDED_BY(m_objectDictMutex);
std::map<ObjectSerial, ObjectId> m_serialObjectDict QT_MM_GUARDED_BY(m_objectDictMutex);
std::vector<SharedObjectRemoveObserver>
m_objectRemoveObserver QT_MM_GUARDED_BY(m_objectDictMutex);
};
} // namespace QtPipeWire
QT_END_NAMESPACE
#endif // QPIPEWIRE_AUDIODEVICEMONITOR_P_H