From 6d8022b709ac74f27bbdcb652bc803daf510e9e6 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 14 Jan 2025 15:08:29 -0800 Subject: [PATCH] service/pipewire: add registry and node ready properties --- src/services/pipewire/core.cpp | 26 ++++++++++++++++++++++++++ src/services/pipewire/core.hpp | 23 +++++++++++++++-------- src/services/pipewire/node.cpp | 22 ++++++++++++++++++++++ src/services/pipewire/node.hpp | 6 ++++++ src/services/pipewire/qml.cpp | 16 ++++++++++++++++ src/services/pipewire/qml.hpp | 18 ++++++++++++++++++ src/services/pipewire/registry.cpp | 23 +++++++++++++++++++++++ src/services/pipewire/registry.hpp | 11 +++++++++++ 8 files changed, 137 insertions(+), 8 deletions(-) diff --git a/src/services/pipewire/core.cpp b/src/services/pipewire/core.cpp index c325bb33..9c2a3dbf 100644 --- a/src/services/pipewire/core.cpp +++ b/src/services/pipewire/core.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include @@ -19,6 +20,19 @@ namespace { Q_LOGGING_CATEGORY(logLoop, "quickshell.service.pipewire.loop", QtWarningMsg); } +const pw_core_events PwCore::EVENTS = { + .version = PW_VERSION_CORE_EVENTS, + .info = nullptr, + .done = &PwCore::onSync, + .ping = nullptr, + .error = nullptr, + .remove_id = nullptr, + .bound_id = nullptr, + .add_mem = nullptr, + .remove_mem = nullptr, + .bound_props = nullptr, +}; + PwCore::PwCore(QObject* parent): QObject(parent), notifier(QSocketNotifier::Read) { qCInfo(logLoop) << "Creating pipewire event loop."; pw_init(nullptr, nullptr); @@ -42,6 +56,8 @@ PwCore::PwCore(QObject* parent): QObject(parent), notifier(QSocketNotifier::Read return; } + pw_core_add_listener(this->core, &this->listener.hook, &PwCore::EVENTS, this); + qCInfo(logLoop) << "Linking pipewire event loop."; // Tie the pw event loop into qt. auto fd = pw_loop_get_fd(this->loop); @@ -79,6 +95,16 @@ void PwCore::poll() { emit this->polled(); } +qint32 PwCore::sync(quint32 id) const { + // Seq param doesn't seem to do anything. Seq is instead the returned value. + return pw_core_sync(this->core, id, 0); +} + +void PwCore::onSync(void* data, quint32 id, qint32 seq) { + auto* self = static_cast(data); + emit self->synced(id, seq); +} + SpaHook::SpaHook() { // NOLINT spa_zero(this->hook); } diff --git a/src/services/pipewire/core.hpp b/src/services/pipewire/core.hpp index 49728eeb..262e2d31 100644 --- a/src/services/pipewire/core.hpp +++ b/src/services/pipewire/core.hpp @@ -14,6 +14,14 @@ namespace qs::service::pipewire { +class SpaHook { +public: + explicit SpaHook(); + + void remove(); + spa_hook hook; +}; + class PwCore: public QObject { Q_OBJECT; @@ -23,6 +31,7 @@ public: Q_DISABLE_COPY_MOVE(PwCore); [[nodiscard]] bool isValid() const; + [[nodiscard]] qint32 sync(quint32 id) const; pw_loop* loop = nullptr; pw_context* context = nullptr; @@ -30,12 +39,18 @@ public: signals: void polled(); + void synced(quint32 id, qint32 seq); private slots: void poll(); private: + static const pw_core_events EVENTS; + + static void onSync(void* data, quint32 id, qint32 seq); + QSocketNotifier notifier; + SpaHook listener; }; template @@ -49,12 +64,4 @@ public: T* object; }; -class SpaHook { -public: - explicit SpaHook(); - - void remove(); - spa_hook hook; -}; - } // namespace qs::service::pipewire diff --git a/src/services/pipewire/node.cpp b/src/services/pipewire/node.cpp index ffb8c164..21815b82 100644 --- a/src/services/pipewire/node.cpp +++ b/src/services/pipewire/node.cpp @@ -24,6 +24,8 @@ #include #include +#include "connection.hpp" +#include "core.hpp" #include "device.hpp" namespace qs::service::pipewire { @@ -92,6 +94,12 @@ void PwNode::bindHooks() { } void PwNode::unbindHooks() { + if (this->ready) { + this->ready = false; + emit this->readyChanged(); + } + + this->syncSeq = 0; this->listener.remove(); this->routeDevice = -1; this->properties.clear(); @@ -201,6 +209,20 @@ void PwNode::onInfo(void* data, const pw_node_info* info) { if (self->boundData != nullptr) { self->boundData->onInfo(info); } + + if (!self->ready && !self->syncSeq) { + auto* core = PwConnection::instance()->registry.core; + QObject::connect(core, &PwCore::synced, self, &PwNode::onCoreSync); + self->syncSeq = core->sync(self->id); + } +} + +void PwNode::onCoreSync(quint32 id, qint32 seq) { + if (id != this->id || seq != this->syncSeq) return; + qCInfo(logNode) << "Completed initial sync for" << this; + this->ready = true; + this->syncSeq = 0; + emit this->readyChanged(); } void PwNode::onParam( diff --git a/src/services/pipewire/node.hpp b/src/services/pipewire/node.hpp index 697a54e0..a18abccf 100644 --- a/src/services/pipewire/node.hpp +++ b/src/services/pipewire/node.hpp @@ -172,6 +172,7 @@ public: PwNodeType type = PwNodeType::Untracked; bool isSink = false; bool isStream = false; + bool ready = false; PwNodeBoundData* boundData = nullptr; @@ -180,6 +181,10 @@ public: signals: void propertiesChanged(); + void readyChanged(); + +private slots: + void onCoreSync(quint32 id, qint32 seq); private: static const pw_node_events EVENTS; @@ -187,6 +192,7 @@ private: static void onParam(void* data, qint32 seq, quint32 id, quint32 index, quint32 next, const spa_pod* param); + qint32 syncSeq = 0; SpaHook listener; }; diff --git a/src/services/pipewire/qml.cpp b/src/services/pipewire/qml.cpp index be50ec6e..6eef238b 100644 --- a/src/services/pipewire/qml.cpp +++ b/src/services/pipewire/qml.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -87,6 +88,16 @@ Pipewire::Pipewire(QObject* parent): QObject(parent) { this, &Pipewire::defaultConfiguredAudioSourceChanged ); + + if (!connection->registry.isInitialized()) { + QObject::connect( + &connection->registry, + &PwRegistry::initialized, + this, + &Pipewire::readyChanged, + Qt::SingleShotConnection + ); + } } ObjectModel* Pipewire::nodes() { return &this->mNodes; } @@ -156,6 +167,8 @@ void Pipewire::setDefaultConfiguredAudioSource(PwNodeIface* node) { PwConnection::instance()->defaults.changeConfiguredSource(node ? node->node() : nullptr); } +bool Pipewire::isReady() { return PwConnection::instance()->registry.isInitialized(); } + PwNodeIface* PwNodeLinkTracker::node() const { return this->mNode; } void PwNodeLinkTracker::setNode(PwNodeIface* node) { @@ -298,6 +311,7 @@ void PwNodeAudioIface::setVolumes(const QVector& volumes) { PwNodeIface::PwNodeIface(PwNode* node): PwObjectIface(node), mNode(node) { QObject::connect(node, &PwNode::propertiesChanged, this, &PwNodeIface::propertiesChanged); + QObject::connect(node, &PwNode::readyChanged, this, &PwNodeIface::readyChanged); if (auto* audioBoundData = dynamic_cast(node->boundData)) { this->audioIface = new PwNodeAudioIface(audioBoundData, this); @@ -318,6 +332,8 @@ bool PwNodeIface::isSink() const { return this->mNode->isSink; } bool PwNodeIface::isStream() const { return this->mNode->isStream; } +bool PwNodeIface::isReady() const { return this->mNode->ready; } + QVariantMap PwNodeIface::properties() const { auto map = QVariantMap(); for (auto [k, v]: this->mNode->properties.asKeyValueRange()) { diff --git a/src/services/pipewire/qml.hpp b/src/services/pipewire/qml.hpp index 675b923b..6313a42b 100644 --- a/src/services/pipewire/qml.hpp +++ b/src/services/pipewire/qml.hpp @@ -116,6 +116,13 @@ class Pipewire: public QObject { /// /// See @@defaultAudioSource for the current default source, regardless of preference. Q_PROPERTY(qs::service::pipewire::PwNodeIface* preferredDefaultAudioSource READ defaultConfiguredAudioSource WRITE setDefaultConfiguredAudioSource NOTIFY defaultConfiguredAudioSourceChanged); + /// This property is true if quickshell has completed its initial sync with + /// the pipewire server. If true, nodes, links and sync/source preferences will be + /// in a good state. + /// + /// > [!NOTE] You can use the pipewire object before it is ready, but some nodes/links + /// > may be missing, and preference metadata may be null. + Q_PROPERTY(bool ready READ isReady NOTIFY readyChanged); // clang-format on QML_ELEMENT; QML_SINGLETON; @@ -136,6 +143,8 @@ public: [[nodiscard]] PwNodeIface* defaultConfiguredAudioSource() const; static void setDefaultConfiguredAudioSource(PwNodeIface* node); + [[nodiscard]] static bool isReady(); + signals: void defaultAudioSinkChanged(); void defaultAudioSourceChanged(); @@ -143,6 +152,8 @@ signals: void defaultConfiguredAudioSinkChanged(); void defaultConfiguredAudioSourceChanged(); + void readyChanged(); + private slots: void onNodeAdded(PwNode* node); void onNodeRemoved(QObject* object); @@ -294,6 +305,11 @@ class PwNodeIface: public PwObjectIface { /// The presence or absence of this property can be used to determine if a node /// manages audio, regardless of if it is bound. If non null, the node is an audio node. Q_PROPERTY(qs::service::pipewire::PwNodeAudioIface* audio READ audio CONSTANT); + /// True if the node is fully bound and ready to use. + /// + /// > [!NOTE] The node may be used before it is fully bound, but some data + /// > may be missing or incorrect. + Q_PROPERTY(bool ready READ isReady NOTIFY readyChanged); QML_NAMED_ELEMENT(PwNode); QML_UNCREATABLE("PwNodes cannot be created directly"); @@ -307,6 +323,7 @@ public: [[nodiscard]] QString nickname() const; [[nodiscard]] bool isSink() const; [[nodiscard]] bool isStream() const; + [[nodiscard]] bool isReady() const; [[nodiscard]] QVariantMap properties() const; [[nodiscard]] PwNodeAudioIface* audio() const; @@ -314,6 +331,7 @@ public: signals: void propertiesChanged(); + void readyChanged(); private: PwNode* mNode; diff --git a/src/services/pipewire/registry.cpp b/src/services/pipewire/registry.cpp index 04bd9ace..d2967d03 100644 --- a/src/services/pipewire/registry.cpp +++ b/src/services/pipewire/registry.cpp @@ -126,6 +126,29 @@ void PwRegistry::init(PwCore& core) { this->core = &core; this->object = pw_core_get_registry(core.core, PW_VERSION_REGISTRY, 0); pw_registry_add_listener(this->object, &this->listener.hook, &PwRegistry::EVENTS, this); + + QObject::connect(this->core, &PwCore::synced, this, &PwRegistry::onCoreSync); + + qCDebug(logRegistry) << "Registry created. Sending core sync for initial object tracking."; + this->coreSyncSeq = this->core->sync(PW_ID_CORE); +} + +void PwRegistry::onCoreSync(quint32 id, qint32 seq) { + if (id != PW_ID_CORE || seq != this->coreSyncSeq) return; + + switch (this->initState) { + case InitState::SendingObjects: + qCDebug(logRegistry) << "Initial sync for objects received. Syncing for metadata binding."; + this->coreSyncSeq = this->core->sync(PW_ID_CORE); + this->initState = InitState::Binding; + break; + case InitState::Binding: + qCInfo(logRegistry) << "Initial state sync complete."; + this->initState = InitState::Done; + emit this->initialized(); + break; + default: break; + } } const pw_registry_events PwRegistry::EVENTS = { diff --git a/src/services/pipewire/registry.hpp b/src/services/pipewire/registry.hpp index be282a20..f1ba9610 100644 --- a/src/services/pipewire/registry.hpp +++ b/src/services/pipewire/registry.hpp @@ -116,6 +116,8 @@ class PwRegistry public: void init(PwCore& core); + [[nodiscard]] bool isInitialized() const { return this->initState == InitState::Done; } + //QHash clients; QHash metadata; QHash nodes; @@ -132,9 +134,11 @@ signals: void linkAdded(PwLink* link); void linkGroupAdded(PwLinkGroup* group); void metadataAdded(PwMetadata* metadata); + void initialized(); private slots: void onLinkGroupDestroyed(QObject* object); + void onCoreSync(quint32 id, qint32 seq); private: static const pw_registry_events EVENTS; @@ -152,6 +156,13 @@ private: void addLinkToGroup(PwLink* link); + enum class InitState : quint8 { + SendingObjects, + Binding, + Done + } initState = InitState::SendingObjects; + + qint32 coreSyncSeq = 0; SpaHook listener; };