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