service/pipewire: add registry and node ready properties

This commit is contained in:
outfoxxed 2025-01-14 15:08:29 -08:00
parent 8b6aa624a2
commit 6d8022b709
Signed by: outfoxxed
GPG key ID: 4C88A185FB89301E
8 changed files with 137 additions and 8 deletions

View file

@ -10,6 +10,7 @@
#include <qobject.h> #include <qobject.h>
#include <qsocketnotifier.h> #include <qsocketnotifier.h>
#include <qtmetamacros.h> #include <qtmetamacros.h>
#include <qtypes.h>
#include <spa/utils/defs.h> #include <spa/utils/defs.h>
#include <spa/utils/hook.h> #include <spa/utils/hook.h>
@ -19,6 +20,19 @@ namespace {
Q_LOGGING_CATEGORY(logLoop, "quickshell.service.pipewire.loop", QtWarningMsg); 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) { PwCore::PwCore(QObject* parent): QObject(parent), notifier(QSocketNotifier::Read) {
qCInfo(logLoop) << "Creating pipewire event loop."; qCInfo(logLoop) << "Creating pipewire event loop.";
pw_init(nullptr, nullptr); pw_init(nullptr, nullptr);
@ -42,6 +56,8 @@ PwCore::PwCore(QObject* parent): QObject(parent), notifier(QSocketNotifier::Read
return; return;
} }
pw_core_add_listener(this->core, &this->listener.hook, &PwCore::EVENTS, this);
qCInfo(logLoop) << "Linking pipewire event loop."; qCInfo(logLoop) << "Linking pipewire event loop.";
// Tie the pw event loop into qt. // Tie the pw event loop into qt.
auto fd = pw_loop_get_fd(this->loop); auto fd = pw_loop_get_fd(this->loop);
@ -79,6 +95,16 @@ void PwCore::poll() {
emit this->polled(); 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<PwCore*>(data);
emit self->synced(id, seq);
}
SpaHook::SpaHook() { // NOLINT SpaHook::SpaHook() { // NOLINT
spa_zero(this->hook); spa_zero(this->hook);
} }

View file

@ -14,6 +14,14 @@
namespace qs::service::pipewire { namespace qs::service::pipewire {
class SpaHook {
public:
explicit SpaHook();
void remove();
spa_hook hook;
};
class PwCore: public QObject { class PwCore: public QObject {
Q_OBJECT; Q_OBJECT;
@ -23,6 +31,7 @@ public:
Q_DISABLE_COPY_MOVE(PwCore); Q_DISABLE_COPY_MOVE(PwCore);
[[nodiscard]] bool isValid() const; [[nodiscard]] bool isValid() const;
[[nodiscard]] qint32 sync(quint32 id) const;
pw_loop* loop = nullptr; pw_loop* loop = nullptr;
pw_context* context = nullptr; pw_context* context = nullptr;
@ -30,12 +39,18 @@ public:
signals: signals:
void polled(); void polled();
void synced(quint32 id, qint32 seq);
private slots: private slots:
void poll(); void poll();
private: private:
static const pw_core_events EVENTS;
static void onSync(void* data, quint32 id, qint32 seq);
QSocketNotifier notifier; QSocketNotifier notifier;
SpaHook listener;
}; };
template <typename T> template <typename T>
@ -49,12 +64,4 @@ public:
T* object; T* object;
}; };
class SpaHook {
public:
explicit SpaHook();
void remove();
spa_hook hook;
};
} // namespace qs::service::pipewire } // namespace qs::service::pipewire

View file

@ -24,6 +24,8 @@
#include <spa/utils/keys.h> #include <spa/utils/keys.h>
#include <spa/utils/type.h> #include <spa/utils/type.h>
#include "connection.hpp"
#include "core.hpp"
#include "device.hpp" #include "device.hpp"
namespace qs::service::pipewire { namespace qs::service::pipewire {
@ -92,6 +94,12 @@ void PwNode::bindHooks() {
} }
void PwNode::unbindHooks() { void PwNode::unbindHooks() {
if (this->ready) {
this->ready = false;
emit this->readyChanged();
}
this->syncSeq = 0;
this->listener.remove(); this->listener.remove();
this->routeDevice = -1; this->routeDevice = -1;
this->properties.clear(); this->properties.clear();
@ -201,6 +209,20 @@ void PwNode::onInfo(void* data, const pw_node_info* info) {
if (self->boundData != nullptr) { if (self->boundData != nullptr) {
self->boundData->onInfo(info); 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( void PwNode::onParam(

View file

@ -172,6 +172,7 @@ public:
PwNodeType type = PwNodeType::Untracked; PwNodeType type = PwNodeType::Untracked;
bool isSink = false; bool isSink = false;
bool isStream = false; bool isStream = false;
bool ready = false;
PwNodeBoundData* boundData = nullptr; PwNodeBoundData* boundData = nullptr;
@ -180,6 +181,10 @@ public:
signals: signals:
void propertiesChanged(); void propertiesChanged();
void readyChanged();
private slots:
void onCoreSync(quint32 id, qint32 seq);
private: private:
static const pw_node_events EVENTS; static const pw_node_events EVENTS;
@ -187,6 +192,7 @@ private:
static void static void
onParam(void* data, qint32 seq, quint32 id, quint32 index, quint32 next, const spa_pod* param); onParam(void* data, qint32 seq, quint32 id, quint32 index, quint32 next, const spa_pod* param);
qint32 syncSeq = 0;
SpaHook listener; SpaHook listener;
}; };

View file

@ -2,6 +2,7 @@
#include <qcontainerfwd.h> #include <qcontainerfwd.h>
#include <qlist.h> #include <qlist.h>
#include <qnamespace.h>
#include <qobject.h> #include <qobject.h>
#include <qqmllist.h> #include <qqmllist.h>
#include <qtmetamacros.h> #include <qtmetamacros.h>
@ -87,6 +88,16 @@ Pipewire::Pipewire(QObject* parent): QObject(parent) {
this, this,
&Pipewire::defaultConfiguredAudioSourceChanged &Pipewire::defaultConfiguredAudioSourceChanged
); );
if (!connection->registry.isInitialized()) {
QObject::connect(
&connection->registry,
&PwRegistry::initialized,
this,
&Pipewire::readyChanged,
Qt::SingleShotConnection
);
}
} }
ObjectModel<PwNodeIface>* Pipewire::nodes() { return &this->mNodes; } ObjectModel<PwNodeIface>* Pipewire::nodes() { return &this->mNodes; }
@ -156,6 +167,8 @@ void Pipewire::setDefaultConfiguredAudioSource(PwNodeIface* node) {
PwConnection::instance()->defaults.changeConfiguredSource(node ? node->node() : nullptr); PwConnection::instance()->defaults.changeConfiguredSource(node ? node->node() : nullptr);
} }
bool Pipewire::isReady() { return PwConnection::instance()->registry.isInitialized(); }
PwNodeIface* PwNodeLinkTracker::node() const { return this->mNode; } PwNodeIface* PwNodeLinkTracker::node() const { return this->mNode; }
void PwNodeLinkTracker::setNode(PwNodeIface* node) { void PwNodeLinkTracker::setNode(PwNodeIface* node) {
@ -298,6 +311,7 @@ void PwNodeAudioIface::setVolumes(const QVector<float>& volumes) {
PwNodeIface::PwNodeIface(PwNode* node): PwObjectIface(node), mNode(node) { PwNodeIface::PwNodeIface(PwNode* node): PwObjectIface(node), mNode(node) {
QObject::connect(node, &PwNode::propertiesChanged, this, &PwNodeIface::propertiesChanged); QObject::connect(node, &PwNode::propertiesChanged, this, &PwNodeIface::propertiesChanged);
QObject::connect(node, &PwNode::readyChanged, this, &PwNodeIface::readyChanged);
if (auto* audioBoundData = dynamic_cast<PwNodeBoundAudio*>(node->boundData)) { if (auto* audioBoundData = dynamic_cast<PwNodeBoundAudio*>(node->boundData)) {
this->audioIface = new PwNodeAudioIface(audioBoundData, this); 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::isStream() const { return this->mNode->isStream; }
bool PwNodeIface::isReady() const { return this->mNode->ready; }
QVariantMap PwNodeIface::properties() const { QVariantMap PwNodeIface::properties() const {
auto map = QVariantMap(); auto map = QVariantMap();
for (auto [k, v]: this->mNode->properties.asKeyValueRange()) { for (auto [k, v]: this->mNode->properties.asKeyValueRange()) {

View file

@ -116,6 +116,13 @@ class Pipewire: public QObject {
/// ///
/// See @@defaultAudioSource for the current default source, regardless of preference. /// See @@defaultAudioSource for the current default source, regardless of preference.
Q_PROPERTY(qs::service::pipewire::PwNodeIface* preferredDefaultAudioSource READ defaultConfiguredAudioSource WRITE setDefaultConfiguredAudioSource NOTIFY defaultConfiguredAudioSourceChanged); 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 // clang-format on
QML_ELEMENT; QML_ELEMENT;
QML_SINGLETON; QML_SINGLETON;
@ -136,6 +143,8 @@ public:
[[nodiscard]] PwNodeIface* defaultConfiguredAudioSource() const; [[nodiscard]] PwNodeIface* defaultConfiguredAudioSource() const;
static void setDefaultConfiguredAudioSource(PwNodeIface* node); static void setDefaultConfiguredAudioSource(PwNodeIface* node);
[[nodiscard]] static bool isReady();
signals: signals:
void defaultAudioSinkChanged(); void defaultAudioSinkChanged();
void defaultAudioSourceChanged(); void defaultAudioSourceChanged();
@ -143,6 +152,8 @@ signals:
void defaultConfiguredAudioSinkChanged(); void defaultConfiguredAudioSinkChanged();
void defaultConfiguredAudioSourceChanged(); void defaultConfiguredAudioSourceChanged();
void readyChanged();
private slots: private slots:
void onNodeAdded(PwNode* node); void onNodeAdded(PwNode* node);
void onNodeRemoved(QObject* object); 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 /// 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. /// 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); 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_NAMED_ELEMENT(PwNode);
QML_UNCREATABLE("PwNodes cannot be created directly"); QML_UNCREATABLE("PwNodes cannot be created directly");
@ -307,6 +323,7 @@ public:
[[nodiscard]] QString nickname() const; [[nodiscard]] QString nickname() const;
[[nodiscard]] bool isSink() const; [[nodiscard]] bool isSink() const;
[[nodiscard]] bool isStream() const; [[nodiscard]] bool isStream() const;
[[nodiscard]] bool isReady() const;
[[nodiscard]] QVariantMap properties() const; [[nodiscard]] QVariantMap properties() const;
[[nodiscard]] PwNodeAudioIface* audio() const; [[nodiscard]] PwNodeAudioIface* audio() const;
@ -314,6 +331,7 @@ public:
signals: signals:
void propertiesChanged(); void propertiesChanged();
void readyChanged();
private: private:
PwNode* mNode; PwNode* mNode;

View file

@ -126,6 +126,29 @@ void PwRegistry::init(PwCore& core) {
this->core = &core; this->core = &core;
this->object = pw_core_get_registry(core.core, PW_VERSION_REGISTRY, 0); this->object = pw_core_get_registry(core.core, PW_VERSION_REGISTRY, 0);
pw_registry_add_listener(this->object, &this->listener.hook, &PwRegistry::EVENTS, this); 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 = { const pw_registry_events PwRegistry::EVENTS = {

View file

@ -116,6 +116,8 @@ class PwRegistry
public: public:
void init(PwCore& core); void init(PwCore& core);
[[nodiscard]] bool isInitialized() const { return this->initState == InitState::Done; }
//QHash<quint32, PwClient*> clients; //QHash<quint32, PwClient*> clients;
QHash<quint32, PwMetadata*> metadata; QHash<quint32, PwMetadata*> metadata;
QHash<quint32, PwNode*> nodes; QHash<quint32, PwNode*> nodes;
@ -132,9 +134,11 @@ signals:
void linkAdded(PwLink* link); void linkAdded(PwLink* link);
void linkGroupAdded(PwLinkGroup* group); void linkGroupAdded(PwLinkGroup* group);
void metadataAdded(PwMetadata* metadata); void metadataAdded(PwMetadata* metadata);
void initialized();
private slots: private slots:
void onLinkGroupDestroyed(QObject* object); void onLinkGroupDestroyed(QObject* object);
void onCoreSync(quint32 id, qint32 seq);
private: private:
static const pw_registry_events EVENTS; static const pw_registry_events EVENTS;
@ -152,6 +156,13 @@ private:
void addLinkToGroup(PwLink* link); void addLinkToGroup(PwLink* link);
enum class InitState : quint8 {
SendingObjects,
Binding,
Done
} initState = InitState::SendingObjects;
qint32 coreSyncSeq = 0;
SpaHook listener; SpaHook listener;
}; };