forked from quickshell/quickshell
service/pipewire: add registry and node ready properties
This commit is contained in:
parent
8b6aa624a2
commit
6d8022b709
8 changed files with 137 additions and 8 deletions
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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()) {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue