Skip to content

Commit 2c4ef64

Browse files
committed
service/pipewire: add registry and node ready properties
1 parent 8b6aa62 commit 2c4ef64

File tree

8 files changed

+137
-8
lines changed

8 files changed

+137
-8
lines changed

src/services/pipewire/core.cpp

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
#include <qobject.h>
1111
#include <qsocketnotifier.h>
1212
#include <qtmetamacros.h>
13+
#include <qtypes.h>
1314
#include <spa/utils/defs.h>
1415
#include <spa/utils/hook.h>
1516

@@ -19,6 +20,19 @@ namespace {
1920
Q_LOGGING_CATEGORY(logLoop, "quickshell.service.pipewire.loop", QtWarningMsg);
2021
}
2122

23+
const pw_core_events PwCore::EVENTS = {
24+
.version = PW_VERSION_CORE_EVENTS,
25+
.info = nullptr,
26+
.done = &PwCore::onSync,
27+
.ping = nullptr,
28+
.error = nullptr,
29+
.remove_id = nullptr,
30+
.bound_id = nullptr,
31+
.add_mem = nullptr,
32+
.remove_mem = nullptr,
33+
.bound_props = nullptr,
34+
};
35+
2236
PwCore::PwCore(QObject* parent): QObject(parent), notifier(QSocketNotifier::Read) {
2337
qCInfo(logLoop) << "Creating pipewire event loop.";
2438
pw_init(nullptr, nullptr);
@@ -42,6 +56,8 @@ PwCore::PwCore(QObject* parent): QObject(parent), notifier(QSocketNotifier::Read
4256
return;
4357
}
4458

59+
pw_core_add_listener(this->core, &this->listener.hook, &PwCore::EVENTS, this);
60+
4561
qCInfo(logLoop) << "Linking pipewire event loop.";
4662
// Tie the pw event loop into qt.
4763
auto fd = pw_loop_get_fd(this->loop);
@@ -79,6 +95,16 @@ void PwCore::poll() {
7995
emit this->polled();
8096
}
8197

98+
qint32 PwCore::sync(quint32 id) const {
99+
// Seq param doesn't seem to do anything. Seq is instead the returned value.
100+
return pw_core_sync(this->core, id, 0);
101+
}
102+
103+
void PwCore::onSync(void* data, quint32 id, qint32 seq) {
104+
auto* self = static_cast<PwCore*>(data);
105+
emit self->synced(id, seq);
106+
}
107+
82108
SpaHook::SpaHook() { // NOLINT
83109
spa_zero(this->hook);
84110
}

src/services/pipewire/core.hpp

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,14 @@
1414

1515
namespace qs::service::pipewire {
1616

17+
class SpaHook {
18+
public:
19+
explicit SpaHook();
20+
21+
void remove();
22+
spa_hook hook;
23+
};
24+
1725
class PwCore: public QObject {
1826
Q_OBJECT;
1927

@@ -23,19 +31,26 @@ class PwCore: public QObject {
2331
Q_DISABLE_COPY_MOVE(PwCore);
2432

2533
[[nodiscard]] bool isValid() const;
34+
[[nodiscard]] qint32 sync(quint32 id) const;
2635

2736
pw_loop* loop = nullptr;
2837
pw_context* context = nullptr;
2938
pw_core* core = nullptr;
3039

3140
signals:
3241
void polled();
42+
void synced(quint32 id, qint32 seq);
3343

3444
private slots:
3545
void poll();
3646

3747
private:
48+
static const pw_core_events EVENTS;
49+
50+
static void onSync(void* data, quint32 id, qint32 seq);
51+
3852
QSocketNotifier notifier;
53+
SpaHook listener;
3954
};
4055

4156
template <typename T>
@@ -49,12 +64,4 @@ class PwObject {
4964
T* object;
5065
};
5166

52-
class SpaHook {
53-
public:
54-
explicit SpaHook();
55-
56-
void remove();
57-
spa_hook hook;
58-
};
59-
6067
} // namespace qs::service::pipewire

src/services/pipewire/node.cpp

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
#include <spa/utils/keys.h>
2525
#include <spa/utils/type.h>
2626

27+
#include "connection.hpp"
28+
#include "core.hpp"
2729
#include "device.hpp"
2830

2931
namespace qs::service::pipewire {
@@ -92,6 +94,12 @@ void PwNode::bindHooks() {
9294
}
9395

9496
void PwNode::unbindHooks() {
97+
if (this->ready) {
98+
this->ready = false;
99+
emit this->readyChanged();
100+
}
101+
102+
this->syncSeq = 0;
95103
this->listener.remove();
96104
this->routeDevice = -1;
97105
this->properties.clear();
@@ -201,6 +209,20 @@ void PwNode::onInfo(void* data, const pw_node_info* info) {
201209
if (self->boundData != nullptr) {
202210
self->boundData->onInfo(info);
203211
}
212+
213+
if (!self->ready && !self->syncSeq) {
214+
auto* core = PwConnection::instance()->registry.core;
215+
QObject::connect(core, &PwCore::synced, self, &PwNode::onCoreSync);
216+
self->syncSeq = core->sync(self->id);
217+
}
218+
}
219+
220+
void PwNode::onCoreSync(quint32 id, qint32 seq) {
221+
if (id != this->id || seq != this->syncSeq) return;
222+
qCInfo(logNode) << "Completed initial sync for" << this;
223+
this->ready = true;
224+
this->syncSeq = 0;
225+
emit this->readyChanged();
204226
}
205227

206228
void PwNode::onParam(

src/services/pipewire/node.hpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ class PwNode: public PwBindable<pw_node, PW_TYPE_INTERFACE_Node, PW_VERSION_NODE
172172
PwNodeType type = PwNodeType::Untracked;
173173
bool isSink = false;
174174
bool isStream = false;
175+
bool ready = false;
175176

176177
PwNodeBoundData* boundData = nullptr;
177178

@@ -180,13 +181,18 @@ class PwNode: public PwBindable<pw_node, PW_TYPE_INTERFACE_Node, PW_VERSION_NODE
180181

181182
signals:
182183
void propertiesChanged();
184+
void readyChanged();
185+
186+
private slots:
187+
void onCoreSync(quint32 id, qint32 seq);
183188

184189
private:
185190
static const pw_node_events EVENTS;
186191
static void onInfo(void* data, const pw_node_info* info);
187192
static void
188193
onParam(void* data, qint32 seq, quint32 id, quint32 index, quint32 next, const spa_pod* param);
189194

195+
qint32 syncSeq = 0;
190196
SpaHook listener;
191197
};
192198

src/services/pipewire/qml.cpp

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
#include <qcontainerfwd.h>
44
#include <qlist.h>
5+
#include <qnamespace.h>
56
#include <qobject.h>
67
#include <qqmllist.h>
78
#include <qtmetamacros.h>
@@ -87,6 +88,16 @@ Pipewire::Pipewire(QObject* parent): QObject(parent) {
8788
this,
8889
&Pipewire::defaultConfiguredAudioSourceChanged
8990
);
91+
92+
if (!connection->registry.isInitialized()) {
93+
QObject::connect(
94+
&connection->registry,
95+
&PwRegistry::initialized,
96+
this,
97+
&Pipewire::readyChanged,
98+
Qt::SingleShotConnection
99+
);
100+
}
90101
}
91102

92103
ObjectModel<PwNodeIface>* Pipewire::nodes() { return &this->mNodes; }
@@ -156,6 +167,8 @@ void Pipewire::setDefaultConfiguredAudioSource(PwNodeIface* node) {
156167
PwConnection::instance()->defaults.changeConfiguredSource(node ? node->node() : nullptr);
157168
}
158169

170+
bool Pipewire::isReady() { return PwConnection::instance()->registry.isInitialized(); }
171+
159172
PwNodeIface* PwNodeLinkTracker::node() const { return this->mNode; }
160173

161174
void PwNodeLinkTracker::setNode(PwNodeIface* node) {
@@ -298,6 +311,7 @@ void PwNodeAudioIface::setVolumes(const QVector<float>& volumes) {
298311

299312
PwNodeIface::PwNodeIface(PwNode* node): PwObjectIface(node), mNode(node) {
300313
QObject::connect(node, &PwNode::propertiesChanged, this, &PwNodeIface::propertiesChanged);
314+
QObject::connect(node, &PwNode::readyChanged, this, &PwNodeIface::readyChanged);
301315

302316
if (auto* audioBoundData = dynamic_cast<PwNodeBoundAudio*>(node->boundData)) {
303317
this->audioIface = new PwNodeAudioIface(audioBoundData, this);
@@ -318,6 +332,8 @@ bool PwNodeIface::isSink() const { return this->mNode->isSink; }
318332

319333
bool PwNodeIface::isStream() const { return this->mNode->isStream; }
320334

335+
bool PwNodeIface::isReady() const { return this->mNode->ready; }
336+
321337
QVariantMap PwNodeIface::properties() const {
322338
auto map = QVariantMap();
323339
for (auto [k, v]: this->mNode->properties.asKeyValueRange()) {

src/services/pipewire/qml.hpp

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,13 @@ class Pipewire: public QObject {
116116
///
117117
/// See @@defaultAudioSource for the current default source, regardless of preference.
118118
Q_PROPERTY(qs::service::pipewire::PwNodeIface* preferredDefaultAudioSource READ defaultConfiguredAudioSource WRITE setDefaultConfiguredAudioSource NOTIFY defaultConfiguredAudioSourceChanged);
119+
/// This property is true if quickshell has completed its initial sync with
120+
/// the pipewire server. If true, nodes, links and sync/source preferences will be
121+
/// in a good state.
122+
///
123+
/// > [!NOTE] You can use the pipewire object before it is ready, but some nodes/links
124+
/// > may be missing, and preference metadata may be null.
125+
Q_PROPERTY(bool ready READ isReady NOTIFY readyChanged);
119126
// clang-format on
120127
QML_ELEMENT;
121128
QML_SINGLETON;
@@ -136,13 +143,17 @@ class Pipewire: public QObject {
136143
[[nodiscard]] PwNodeIface* defaultConfiguredAudioSource() const;
137144
static void setDefaultConfiguredAudioSource(PwNodeIface* node);
138145

146+
[[nodiscard]] static bool isReady();
147+
139148
signals:
140149
void defaultAudioSinkChanged();
141150
void defaultAudioSourceChanged();
142151

143152
void defaultConfiguredAudioSinkChanged();
144153
void defaultConfiguredAudioSourceChanged();
145154

155+
void readyChanged();
156+
146157
private slots:
147158
void onNodeAdded(PwNode* node);
148159
void onNodeRemoved(QObject* object);
@@ -294,6 +305,11 @@ class PwNodeIface: public PwObjectIface {
294305
/// The presence or absence of this property can be used to determine if a node
295306
/// manages audio, regardless of if it is bound. If non null, the node is an audio node.
296307
Q_PROPERTY(qs::service::pipewire::PwNodeAudioIface* audio READ audio CONSTANT);
308+
/// True if the node is fully bound and ready to use.
309+
///
310+
/// > [!NODE] The node may be used before it is fully bound, but some data
311+
/// > may be missing or incorrect.
312+
Q_PROPERTY(bool ready READ isReady NOTIFY readyChanged);
297313
QML_NAMED_ELEMENT(PwNode);
298314
QML_UNCREATABLE("PwNodes cannot be created directly");
299315

@@ -307,13 +323,15 @@ class PwNodeIface: public PwObjectIface {
307323
[[nodiscard]] QString nickname() const;
308324
[[nodiscard]] bool isSink() const;
309325
[[nodiscard]] bool isStream() const;
326+
[[nodiscard]] bool isReady() const;
310327
[[nodiscard]] QVariantMap properties() const;
311328
[[nodiscard]] PwNodeAudioIface* audio() const;
312329

313330
static PwNodeIface* instance(PwNode* node);
314331

315332
signals:
316333
void propertiesChanged();
334+
void readyChanged();
317335

318336
private:
319337
PwNode* mNode;

src/services/pipewire/registry.cpp

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,29 @@ void PwRegistry::init(PwCore& core) {
126126
this->core = &core;
127127
this->object = pw_core_get_registry(core.core, PW_VERSION_REGISTRY, 0);
128128
pw_registry_add_listener(this->object, &this->listener.hook, &PwRegistry::EVENTS, this);
129+
130+
QObject::connect(this->core, &PwCore::synced, this, &PwRegistry::onCoreSync);
131+
132+
qCDebug(logRegistry) << "Registry created. Sending core sync for initial object tracking.";
133+
this->coreSyncSeq = this->core->sync(PW_ID_CORE);
134+
}
135+
136+
void PwRegistry::onCoreSync(quint32 id, qint32 seq) {
137+
if (id != PW_ID_CORE || seq != this->coreSyncSeq) return;
138+
139+
switch (this->initState) {
140+
case InitState::SendingObjects:
141+
qCDebug(logRegistry) << "Initial sync for objects received. Syncing for metadata binding.";
142+
this->coreSyncSeq = this->core->sync(PW_ID_CORE);
143+
this->initState = InitState::Binding;
144+
break;
145+
case InitState::Binding:
146+
qCInfo(logRegistry) << "Initial state sync complete.";
147+
this->initState = InitState::Done;
148+
emit this->initialized();
149+
break;
150+
default: break;
151+
}
129152
}
130153

131154
const pw_registry_events PwRegistry::EVENTS = {

src/services/pipewire/registry.hpp

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,8 @@ class PwRegistry
116116
public:
117117
void init(PwCore& core);
118118

119+
[[nodiscard]] bool isInitialized() const { return this->initState == InitState::Done; }
120+
119121
//QHash<quint32, PwClient*> clients;
120122
QHash<quint32, PwMetadata*> metadata;
121123
QHash<quint32, PwNode*> nodes;
@@ -132,9 +134,11 @@ class PwRegistry
132134
void linkAdded(PwLink* link);
133135
void linkGroupAdded(PwLinkGroup* group);
134136
void metadataAdded(PwMetadata* metadata);
137+
void initialized();
135138

136139
private slots:
137140
void onLinkGroupDestroyed(QObject* object);
141+
void onCoreSync(quint32 id, qint32 seq);
138142

139143
private:
140144
static const pw_registry_events EVENTS;
@@ -152,6 +156,13 @@ private slots:
152156

153157
void addLinkToGroup(PwLink* link);
154158

159+
enum class InitState : quint8 {
160+
SendingObjects,
161+
Binding,
162+
Done
163+
} initState = InitState::SendingObjects;
164+
165+
qint32 coreSyncSeq = 0;
155166
SpaHook listener;
156167
};
157168

0 commit comments

Comments
 (0)