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 <qsocketnotifier.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include <spa/utils/defs.h>
#include <spa/utils/hook.h>
@ -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<PwCore*>(data);
emit self->synced(id, seq);
}
SpaHook::SpaHook() { // NOLINT
spa_zero(this->hook);
}

View file

@ -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 <typename T>
@ -49,12 +64,4 @@ public:
T* object;
};
class SpaHook {
public:
explicit SpaHook();
void remove();
spa_hook hook;
};
} // namespace qs::service::pipewire

View file

@ -24,6 +24,8 @@
#include <spa/utils/keys.h>
#include <spa/utils/type.h>
#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(

View file

@ -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;
};

View file

@ -2,6 +2,7 @@
#include <qcontainerfwd.h>
#include <qlist.h>
#include <qnamespace.h>
#include <qobject.h>
#include <qqmllist.h>
#include <qtmetamacros.h>
@ -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<PwNodeIface>* 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<float>& 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<PwNodeBoundAudio*>(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()) {

View file

@ -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;

View file

@ -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 = {

View file

@ -116,6 +116,8 @@ class PwRegistry
public:
void init(PwCore& core);
[[nodiscard]] bool isInitialized() const { return this->initState == InitState::Done; }
//QHash<quint32, PwClient*> clients;
QHash<quint32, PwMetadata*> metadata;
QHash<quint32, PwNode*> 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;
};