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 <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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue