forked from quickshell/quickshell
		
	service/pipewire: add pipewire module
This commit is contained in:
		
							parent
							
								
									bba8cb8a7d
								
							
						
					
					
						commit
						3e80c4a4fd
					
				
					 21 changed files with 2476 additions and 4 deletions
				
			
		| 
						 | 
				
			
			@ -36,6 +36,7 @@ Checks: >
 | 
			
		|||
  -readability-braces-around-statements,
 | 
			
		||||
  -readability-redundant-access-specifiers,
 | 
			
		||||
  -readability-else-after-return,
 | 
			
		||||
  -readability-container-data-pointer,
 | 
			
		||||
  tidyfox-*,
 | 
			
		||||
CheckOptions:
 | 
			
		||||
  performance-for-range-copy.WarnOnAllAutoCopies: true
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -18,6 +18,7 @@ option(HYPRLAND "Support hyprland specific features" ON)
 | 
			
		|||
option(HYPRLAND_GLOBAL_SHORTCUTS "Hyprland Global Shortcuts" ON)
 | 
			
		||||
option(HYPRLAND_FOCUS_GRAB "Hyprland Focus Grabbing" ON)
 | 
			
		||||
option(SERVICE_STATUS_NOTIFIER "StatusNotifierItem service" ON)
 | 
			
		||||
option(SERVICE_PIPEWIRE "PipeWire service" ON)
 | 
			
		||||
 | 
			
		||||
message(STATUS "Quickshell configuration")
 | 
			
		||||
message(STATUS "  NVIDIA workarounds: ${NVIDIA_COMPAT}")
 | 
			
		||||
| 
						 | 
				
			
			@ -30,6 +31,7 @@ if (WAYLAND)
 | 
			
		|||
endif ()
 | 
			
		||||
message(STATUS "  Services")
 | 
			
		||||
message(STATUS "    StatusNotifier: ${SERVICE_STATUS_NOTIFIER}")
 | 
			
		||||
message(STATUS "    PipeWire: ${SERVICE_PIPEWIRE}")
 | 
			
		||||
message(STATUS "  Hyprland: ${HYPRLAND}")
 | 
			
		||||
if (HYPRLAND)
 | 
			
		||||
	message(STATUS "    Focus Grabbing: ${HYPRLAND_FOCUS_GRAB}")
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -24,6 +24,7 @@
 | 
			
		|||
 | 
			
		||||
  debug ? false,
 | 
			
		||||
  enableWayland ? true,
 | 
			
		||||
  enablePipewire ? true,
 | 
			
		||||
  nvidiaCompat ? false,
 | 
			
		||||
  svgSupport ? true, # you almost always want this
 | 
			
		||||
}: buildStdenv.mkDerivation {
 | 
			
		||||
| 
						 | 
				
			
			@ -46,7 +47,8 @@
 | 
			
		|||
    qt6.qtdeclarative
 | 
			
		||||
  ]
 | 
			
		||||
  ++ (lib.optionals enableWayland [ qt6.qtwayland wayland ])
 | 
			
		||||
  ++ (lib.optionals svgSupport [ qt6.qtsvg ]);
 | 
			
		||||
  ++ (lib.optionals svgSupport [ qt6.qtsvg ])
 | 
			
		||||
  ++ (lib.optionals enablePipewire [ pipewire ]);
 | 
			
		||||
 | 
			
		||||
  QTWAYLANDSCANNER = lib.optionalString enableWayland "${qt6.qtwayland}/libexec/qtwaylandscanner";
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -62,7 +64,8 @@
 | 
			
		|||
  cmakeFlags = [
 | 
			
		||||
    "-DGIT_REVISION=${gitRev}"
 | 
			
		||||
  ] ++ lib.optional (!enableWayland) "-DWAYLAND=OFF"
 | 
			
		||||
  ++ lib.optional nvidiaCompat "-DNVIDIA_COMPAT=ON";
 | 
			
		||||
  ++ lib.optional nvidiaCompat "-DNVIDIA_COMPAT=ON"
 | 
			
		||||
  ++ lib.optional (!enablePipewire) "-DSERVICE_PIPEWIRE=OFF";
 | 
			
		||||
 | 
			
		||||
  buildPhase = "ninjaBuildPhase";
 | 
			
		||||
  enableParallelBuilding = true;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										2
									
								
								docs
									
										
									
									
									
								
							
							
						
						
									
										2
									
								
								docs
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -1 +1 @@
 | 
			
		|||
Subproject commit 149b784a5a4c40ada67cb9f6af5a5350678ab6d4
 | 
			
		||||
Subproject commit ff5da84a8b258a9b2caaf978ddb6de23635d8903
 | 
			
		||||
| 
						 | 
				
			
			@ -1,3 +1,7 @@
 | 
			
		|||
if (SERVICE_STATUS_NOTIFIER)
 | 
			
		||||
	add_subdirectory(status_notifier)
 | 
			
		||||
endif()
 | 
			
		||||
 | 
			
		||||
if (SERVICE_PIPEWIRE)
 | 
			
		||||
	add_subdirectory(pipewire)
 | 
			
		||||
endif()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										24
									
								
								src/services/pipewire/CMakeLists.txt
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								src/services/pipewire/CMakeLists.txt
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,24 @@
 | 
			
		|||
find_package(PkgConfig REQUIRED)
 | 
			
		||||
pkg_check_modules(pipewire REQUIRED IMPORTED_TARGET libpipewire-0.3)
 | 
			
		||||
 | 
			
		||||
qt_add_library(quickshell-service-pipewire STATIC
 | 
			
		||||
	qml.cpp
 | 
			
		||||
	core.cpp
 | 
			
		||||
	connection.cpp
 | 
			
		||||
	registry.cpp
 | 
			
		||||
	node.cpp
 | 
			
		||||
	metadata.cpp
 | 
			
		||||
	link.cpp
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
qt_add_qml_module(quickshell-service-pipewire
 | 
			
		||||
	URI Quickshell.Services.Pipewire
 | 
			
		||||
	VERSION 0.1
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
target_link_libraries(quickshell-service-pipewire PRIVATE ${QT_DEPS} PkgConfig::pipewire)
 | 
			
		||||
 | 
			
		||||
qs_pch(quickshell-service-pipewire)
 | 
			
		||||
qs_pch(quickshell-service-pipewireplugin)
 | 
			
		||||
 | 
			
		||||
target_link_libraries(quickshell PRIVATE quickshell-service-pipewireplugin)
 | 
			
		||||
							
								
								
									
										23
									
								
								src/services/pipewire/connection.cpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/services/pipewire/connection.cpp
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,23 @@
 | 
			
		|||
#include "connection.hpp"
 | 
			
		||||
 | 
			
		||||
#include <qobject.h>
 | 
			
		||||
 | 
			
		||||
namespace qs::service::pipewire {
 | 
			
		||||
 | 
			
		||||
PwConnection::PwConnection(QObject* parent): QObject(parent) {
 | 
			
		||||
	if (this->core.isValid()) {
 | 
			
		||||
		this->registry.init(this->core);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
PwConnection* PwConnection::instance() {
 | 
			
		||||
	static PwConnection* instance = nullptr; // NOLINT
 | 
			
		||||
 | 
			
		||||
	if (instance == nullptr) {
 | 
			
		||||
		instance = new PwConnection();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return instance;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
} // namespace qs::service::pipewire
 | 
			
		||||
							
								
								
									
										25
									
								
								src/services/pipewire/connection.hpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src/services/pipewire/connection.hpp
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,25 @@
 | 
			
		|||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "core.hpp"
 | 
			
		||||
#include "metadata.hpp"
 | 
			
		||||
#include "registry.hpp"
 | 
			
		||||
 | 
			
		||||
namespace qs::service::pipewire {
 | 
			
		||||
 | 
			
		||||
class PwConnection: public QObject {
 | 
			
		||||
	Q_OBJECT;
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
	explicit PwConnection(QObject* parent = nullptr);
 | 
			
		||||
 | 
			
		||||
	PwRegistry registry;
 | 
			
		||||
	PwDefaultsMetadata defaults {&this->registry};
 | 
			
		||||
 | 
			
		||||
	static PwConnection* instance();
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
	// init/destroy order is important. do not rearrange.
 | 
			
		||||
	PwCore core;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
} // namespace qs::service::pipewire
 | 
			
		||||
							
								
								
									
										87
									
								
								src/services/pipewire/core.cpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								src/services/pipewire/core.cpp
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,87 @@
 | 
			
		|||
#include "core.hpp"
 | 
			
		||||
#include <cerrno>
 | 
			
		||||
 | 
			
		||||
#include <pipewire/context.h>
 | 
			
		||||
#include <pipewire/core.h>
 | 
			
		||||
#include <pipewire/loop.h>
 | 
			
		||||
#include <pipewire/pipewire.h>
 | 
			
		||||
#include <qlogging.h>
 | 
			
		||||
#include <qloggingcategory.h>
 | 
			
		||||
#include <qobject.h>
 | 
			
		||||
#include <qsocketnotifier.h>
 | 
			
		||||
#include <spa/utils/defs.h>
 | 
			
		||||
#include <spa/utils/hook.h>
 | 
			
		||||
 | 
			
		||||
namespace qs::service::pipewire {
 | 
			
		||||
 | 
			
		||||
Q_LOGGING_CATEGORY(logLoop, "quickshell.service.pipewire.loop", QtWarningMsg);
 | 
			
		||||
 | 
			
		||||
PwCore::PwCore(QObject* parent): QObject(parent), notifier(QSocketNotifier::Read) {
 | 
			
		||||
	qCInfo(logLoop) << "Creating pipewire event loop.";
 | 
			
		||||
	pw_init(nullptr, nullptr);
 | 
			
		||||
 | 
			
		||||
	this->loop = pw_loop_new(nullptr);
 | 
			
		||||
	if (this->loop == nullptr) {
 | 
			
		||||
		qCCritical(logLoop) << "Failed to create pipewire event loop.";
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	this->context = pw_context_new(this->loop, nullptr, 0);
 | 
			
		||||
	if (this->context == nullptr) {
 | 
			
		||||
		qCCritical(logLoop) << "Failed to create pipewire context.";
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	qCInfo(logLoop) << "Connecting to pipewire server.";
 | 
			
		||||
	this->core = pw_context_connect(this->context, nullptr, 0);
 | 
			
		||||
	if (this->core == nullptr) {
 | 
			
		||||
		qCCritical(logLoop) << "Failed to connect pipewire context. Errno:" << errno;
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	qCInfo(logLoop) << "Linking pipewire event loop.";
 | 
			
		||||
	// Tie the pw event loop into qt.
 | 
			
		||||
	auto fd = pw_loop_get_fd(this->loop);
 | 
			
		||||
	this->notifier.setSocket(fd);
 | 
			
		||||
	QObject::connect(&this->notifier, &QSocketNotifier::activated, this, &PwCore::poll);
 | 
			
		||||
	this->notifier.setEnabled(true);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
PwCore::~PwCore() {
 | 
			
		||||
	qCInfo(logLoop) << "Destroying PwCore.";
 | 
			
		||||
 | 
			
		||||
	if (this->loop != nullptr) {
 | 
			
		||||
		if (this->context != nullptr) {
 | 
			
		||||
			if (this->core != nullptr) {
 | 
			
		||||
				pw_core_disconnect(this->core);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			pw_context_destroy(this->context);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		pw_loop_destroy(this->loop);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool PwCore::isValid() const {
 | 
			
		||||
	// others must init first
 | 
			
		||||
	return this->core != nullptr;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void PwCore::poll() const {
 | 
			
		||||
	qCDebug(logLoop) << "Pipewire event loop received new events, iterating.";
 | 
			
		||||
	// Spin pw event loop.
 | 
			
		||||
	pw_loop_iterate(this->loop, 0);
 | 
			
		||||
	qCDebug(logLoop) << "Done iterating pipewire event loop.";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
SpaHook::SpaHook() { // NOLINT
 | 
			
		||||
	spa_zero(this->hook);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void SpaHook::remove() {
 | 
			
		||||
	spa_hook_remove(&this->hook);
 | 
			
		||||
	spa_zero(this->hook);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
} // namespace qs::service::pipewire
 | 
			
		||||
							
								
								
									
										59
									
								
								src/services/pipewire/core.hpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								src/services/pipewire/core.hpp
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,59 @@
 | 
			
		|||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include <pipewire/context.h>
 | 
			
		||||
#include <pipewire/core.h>
 | 
			
		||||
#include <pipewire/loop.h>
 | 
			
		||||
#include <pipewire/proxy.h>
 | 
			
		||||
#include <qloggingcategory.h>
 | 
			
		||||
#include <qobject.h>
 | 
			
		||||
#include <qsocketnotifier.h>
 | 
			
		||||
#include <qtclasshelpermacros.h>
 | 
			
		||||
#include <qtmetamacros.h>
 | 
			
		||||
#include <qtypes.h>
 | 
			
		||||
#include <spa/utils/hook.h>
 | 
			
		||||
 | 
			
		||||
namespace qs::service::pipewire {
 | 
			
		||||
 | 
			
		||||
class PwCore: public QObject {
 | 
			
		||||
	Q_OBJECT;
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
	explicit PwCore(QObject* parent = nullptr);
 | 
			
		||||
	~PwCore() override;
 | 
			
		||||
	Q_DISABLE_COPY_MOVE(PwCore);
 | 
			
		||||
 | 
			
		||||
	[[nodiscard]] bool isValid() const;
 | 
			
		||||
 | 
			
		||||
	pw_loop* loop = nullptr;
 | 
			
		||||
	pw_context* context = nullptr;
 | 
			
		||||
	pw_core* core = nullptr;
 | 
			
		||||
 | 
			
		||||
private slots:
 | 
			
		||||
	void poll() const;
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
	QSocketNotifier notifier;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
template <typename T>
 | 
			
		||||
class PwObject {
 | 
			
		||||
public:
 | 
			
		||||
	explicit PwObject(T* object = nullptr): object(object) {}
 | 
			
		||||
	~PwObject() {
 | 
			
		||||
		pw_proxy_destroy(reinterpret_cast<pw_proxy*>(this->object)); // NOLINT
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	Q_DISABLE_COPY_MOVE(PwObject);
 | 
			
		||||
 | 
			
		||||
	T* object;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class SpaHook {
 | 
			
		||||
public:
 | 
			
		||||
	explicit SpaHook();
 | 
			
		||||
 | 
			
		||||
	void remove();
 | 
			
		||||
	spa_hook hook;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
} // namespace qs::service::pipewire
 | 
			
		||||
							
								
								
									
										184
									
								
								src/services/pipewire/link.cpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										184
									
								
								src/services/pipewire/link.cpp
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,184 @@
 | 
			
		|||
#include "link.hpp"
 | 
			
		||||
#include <cstring>
 | 
			
		||||
 | 
			
		||||
#include <pipewire/link.h>
 | 
			
		||||
#include <qdebug.h>
 | 
			
		||||
#include <qlogging.h>
 | 
			
		||||
#include <qloggingcategory.h>
 | 
			
		||||
#include <qobject.h>
 | 
			
		||||
#include <qtmetamacros.h>
 | 
			
		||||
#include <qtypes.h>
 | 
			
		||||
#include <spa/utils/dict.h>
 | 
			
		||||
 | 
			
		||||
#include "registry.hpp"
 | 
			
		||||
 | 
			
		||||
namespace qs::service::pipewire {
 | 
			
		||||
 | 
			
		||||
Q_LOGGING_CATEGORY(logLink, "quickshell.service.pipewire.link", QtWarningMsg);
 | 
			
		||||
 | 
			
		||||
QString PwLinkState::toString(Enum value) {
 | 
			
		||||
	return QString(pw_link_state_as_string(static_cast<pw_link_state>(value)));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void PwLink::bindHooks() {
 | 
			
		||||
	pw_link_add_listener(this->proxy(), &this->listener.hook, &PwLink::EVENTS, this);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void PwLink::unbindHooks() {
 | 
			
		||||
	this->listener.remove();
 | 
			
		||||
	this->setState(PW_LINK_STATE_UNLINKED);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void PwLink::initProps(const spa_dict* props) {
 | 
			
		||||
	qCDebug(logLink) << "Parsing initial SPA props of link" << this;
 | 
			
		||||
 | 
			
		||||
	const spa_dict_item* item = nullptr;
 | 
			
		||||
	spa_dict_for_each(item, props) {
 | 
			
		||||
		if (strcmp(item->key, "link.output.node") == 0) {
 | 
			
		||||
			auto str = QString(item->value);
 | 
			
		||||
			auto ok = false;
 | 
			
		||||
			auto value = str.toInt(&ok);
 | 
			
		||||
			if (ok) this->setOutputNode(value);
 | 
			
		||||
			else {
 | 
			
		||||
				qCWarning(logLink) << "Could not parse link.output.node for" << this << ":" << item->value;
 | 
			
		||||
			}
 | 
			
		||||
		} else if (strcmp(item->key, "link.input.node") == 0) {
 | 
			
		||||
			auto str = QString(item->value);
 | 
			
		||||
			auto ok = false;
 | 
			
		||||
			auto value = str.toInt(&ok);
 | 
			
		||||
			if (ok) this->setInputNode(value);
 | 
			
		||||
			else {
 | 
			
		||||
				qCWarning(logLink) << "Could not parse link.input.node for" << this << ":" << item->value;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const pw_link_events PwLink::EVENTS = {
 | 
			
		||||
    .version = PW_VERSION_LINK_EVENTS,
 | 
			
		||||
    .info = &PwLink::onInfo,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
void PwLink::onInfo(void* data, const struct pw_link_info* info) {
 | 
			
		||||
	auto* self = static_cast<PwLink*>(data);
 | 
			
		||||
	qCDebug(logLink) << "Got link info update for" << self << "with mask" << info->change_mask;
 | 
			
		||||
	self->setOutputNode(info->output_node_id);
 | 
			
		||||
	self->setInputNode(info->input_node_id);
 | 
			
		||||
 | 
			
		||||
	if ((info->change_mask & PW_LINK_CHANGE_MASK_STATE) != 0) {
 | 
			
		||||
		self->setState(info->state);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
quint32 PwLink::outputNode() const { return this->mOutputNode; }
 | 
			
		||||
quint32 PwLink::inputNode() const { return this->mInputNode; }
 | 
			
		||||
PwLinkState::Enum PwLink::state() const { return static_cast<PwLinkState::Enum>(this->mState); }
 | 
			
		||||
 | 
			
		||||
void PwLink::setOutputNode(quint32 outputNode) {
 | 
			
		||||
	if (outputNode == this->mOutputNode) return;
 | 
			
		||||
 | 
			
		||||
	if (this->mOutputNode != 0) {
 | 
			
		||||
		qCWarning(logLink) << "Got unexpected output node update for" << this << "to" << outputNode;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	this->mOutputNode = outputNode;
 | 
			
		||||
	qCDebug(logLink) << "Updated output node of" << this;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void PwLink::setInputNode(quint32 inputNode) {
 | 
			
		||||
	if (inputNode == this->mInputNode) return;
 | 
			
		||||
 | 
			
		||||
	if (this->mInputNode != 0) {
 | 
			
		||||
		qCWarning(logLink) << "Got unexpected input node update for" << this << "to" << inputNode;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	this->mInputNode = inputNode;
 | 
			
		||||
	qCDebug(logLink) << "Updated input node of" << this;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void PwLink::setState(pw_link_state state) {
 | 
			
		||||
	if (state == this->mState) return;
 | 
			
		||||
 | 
			
		||||
	this->mState = state;
 | 
			
		||||
	qCDebug(logLink) << "Updated state of" << this;
 | 
			
		||||
	emit this->stateChanged();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
QDebug operator<<(QDebug debug, const PwLink* link) {
 | 
			
		||||
	if (link == nullptr) {
 | 
			
		||||
		debug << "PwLink(0x0)";
 | 
			
		||||
	} else {
 | 
			
		||||
		auto saver = QDebugStateSaver(debug);
 | 
			
		||||
		debug.nospace() << "PwLink(" << link->outputNode() << " -> " << link->inputNode() << ", "
 | 
			
		||||
		                << static_cast<const void*>(link) << ", id=";
 | 
			
		||||
		link->debugId(debug);
 | 
			
		||||
		debug << ", state=" << link->state() << ')';
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return debug;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
PwLinkGroup::PwLinkGroup(PwLink* firstLink, QObject* parent)
 | 
			
		||||
    : QObject(parent)
 | 
			
		||||
    , mOutputNode(firstLink->outputNode())
 | 
			
		||||
    , mInputNode(firstLink->inputNode()) {
 | 
			
		||||
	this->tryAddLink(firstLink);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void PwLinkGroup::ref() {
 | 
			
		||||
	this->refcount++;
 | 
			
		||||
 | 
			
		||||
	if (this->refcount == 1) {
 | 
			
		||||
		this->trackedLink = *this->links.begin();
 | 
			
		||||
		this->trackedLink->ref();
 | 
			
		||||
		QObject::connect(this->trackedLink, &PwLink::stateChanged, this, &PwLinkGroup::stateChanged);
 | 
			
		||||
		emit this->stateChanged();
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void PwLinkGroup::unref() {
 | 
			
		||||
	if (this->refcount == 0) return;
 | 
			
		||||
	this->refcount--;
 | 
			
		||||
 | 
			
		||||
	if (this->refcount == 0) {
 | 
			
		||||
		this->trackedLink->unref();
 | 
			
		||||
		this->trackedLink = nullptr;
 | 
			
		||||
		emit this->stateChanged();
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
quint32 PwLinkGroup::outputNode() const { return this->mOutputNode; }
 | 
			
		||||
 | 
			
		||||
quint32 PwLinkGroup::inputNode() const { return this->mInputNode; }
 | 
			
		||||
 | 
			
		||||
PwLinkState::Enum PwLinkGroup::state() const {
 | 
			
		||||
	if (this->trackedLink == nullptr) {
 | 
			
		||||
		return PwLinkState::Unlinked;
 | 
			
		||||
	} else {
 | 
			
		||||
		return this->trackedLink->state();
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool PwLinkGroup::tryAddLink(PwLink* link) {
 | 
			
		||||
	if (link->outputNode() != this->mOutputNode || link->inputNode() != this->mInputNode)
 | 
			
		||||
		return false;
 | 
			
		||||
 | 
			
		||||
	this->links.insert(link->id, link);
 | 
			
		||||
	QObject::connect(link, &PwBindableObject::destroying, this, &PwLinkGroup::onLinkRemoved);
 | 
			
		||||
	return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void PwLinkGroup::onLinkRemoved(QObject* object) {
 | 
			
		||||
	auto* link = static_cast<PwLink*>(object); // NOLINT
 | 
			
		||||
	this->links.remove(link->id);
 | 
			
		||||
 | 
			
		||||
	if (this->links.empty()) {
 | 
			
		||||
		delete this;
 | 
			
		||||
	} else if (link == this->trackedLink) {
 | 
			
		||||
		this->trackedLink = *this->links.begin();
 | 
			
		||||
		QObject::connect(this->trackedLink, &PwLink::stateChanged, this, &PwLinkGroup::stateChanged);
 | 
			
		||||
		emit this->stateChanged();
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
} // namespace qs::service::pipewire
 | 
			
		||||
							
								
								
									
										99
									
								
								src/services/pipewire/link.hpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								src/services/pipewire/link.hpp
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,99 @@
 | 
			
		|||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include <pipewire/link.h>
 | 
			
		||||
#include <pipewire/type.h>
 | 
			
		||||
#include <qcontainerfwd.h>
 | 
			
		||||
#include <qdebug.h>
 | 
			
		||||
#include <qobject.h>
 | 
			
		||||
#include <qqmlintegration.h>
 | 
			
		||||
#include <qtmetamacros.h>
 | 
			
		||||
#include <qtypes.h>
 | 
			
		||||
 | 
			
		||||
#include "registry.hpp"
 | 
			
		||||
 | 
			
		||||
namespace qs::service::pipewire {
 | 
			
		||||
 | 
			
		||||
class PwLinkState: public QObject {
 | 
			
		||||
	Q_OBJECT;
 | 
			
		||||
	QML_ELEMENT;
 | 
			
		||||
	QML_SINGLETON;
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
	enum Enum {
 | 
			
		||||
		Error = PW_LINK_STATE_ERROR,
 | 
			
		||||
		Unlinked = PW_LINK_STATE_UNLINKED,
 | 
			
		||||
		Init = PW_LINK_STATE_INIT,
 | 
			
		||||
		Negotiating = PW_LINK_STATE_NEGOTIATING,
 | 
			
		||||
		Allocating = PW_LINK_STATE_ALLOCATING,
 | 
			
		||||
		Paused = PW_LINK_STATE_PAUSED,
 | 
			
		||||
		Active = PW_LINK_STATE_ACTIVE,
 | 
			
		||||
	};
 | 
			
		||||
	Q_ENUM(Enum);
 | 
			
		||||
 | 
			
		||||
	Q_INVOKABLE static QString toString(PwLinkState::Enum value);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
constexpr const char TYPE_INTERFACE_Link[] = PW_TYPE_INTERFACE_Link;             // NOLINT
 | 
			
		||||
class PwLink: public PwBindable<pw_link, TYPE_INTERFACE_Link, PW_VERSION_LINK> { // NOLINT
 | 
			
		||||
	Q_OBJECT;
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
	void bindHooks() override;
 | 
			
		||||
	void unbindHooks() override;
 | 
			
		||||
	void initProps(const spa_dict* props) override;
 | 
			
		||||
 | 
			
		||||
	[[nodiscard]] quint32 outputNode() const;
 | 
			
		||||
	[[nodiscard]] quint32 inputNode() const;
 | 
			
		||||
	[[nodiscard]] PwLinkState::Enum state() const;
 | 
			
		||||
 | 
			
		||||
signals:
 | 
			
		||||
	void stateChanged();
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
	static const pw_link_events EVENTS;
 | 
			
		||||
	static void onInfo(void* data, const struct pw_link_info* info);
 | 
			
		||||
 | 
			
		||||
	void setOutputNode(quint32 outputNode);
 | 
			
		||||
	void setInputNode(quint32 inputNode);
 | 
			
		||||
	void setState(pw_link_state state);
 | 
			
		||||
 | 
			
		||||
	SpaHook listener;
 | 
			
		||||
 | 
			
		||||
	quint32 mOutputNode = 0;
 | 
			
		||||
	quint32 mInputNode = 0;
 | 
			
		||||
	pw_link_state mState = PW_LINK_STATE_UNLINKED;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
QDebug operator<<(QDebug debug, const PwLink* link);
 | 
			
		||||
 | 
			
		||||
class PwLinkGroup: public QObject {
 | 
			
		||||
	Q_OBJECT;
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
	explicit PwLinkGroup(PwLink* firstLink, QObject* parent = nullptr);
 | 
			
		||||
 | 
			
		||||
	void ref();
 | 
			
		||||
	void unref();
 | 
			
		||||
 | 
			
		||||
	[[nodiscard]] quint32 outputNode() const;
 | 
			
		||||
	[[nodiscard]] quint32 inputNode() const;
 | 
			
		||||
	[[nodiscard]] PwLinkState::Enum state() const;
 | 
			
		||||
 | 
			
		||||
	QHash<quint32, PwLink*> links;
 | 
			
		||||
 | 
			
		||||
	bool tryAddLink(PwLink* link);
 | 
			
		||||
 | 
			
		||||
signals:
 | 
			
		||||
	void stateChanged();
 | 
			
		||||
 | 
			
		||||
private slots:
 | 
			
		||||
	void onLinkRemoved(QObject* object);
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
	quint32 mOutputNode = 0;
 | 
			
		||||
	quint32 mInputNode = 0;
 | 
			
		||||
	PwLink* trackedLink = nullptr;
 | 
			
		||||
	quint32 refcount = 0;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
} // namespace qs::service::pipewire
 | 
			
		||||
							
								
								
									
										146
									
								
								src/services/pipewire/metadata.cpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										146
									
								
								src/services/pipewire/metadata.cpp
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,146 @@
 | 
			
		|||
#include "metadata.hpp"
 | 
			
		||||
#include <array>
 | 
			
		||||
#include <cstring>
 | 
			
		||||
 | 
			
		||||
#include <pipewire/extensions/metadata.h>
 | 
			
		||||
#include <qlogging.h>
 | 
			
		||||
#include <qloggingcategory.h>
 | 
			
		||||
#include <qobject.h>
 | 
			
		||||
#include <qtmetamacros.h>
 | 
			
		||||
#include <qtypes.h>
 | 
			
		||||
#include <spa/utils/json.h>
 | 
			
		||||
 | 
			
		||||
#include "registry.hpp"
 | 
			
		||||
 | 
			
		||||
namespace qs::service::pipewire {
 | 
			
		||||
 | 
			
		||||
Q_LOGGING_CATEGORY(logMeta, "quickshell.service.pipewire.metadata", QtWarningMsg);
 | 
			
		||||
 | 
			
		||||
void PwMetadata::bindHooks() {
 | 
			
		||||
	pw_metadata_add_listener(this->proxy(), &this->listener.hook, &PwMetadata::EVENTS, this);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void PwMetadata::unbindHooks() { this->listener.remove(); }
 | 
			
		||||
 | 
			
		||||
const pw_metadata_events PwMetadata::EVENTS = {
 | 
			
		||||
    .version = PW_VERSION_METADATA_EVENTS,
 | 
			
		||||
    .property = &PwMetadata::onProperty,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
int PwMetadata::onProperty(
 | 
			
		||||
    void* data,
 | 
			
		||||
    quint32 subject,
 | 
			
		||||
    const char* key,
 | 
			
		||||
    const char* type,
 | 
			
		||||
    const char* value
 | 
			
		||||
) {
 | 
			
		||||
	auto* self = static_cast<PwMetadata*>(data);
 | 
			
		||||
	qCDebug(logMeta) << "Received metadata for" << self << "- subject:" << subject
 | 
			
		||||
	                 << "key:" << QString(key) << "type:" << QString(type)
 | 
			
		||||
	                 << "value:" << QString(value);
 | 
			
		||||
 | 
			
		||||
	emit self->registry->metadataUpdate(self, subject, key, type, value);
 | 
			
		||||
 | 
			
		||||
	// ideally we'd dealloc metadata that wasn't picked up but there's no information
 | 
			
		||||
	// available about if updates can come in later, so I assume they can.
 | 
			
		||||
 | 
			
		||||
	return 0; // ??? - no docs and no reason for a callback to return an int
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
PwDefaultsMetadata::PwDefaultsMetadata(PwRegistry* registry) {
 | 
			
		||||
	QObject::connect(
 | 
			
		||||
	    registry,
 | 
			
		||||
	    &PwRegistry::metadataUpdate,
 | 
			
		||||
	    this,
 | 
			
		||||
	    &PwDefaultsMetadata::onMetadataUpdate
 | 
			
		||||
	);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
QString PwDefaultsMetadata::defaultSink() const { return this->mDefaultSink; }
 | 
			
		||||
 | 
			
		||||
QString PwDefaultsMetadata::defaultSource() const { return this->mDefaultSource; }
 | 
			
		||||
 | 
			
		||||
// we don't really care if the metadata objects are destroyed, but try to ref them so we get property updates
 | 
			
		||||
void PwDefaultsMetadata::onMetadataUpdate(
 | 
			
		||||
    PwMetadata* metadata,
 | 
			
		||||
    quint32 subject,
 | 
			
		||||
    const char* key,
 | 
			
		||||
    const char* /*type*/,
 | 
			
		||||
    const char* value
 | 
			
		||||
) {
 | 
			
		||||
	if (subject != 0) return;
 | 
			
		||||
 | 
			
		||||
	// non "configured" sinks and sources have lower priority as wireplumber seems to only change
 | 
			
		||||
	// the "configured" ones.
 | 
			
		||||
 | 
			
		||||
	bool sink = false;
 | 
			
		||||
	if (strcmp(key, "default.configured.audio.sink") == 0) {
 | 
			
		||||
		sink = true;
 | 
			
		||||
		this->sinkConfigured = true;
 | 
			
		||||
	} else if ((!this->sinkConfigured && strcmp(key, "default.audio.sink") == 0)) {
 | 
			
		||||
		sink = true;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (sink) {
 | 
			
		||||
		this->defaultSinkHolder.setObject(metadata);
 | 
			
		||||
 | 
			
		||||
		auto newSink = PwDefaultsMetadata::parseNameSpaJson(value);
 | 
			
		||||
		qCInfo(logMeta) << "Got default sink" << newSink << "configured:" << this->sinkConfigured;
 | 
			
		||||
		if (newSink == this->mDefaultSink) return;
 | 
			
		||||
 | 
			
		||||
		this->mDefaultSink = newSink;
 | 
			
		||||
		emit this->defaultSinkChanged();
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	bool source = false;
 | 
			
		||||
	if (strcmp(key, "default.configured.audio.source") == 0) {
 | 
			
		||||
		source = true;
 | 
			
		||||
		this->sourceConfigured = true;
 | 
			
		||||
	} else if ((!this->sourceConfigured && strcmp(key, "default.audio.source") == 0)) {
 | 
			
		||||
		source = true;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (source) {
 | 
			
		||||
		this->defaultSourceHolder.setObject(metadata);
 | 
			
		||||
 | 
			
		||||
		auto newSource = PwDefaultsMetadata::parseNameSpaJson(value);
 | 
			
		||||
		qCInfo(logMeta) << "Got default source" << newSource << "configured:" << this->sourceConfigured;
 | 
			
		||||
		if (newSource == this->mDefaultSource) return;
 | 
			
		||||
 | 
			
		||||
		this->mDefaultSource = newSource;
 | 
			
		||||
		emit this->defaultSourceChanged();
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
QString PwDefaultsMetadata::parseNameSpaJson(const char* spaJson) {
 | 
			
		||||
	auto iter = std::array<spa_json, 2>();
 | 
			
		||||
	spa_json_init(&iter[0], spaJson, strlen(spaJson));
 | 
			
		||||
 | 
			
		||||
	if (spa_json_enter_object(&iter[0], &iter[1]) < 0) {
 | 
			
		||||
		qCWarning(logMeta) << "Failed to parse source/sink SPA json - failed to enter object of"
 | 
			
		||||
		                   << QString(spaJson);
 | 
			
		||||
		return "";
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	auto buf = std::array<char, 512>();
 | 
			
		||||
	while (spa_json_get_string(&iter[1], buf.data(), buf.size()) > 0) {
 | 
			
		||||
		if (strcmp(buf.data(), "name") != 0) continue;
 | 
			
		||||
 | 
			
		||||
		if (spa_json_get_string(&iter[1], buf.data(), buf.size()) < 0) {
 | 
			
		||||
			qCWarning(logMeta
 | 
			
		||||
			) << "Failed to parse source/sink SPA json - failed to read value of name property"
 | 
			
		||||
			  << QString(spaJson);
 | 
			
		||||
			return "";
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return QString(buf.data());
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	qCWarning(logMeta) << "Failed to parse source/sink SPA json - failed to find name property of"
 | 
			
		||||
	                   << QString(spaJson);
 | 
			
		||||
	return "";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
} // namespace qs::service::pipewire
 | 
			
		||||
							
								
								
									
										64
									
								
								src/services/pipewire/metadata.hpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								src/services/pipewire/metadata.hpp
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,64 @@
 | 
			
		|||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include <pipewire/extensions/metadata.h>
 | 
			
		||||
#include <pipewire/type.h>
 | 
			
		||||
#include <qobject.h>
 | 
			
		||||
#include <qtmetamacros.h>
 | 
			
		||||
 | 
			
		||||
#include "core.hpp"
 | 
			
		||||
#include "registry.hpp"
 | 
			
		||||
 | 
			
		||||
namespace qs::service::pipewire {
 | 
			
		||||
 | 
			
		||||
constexpr const char TYPE_INTERFACE_Metadata[] = PW_TYPE_INTERFACE_Metadata; // NOLINT
 | 
			
		||||
class PwMetadata
 | 
			
		||||
    : public PwBindable<pw_metadata, TYPE_INTERFACE_Metadata, PW_VERSION_METADATA> { // NOLINT
 | 
			
		||||
	Q_OBJECT;
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
	void bindHooks() override;
 | 
			
		||||
	void unbindHooks() override;
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
	static const pw_metadata_events EVENTS;
 | 
			
		||||
	static int
 | 
			
		||||
	onProperty(void* data, quint32 subject, const char* key, const char* type, const char* value);
 | 
			
		||||
 | 
			
		||||
	SpaHook listener;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class PwDefaultsMetadata: public QObject {
 | 
			
		||||
	Q_OBJECT;
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
	explicit PwDefaultsMetadata(PwRegistry* registry);
 | 
			
		||||
 | 
			
		||||
	[[nodiscard]] QString defaultSource() const;
 | 
			
		||||
	[[nodiscard]] QString defaultSink() const;
 | 
			
		||||
 | 
			
		||||
signals:
 | 
			
		||||
	void defaultSourceChanged();
 | 
			
		||||
	void defaultSinkChanged();
 | 
			
		||||
 | 
			
		||||
private slots:
 | 
			
		||||
	void onMetadataUpdate(
 | 
			
		||||
	    PwMetadata* metadata,
 | 
			
		||||
	    quint32 subject,
 | 
			
		||||
	    const char* key,
 | 
			
		||||
	    const char* type,
 | 
			
		||||
	    const char* value
 | 
			
		||||
	);
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
	static QString parseNameSpaJson(const char* spaJson);
 | 
			
		||||
 | 
			
		||||
	PwBindableRef<PwMetadata> defaultSinkHolder;
 | 
			
		||||
	PwBindableRef<PwMetadata> defaultSourceHolder;
 | 
			
		||||
 | 
			
		||||
	bool sinkConfigured = false;
 | 
			
		||||
	QString mDefaultSink;
 | 
			
		||||
	bool sourceConfigured = false;
 | 
			
		||||
	QString mDefaultSource;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
} // namespace qs::service::pipewire
 | 
			
		||||
							
								
								
									
										384
									
								
								src/services/pipewire/node.cpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										384
									
								
								src/services/pipewire/node.cpp
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,384 @@
 | 
			
		|||
#include "node.hpp"
 | 
			
		||||
#include <array>
 | 
			
		||||
#include <cmath>
 | 
			
		||||
#include <cstdint>
 | 
			
		||||
#include <cstring>
 | 
			
		||||
 | 
			
		||||
#include <pipewire/core.h>
 | 
			
		||||
#include <pipewire/node.h>
 | 
			
		||||
#include <qcontainerfwd.h>
 | 
			
		||||
#include <qlogging.h>
 | 
			
		||||
#include <qloggingcategory.h>
 | 
			
		||||
#include <qobject.h>
 | 
			
		||||
#include <qtmetamacros.h>
 | 
			
		||||
#include <qtypes.h>
 | 
			
		||||
#include <spa/node/keys.h>
 | 
			
		||||
#include <spa/param/param.h>
 | 
			
		||||
#include <spa/param/props.h>
 | 
			
		||||
#include <spa/pod/builder.h>
 | 
			
		||||
#include <spa/pod/iter.h>
 | 
			
		||||
#include <spa/pod/pod.h>
 | 
			
		||||
#include <spa/pod/vararg.h>
 | 
			
		||||
#include <spa/utils/dict.h>
 | 
			
		||||
#include <spa/utils/keys.h>
 | 
			
		||||
#include <spa/utils/type.h>
 | 
			
		||||
 | 
			
		||||
namespace qs::service::pipewire {
 | 
			
		||||
 | 
			
		||||
Q_LOGGING_CATEGORY(logNode, "quickshell.service.pipewire.node", QtWarningMsg);
 | 
			
		||||
 | 
			
		||||
QString PwAudioChannel::toString(Enum value) {
 | 
			
		||||
	switch (value) {
 | 
			
		||||
	case Unknown: return "Unknown";
 | 
			
		||||
	case NA: return "N/A";
 | 
			
		||||
	case Mono: return "Mono";
 | 
			
		||||
	case FrontCenter: return "Front Center";
 | 
			
		||||
	case FrontLeft: return "Front Left";
 | 
			
		||||
	case FrontRight: return "Front Right";
 | 
			
		||||
	case FrontLeftCenter: return "Front Left Center";
 | 
			
		||||
	case FrontRightCenter: return "Front Right Center";
 | 
			
		||||
	case FrontLeftWide: return "Front Left Wide";
 | 
			
		||||
	case FrontRightWide: return "Front Right Wide";
 | 
			
		||||
	case FrontCenterHigh: return "Front Center High";
 | 
			
		||||
	case FrontLeftHigh: return "Front Left High";
 | 
			
		||||
	case FrontRightHigh: return "Front Right High";
 | 
			
		||||
	case LowFrequencyEffects: return "Low Frequency Effects";
 | 
			
		||||
	case LowFrequencyEffects2: return "Low Frequency Effects 2";
 | 
			
		||||
	case LowFrequencyEffectsLeft: return "Low Frequency Effects Left";
 | 
			
		||||
	case LowFrequencyEffectsRight: return "Low Frequency Effects Right";
 | 
			
		||||
	case SideLeft: return "Side Left";
 | 
			
		||||
	case SideRight: return "Side Right";
 | 
			
		||||
	case RearCenter: return "Rear Center";
 | 
			
		||||
	case RearLeft: return "Rear Left";
 | 
			
		||||
	case RearRight: return "Rear Right";
 | 
			
		||||
	case RearLeftCenter: return "Rear Left Center";
 | 
			
		||||
	case RearRightCenter: return "Rear Right Center";
 | 
			
		||||
	case TopCenter: return "Top Center";
 | 
			
		||||
	case TopFrontCenter: return "Top Front Center";
 | 
			
		||||
	case TopFrontLeft: return "Top Front Left";
 | 
			
		||||
	case TopFrontRight: return "Top Front Right";
 | 
			
		||||
	case TopFrontLeftCenter: return "Top Front Left Center";
 | 
			
		||||
	case TopFrontRightCenter: return "Top Front Right Center";
 | 
			
		||||
	case TopSideLeft: return "Top Side Left";
 | 
			
		||||
	case TopSideRight: return "Top Side Right";
 | 
			
		||||
	case TopRearCenter: return "Top Rear Center";
 | 
			
		||||
	case TopRearLeft: return "Top Rear Left";
 | 
			
		||||
	case TopRearRight: return "Top Rear Right";
 | 
			
		||||
	case BottomCenter: return "Bottom Center";
 | 
			
		||||
	case BottomLeftCenter: return "Bottom Left Center";
 | 
			
		||||
	case BottomRightCenter: return "Bottom Right Center";
 | 
			
		||||
	default:
 | 
			
		||||
		if (value >= AuxRangeStart && value <= AuxRangeEnd) {
 | 
			
		||||
			return QString("Aux %1").arg(value - AuxRangeStart + 1);
 | 
			
		||||
		} else if (value >= CustomRangeStart) {
 | 
			
		||||
			return QString("Custom %1").arg(value - CustomRangeStart + 1);
 | 
			
		||||
		} else {
 | 
			
		||||
			return "Unknown";
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void PwNode::bindHooks() {
 | 
			
		||||
	pw_node_add_listener(this->proxy(), &this->listener.hook, &PwNode::EVENTS, this);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void PwNode::unbindHooks() {
 | 
			
		||||
	this->listener.remove();
 | 
			
		||||
	this->properties.clear();
 | 
			
		||||
	emit this->propertiesChanged();
 | 
			
		||||
 | 
			
		||||
	if (this->boundData != nullptr) {
 | 
			
		||||
		this->boundData->onUnbind();
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void PwNode::initProps(const spa_dict* props) {
 | 
			
		||||
	if (const auto* mediaClass = spa_dict_lookup(props, SPA_KEY_MEDIA_CLASS)) {
 | 
			
		||||
		if (strcmp(mediaClass, "Audio/Sink") == 0) {
 | 
			
		||||
			this->type = PwNodeType::Audio;
 | 
			
		||||
			this->isSink = true;
 | 
			
		||||
			this->isStream = false;
 | 
			
		||||
		} else if (strcmp(mediaClass, "Audio/Source") == 0) {
 | 
			
		||||
			this->type = PwNodeType::Audio;
 | 
			
		||||
			this->isSink = false;
 | 
			
		||||
			this->isStream = false;
 | 
			
		||||
		} else if (strcmp(mediaClass, "Stream/Output/Audio") == 0) {
 | 
			
		||||
			this->type = PwNodeType::Audio;
 | 
			
		||||
			this->isSink = false;
 | 
			
		||||
			this->isStream = true;
 | 
			
		||||
		} else if (strcmp(mediaClass, "Stream/Input/Audio") == 0) {
 | 
			
		||||
			this->type = PwNodeType::Audio;
 | 
			
		||||
			this->isSink = true;
 | 
			
		||||
			this->isStream = true;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (const auto* nodeName = spa_dict_lookup(props, SPA_KEY_NODE_NAME)) {
 | 
			
		||||
		this->name = nodeName;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (const auto* nodeDesc = spa_dict_lookup(props, SPA_KEY_NODE_DESCRIPTION)) {
 | 
			
		||||
		this->description = nodeDesc;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (const auto* nodeNick = spa_dict_lookup(props, "node.nick")) {
 | 
			
		||||
		this->nick = nodeNick;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (this->type == PwNodeType::Audio) {
 | 
			
		||||
		this->boundData = new PwNodeBoundAudio(this);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const pw_node_events PwNode::EVENTS = {
 | 
			
		||||
    .version = PW_VERSION_NODE_EVENTS,
 | 
			
		||||
    .info = &PwNode::onInfo,
 | 
			
		||||
    .param = &PwNode::onParam,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
void PwNode::onInfo(void* data, const pw_node_info* info) {
 | 
			
		||||
	auto* self = static_cast<PwNode*>(data);
 | 
			
		||||
 | 
			
		||||
	if ((info->change_mask & PW_NODE_CHANGE_MASK_PROPS) != 0) {
 | 
			
		||||
		auto properties = QMap<QString, QString>();
 | 
			
		||||
 | 
			
		||||
		const spa_dict_item* item = nullptr;
 | 
			
		||||
		spa_dict_for_each(item, info->props) { properties.insert(item->key, item->value); }
 | 
			
		||||
 | 
			
		||||
		self->properties = properties;
 | 
			
		||||
		emit self->propertiesChanged();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (self->boundData != nullptr) {
 | 
			
		||||
		self->boundData->onInfo(info);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void PwNode::onParam(
 | 
			
		||||
    void* data,
 | 
			
		||||
    qint32 /*seq*/,
 | 
			
		||||
    quint32 id,
 | 
			
		||||
    quint32 index,
 | 
			
		||||
    quint32 /*next*/,
 | 
			
		||||
    const spa_pod* param
 | 
			
		||||
) {
 | 
			
		||||
	auto* self = static_cast<PwNode*>(data);
 | 
			
		||||
	if (self->boundData != nullptr) {
 | 
			
		||||
		self->boundData->onSpaParam(id, index, param);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void PwNodeBoundAudio::onInfo(const pw_node_info* info) {
 | 
			
		||||
	if ((info->change_mask & PW_NODE_CHANGE_MASK_PARAMS) != 0) {
 | 
			
		||||
		for (quint32 i = 0; i < info->n_params; i++) {
 | 
			
		||||
			auto& param = info->params[i]; // NOLINT
 | 
			
		||||
 | 
			
		||||
			if (param.id == SPA_PARAM_Props && (param.flags & SPA_PARAM_INFO_READ) != 0) {
 | 
			
		||||
				pw_node_enum_params(this->node->proxy(), 0, param.id, 0, UINT32_MAX, nullptr);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void PwNodeBoundAudio::onSpaParam(quint32 id, quint32 index, const spa_pod* param) {
 | 
			
		||||
	if (id == SPA_PARAM_Props && index == 0) {
 | 
			
		||||
		this->updateVolumeFromParam(param);
 | 
			
		||||
		this->updateMutedFromParam(param);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void PwNodeBoundAudio::updateVolumeFromParam(const spa_pod* param) {
 | 
			
		||||
	const auto* volumesProp = spa_pod_find_prop(param, nullptr, SPA_PROP_channelVolumes);
 | 
			
		||||
	const auto* channelsProp = spa_pod_find_prop(param, nullptr, SPA_PROP_channelMap);
 | 
			
		||||
 | 
			
		||||
	if (volumesProp == nullptr) {
 | 
			
		||||
		qCWarning(logNode) << "Cannot update volume props of" << this->node
 | 
			
		||||
		                   << "- channelVolumes was null.";
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (channelsProp == nullptr) {
 | 
			
		||||
		qCWarning(logNode) << "Cannot update volume props of" << this->node << "- channelMap was null.";
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (spa_pod_is_array(&volumesProp->value) == 0) {
 | 
			
		||||
		qCWarning(logNode) << "Cannot update volume props of" << this->node
 | 
			
		||||
		                   << "- channelVolumes was not an array.";
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (spa_pod_is_array(&channelsProp->value) == 0) {
 | 
			
		||||
		qCWarning(logNode) << "Cannot update volume props of" << this->node
 | 
			
		||||
		                   << "- channelMap was not an array.";
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const auto* volumes = reinterpret_cast<const spa_pod_array*>(&volumesProp->value);   // NOLINT
 | 
			
		||||
	const auto* channels = reinterpret_cast<const spa_pod_array*>(&channelsProp->value); // NOLINT
 | 
			
		||||
 | 
			
		||||
	auto volumesVec = QVector<float>();
 | 
			
		||||
	auto channelsVec = QVector<PwAudioChannel::Enum>();
 | 
			
		||||
 | 
			
		||||
	spa_pod* iter = nullptr;
 | 
			
		||||
	SPA_POD_ARRAY_FOREACH(volumes, iter) {
 | 
			
		||||
		// Cubing behavior found in MPD source, and appears to corrospond to everyone else's measurements correctly.
 | 
			
		||||
		auto linear = *reinterpret_cast<float*>(iter); // NOLINT
 | 
			
		||||
		auto visual = std::cbrt(linear);
 | 
			
		||||
		volumesVec.push_back(visual);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	SPA_POD_ARRAY_FOREACH(channels, iter) {
 | 
			
		||||
		channelsVec.push_back(*reinterpret_cast<PwAudioChannel::Enum*>(iter)); // NOLINT
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (volumesVec.size() != channelsVec.size()) {
 | 
			
		||||
		qCWarning(logNode) << "Cannot update volume props of" << this->node
 | 
			
		||||
		                   << "- channelVolumes and channelMap are not the same size. Sizes:"
 | 
			
		||||
		                   << volumesVec.size() << channelsVec.size();
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// It is important that the lengths of channels and volumes stay in sync whenever you read them.
 | 
			
		||||
	auto channelsChanged = false;
 | 
			
		||||
	auto volumesChanged = false;
 | 
			
		||||
 | 
			
		||||
	if (this->mChannels != channelsVec) {
 | 
			
		||||
		this->mChannels = channelsVec;
 | 
			
		||||
		channelsChanged = true;
 | 
			
		||||
		qCDebug(logNode) << "Got updated channels of" << this->node << '-' << this->mChannels;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (this->mVolumes != volumesVec) {
 | 
			
		||||
		this->mVolumes = volumesVec;
 | 
			
		||||
		volumesChanged = true;
 | 
			
		||||
		qCDebug(logNode) << "Got updated volumes of" << this->node << '-' << this->mVolumes;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (channelsChanged) emit this->channelsChanged();
 | 
			
		||||
	if (volumesChanged) emit this->volumesChanged();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void PwNodeBoundAudio::updateMutedFromParam(const spa_pod* param) {
 | 
			
		||||
	const auto* mutedProp = spa_pod_find_prop(param, nullptr, SPA_PROP_mute);
 | 
			
		||||
 | 
			
		||||
	if (mutedProp == nullptr) {
 | 
			
		||||
		qCWarning(logNode) << "Cannot update muted state of" << this->node
 | 
			
		||||
		                   << "- mute property was null.";
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (spa_pod_is_bool(&mutedProp->value) == 0) {
 | 
			
		||||
		qCWarning(logNode) << "Cannot update muted state of" << this->node
 | 
			
		||||
		                   << "- mute property was not a boolean.";
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	bool muted = false;
 | 
			
		||||
	spa_pod_get_bool(&mutedProp->value, &muted);
 | 
			
		||||
 | 
			
		||||
	if (muted != this->mMuted) {
 | 
			
		||||
		qCDebug(logNode) << "Got updated mute status of" << this->node << '-' << muted;
 | 
			
		||||
		this->mMuted = muted;
 | 
			
		||||
		emit this->mutedChanged();
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void PwNodeBoundAudio::onUnbind() {
 | 
			
		||||
	this->mChannels.clear();
 | 
			
		||||
	this->mVolumes.clear();
 | 
			
		||||
	emit this->channelsChanged();
 | 
			
		||||
	emit this->volumesChanged();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool PwNodeBoundAudio::isMuted() const { return this->mMuted; }
 | 
			
		||||
 | 
			
		||||
void PwNodeBoundAudio::setMuted(bool muted) {
 | 
			
		||||
	if (this->node->proxy() == nullptr) {
 | 
			
		||||
		qCWarning(logNode) << "Tried to change mute state for" << this->node << "which is not bound.";
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (muted == this->mMuted) return;
 | 
			
		||||
 | 
			
		||||
	auto buffer = std::array<quint32, 1024>();
 | 
			
		||||
	auto builder = SPA_POD_BUILDER_INIT(buffer.data(), buffer.size());
 | 
			
		||||
 | 
			
		||||
	// is this a leak? seems like probably not? docs don't say, as usual.
 | 
			
		||||
	// clang-format off
 | 
			
		||||
	auto* pod = spa_pod_builder_add_object(
 | 
			
		||||
			&builder, SPA_TYPE_OBJECT_Props, SPA_PARAM_Props,
 | 
			
		||||
			SPA_PROP_mute, SPA_POD_Bool(muted)
 | 
			
		||||
	);
 | 
			
		||||
	// clang-format on
 | 
			
		||||
 | 
			
		||||
	qCDebug(logNode) << "Changed muted state of" << this->node << "to" << muted;
 | 
			
		||||
	this->mMuted = muted;
 | 
			
		||||
	pw_node_set_param(this->node->proxy(), SPA_PARAM_Props, 0, static_cast<spa_pod*>(pod));
 | 
			
		||||
	emit this->mutedChanged();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
float PwNodeBoundAudio::averageVolume() const {
 | 
			
		||||
	float total = 0;
 | 
			
		||||
 | 
			
		||||
	for (auto volume: this->mVolumes) {
 | 
			
		||||
		total += volume;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return total / static_cast<float>(this->mVolumes.size());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void PwNodeBoundAudio::setAverageVolume(float volume) {
 | 
			
		||||
	auto oldAverage = this->averageVolume();
 | 
			
		||||
	auto mul = oldAverage == 0 ? 0 : volume / oldAverage;
 | 
			
		||||
	auto volumes = QVector<float>();
 | 
			
		||||
 | 
			
		||||
	for (auto oldVolume: this->mVolumes) {
 | 
			
		||||
		volumes.push_back(mul == 0 ? volume : oldVolume * mul);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	this->setVolumes(volumes);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
QVector<PwAudioChannel::Enum> PwNodeBoundAudio::channels() const { return this->mChannels; }
 | 
			
		||||
 | 
			
		||||
QVector<float> PwNodeBoundAudio::volumes() const { return this->mVolumes; }
 | 
			
		||||
 | 
			
		||||
void PwNodeBoundAudio::setVolumes(const QVector<float>& volumes) {
 | 
			
		||||
	if (this->node->proxy() == nullptr) {
 | 
			
		||||
		qCWarning(logNode) << "Tried to change node volumes for" << this->node << "which is not bound.";
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (volumes == this->mVolumes) return;
 | 
			
		||||
 | 
			
		||||
	if (volumes.length() != this->mVolumes.length()) {
 | 
			
		||||
		qCWarning(logNode) << "Tried to change node volumes for" << this->node << "from"
 | 
			
		||||
		                   << this->mVolumes << "to" << volumes
 | 
			
		||||
		                   << "which has a different length than the list of channels"
 | 
			
		||||
		                   << this->mChannels;
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	auto buffer = std::array<quint32, 1024>();
 | 
			
		||||
	auto builder = SPA_POD_BUILDER_INIT(buffer.data(), buffer.size());
 | 
			
		||||
 | 
			
		||||
	auto cubedVolumes = QVector<float>();
 | 
			
		||||
	for (auto volume: volumes) {
 | 
			
		||||
		cubedVolumes.push_back(volume * volume * volume);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// clang-format off
 | 
			
		||||
	auto* pod = spa_pod_builder_add_object(
 | 
			
		||||
			&builder, SPA_TYPE_OBJECT_Props, SPA_PARAM_Props,
 | 
			
		||||
			SPA_PROP_channelVolumes, SPA_POD_Array(sizeof(float), SPA_TYPE_Float, cubedVolumes.length(), cubedVolumes.data())
 | 
			
		||||
	);
 | 
			
		||||
	// clang-format on
 | 
			
		||||
 | 
			
		||||
	qCDebug(logNode) << "Changed volumes of" << this->node << "to" << volumes;
 | 
			
		||||
	this->mVolumes = volumes;
 | 
			
		||||
	pw_node_set_param(this->node->proxy(), SPA_PARAM_Props, 0, static_cast<spa_pod*>(pod));
 | 
			
		||||
	emit this->volumesChanged();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
} // namespace qs::service::pipewire
 | 
			
		||||
							
								
								
									
										174
									
								
								src/services/pipewire/node.hpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										174
									
								
								src/services/pipewire/node.hpp
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,174 @@
 | 
			
		|||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include <pipewire/core.h>
 | 
			
		||||
#include <pipewire/node.h>
 | 
			
		||||
#include <pipewire/type.h>
 | 
			
		||||
#include <qcontainerfwd.h>
 | 
			
		||||
#include <qmap.h>
 | 
			
		||||
#include <qobject.h>
 | 
			
		||||
#include <qqmlintegration.h>
 | 
			
		||||
#include <qtclasshelpermacros.h>
 | 
			
		||||
#include <qtmetamacros.h>
 | 
			
		||||
#include <qtypes.h>
 | 
			
		||||
#include <spa/param/audio/raw.h>
 | 
			
		||||
#include <spa/pod/pod.h>
 | 
			
		||||
 | 
			
		||||
#include "core.hpp"
 | 
			
		||||
#include "registry.hpp"
 | 
			
		||||
 | 
			
		||||
namespace qs::service::pipewire {
 | 
			
		||||
 | 
			
		||||
class PwAudioChannel: public QObject {
 | 
			
		||||
	Q_OBJECT;
 | 
			
		||||
	QML_ELEMENT;
 | 
			
		||||
	QML_SINGLETON;
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
	enum Enum {
 | 
			
		||||
		Unknown = SPA_AUDIO_CHANNEL_UNKNOWN,
 | 
			
		||||
		NA = SPA_AUDIO_CHANNEL_NA,
 | 
			
		||||
		Mono = SPA_AUDIO_CHANNEL_MONO,
 | 
			
		||||
		FrontCenter = SPA_AUDIO_CHANNEL_FC,
 | 
			
		||||
		FrontLeft = SPA_AUDIO_CHANNEL_FL,
 | 
			
		||||
		FrontRight = SPA_AUDIO_CHANNEL_FR,
 | 
			
		||||
		FrontLeftCenter = SPA_AUDIO_CHANNEL_FLC,
 | 
			
		||||
		FrontRightCenter = SPA_AUDIO_CHANNEL_FRC,
 | 
			
		||||
		FrontLeftWide = SPA_AUDIO_CHANNEL_FLW,
 | 
			
		||||
		FrontRightWide = SPA_AUDIO_CHANNEL_FRW,
 | 
			
		||||
		FrontCenterHigh = SPA_AUDIO_CHANNEL_FCH,
 | 
			
		||||
		FrontLeftHigh = SPA_AUDIO_CHANNEL_FLH,
 | 
			
		||||
		FrontRightHigh = SPA_AUDIO_CHANNEL_FRH,
 | 
			
		||||
		LowFrequencyEffects = SPA_AUDIO_CHANNEL_LFE,
 | 
			
		||||
		LowFrequencyEffects2 = SPA_AUDIO_CHANNEL_LFE2,
 | 
			
		||||
		LowFrequencyEffectsLeft = SPA_AUDIO_CHANNEL_LLFE,
 | 
			
		||||
		LowFrequencyEffectsRight = SPA_AUDIO_CHANNEL_RLFE,
 | 
			
		||||
		SideLeft = SPA_AUDIO_CHANNEL_SL,
 | 
			
		||||
		SideRight = SPA_AUDIO_CHANNEL_SR,
 | 
			
		||||
		RearCenter = SPA_AUDIO_CHANNEL_RC,
 | 
			
		||||
		RearLeft = SPA_AUDIO_CHANNEL_RL,
 | 
			
		||||
		RearRight = SPA_AUDIO_CHANNEL_RR,
 | 
			
		||||
		RearLeftCenter = SPA_AUDIO_CHANNEL_RLC,
 | 
			
		||||
		RearRightCenter = SPA_AUDIO_CHANNEL_RRC,
 | 
			
		||||
		TopCenter = SPA_AUDIO_CHANNEL_TC,
 | 
			
		||||
		TopFrontCenter = SPA_AUDIO_CHANNEL_TFC,
 | 
			
		||||
		TopFrontLeft = SPA_AUDIO_CHANNEL_TFL,
 | 
			
		||||
		TopFrontRight = SPA_AUDIO_CHANNEL_TFR,
 | 
			
		||||
		TopFrontLeftCenter = SPA_AUDIO_CHANNEL_TFLC,
 | 
			
		||||
		TopFrontRightCenter = SPA_AUDIO_CHANNEL_TFRC,
 | 
			
		||||
		TopSideLeft = SPA_AUDIO_CHANNEL_TSL,
 | 
			
		||||
		TopSideRight = SPA_AUDIO_CHANNEL_TSR,
 | 
			
		||||
		TopRearCenter = SPA_AUDIO_CHANNEL_TRC,
 | 
			
		||||
		TopRearLeft = SPA_AUDIO_CHANNEL_TRL,
 | 
			
		||||
		TopRearRight = SPA_AUDIO_CHANNEL_TRR,
 | 
			
		||||
		BottomCenter = SPA_AUDIO_CHANNEL_BC,
 | 
			
		||||
		BottomLeftCenter = SPA_AUDIO_CHANNEL_BLC,
 | 
			
		||||
		BottomRightCenter = SPA_AUDIO_CHANNEL_BRC,
 | 
			
		||||
		/// The start of the aux channel range.
 | 
			
		||||
		///
 | 
			
		||||
		/// Values between AuxRangeStart and AuxRangeEnd are valid.
 | 
			
		||||
		AuxRangeStart = SPA_AUDIO_CHANNEL_START_Aux,
 | 
			
		||||
		/// The end of the aux channel range.
 | 
			
		||||
		///
 | 
			
		||||
		/// Values between AuxRangeStart and AuxRangeEnd are valid.
 | 
			
		||||
		AuxRangeEnd = SPA_AUDIO_CHANNEL_LAST_Aux,
 | 
			
		||||
		/// The end of the custom channel range.
 | 
			
		||||
		///
 | 
			
		||||
		/// Values starting at CustomRangeStart are valid.
 | 
			
		||||
		CustomRangeStart = SPA_AUDIO_CHANNEL_START_Custom,
 | 
			
		||||
	};
 | 
			
		||||
	Q_ENUM(Enum);
 | 
			
		||||
 | 
			
		||||
	/// Print a human readable representation of the given channel,
 | 
			
		||||
	/// including aux and custom channel ranges.
 | 
			
		||||
	Q_INVOKABLE static QString toString(PwAudioChannel::Enum value);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
enum class PwNodeType {
 | 
			
		||||
	Untracked,
 | 
			
		||||
	Audio,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class PwNode;
 | 
			
		||||
 | 
			
		||||
class PwNodeBoundData {
 | 
			
		||||
public:
 | 
			
		||||
	PwNodeBoundData() = default;
 | 
			
		||||
	virtual ~PwNodeBoundData() = default;
 | 
			
		||||
	Q_DISABLE_COPY_MOVE(PwNodeBoundData);
 | 
			
		||||
 | 
			
		||||
	virtual void onInfo(const pw_node_info* /*info*/) {}
 | 
			
		||||
	virtual void onSpaParam(quint32 /*id*/, quint32 /*index*/, const spa_pod* /*param*/) {}
 | 
			
		||||
	virtual void onUnbind() {}
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class PwNodeBoundAudio
 | 
			
		||||
    : public QObject
 | 
			
		||||
    , public PwNodeBoundData {
 | 
			
		||||
	Q_OBJECT;
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
	explicit PwNodeBoundAudio(PwNode* node): node(node) {}
 | 
			
		||||
 | 
			
		||||
	void onInfo(const pw_node_info* info) override;
 | 
			
		||||
	void onSpaParam(quint32 id, quint32 index, const spa_pod* param) override;
 | 
			
		||||
	void onUnbind() override;
 | 
			
		||||
 | 
			
		||||
	[[nodiscard]] bool isMuted() const;
 | 
			
		||||
	void setMuted(bool muted);
 | 
			
		||||
 | 
			
		||||
	[[nodiscard]] float averageVolume() const;
 | 
			
		||||
	void setAverageVolume(float volume);
 | 
			
		||||
 | 
			
		||||
	[[nodiscard]] QVector<PwAudioChannel::Enum> channels() const;
 | 
			
		||||
 | 
			
		||||
	[[nodiscard]] QVector<float> volumes() const;
 | 
			
		||||
	void setVolumes(const QVector<float>& volumes);
 | 
			
		||||
 | 
			
		||||
signals:
 | 
			
		||||
	void volumesChanged();
 | 
			
		||||
	void channelsChanged();
 | 
			
		||||
	void mutedChanged();
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
	void updateVolumeFromParam(const spa_pod* param);
 | 
			
		||||
	void updateMutedFromParam(const spa_pod* param);
 | 
			
		||||
 | 
			
		||||
	bool mMuted = false;
 | 
			
		||||
	QVector<PwAudioChannel::Enum> mChannels;
 | 
			
		||||
	QVector<float> mVolumes;
 | 
			
		||||
	PwNode* node;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
constexpr const char TYPE_INTERFACE_Node[] = PW_TYPE_INTERFACE_Node;             // NOLINT
 | 
			
		||||
class PwNode: public PwBindable<pw_node, TYPE_INTERFACE_Node, PW_VERSION_NODE> { // NOLINT
 | 
			
		||||
	Q_OBJECT;
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
	void bindHooks() override;
 | 
			
		||||
	void unbindHooks() override;
 | 
			
		||||
	void initProps(const spa_dict* props) override;
 | 
			
		||||
 | 
			
		||||
	QString name;
 | 
			
		||||
	QString description;
 | 
			
		||||
	QString nick;
 | 
			
		||||
	QMap<QString, QString> properties;
 | 
			
		||||
 | 
			
		||||
	PwNodeType type = PwNodeType::Untracked;
 | 
			
		||||
	bool isSink = false;
 | 
			
		||||
	bool isStream = false;
 | 
			
		||||
 | 
			
		||||
	PwNodeBoundData* boundData = nullptr;
 | 
			
		||||
 | 
			
		||||
signals:
 | 
			
		||||
	void propertiesChanged();
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
	static const pw_node_events EVENTS;
 | 
			
		||||
	static void onInfo(void* data, const pw_node_info* info);
 | 
			
		||||
	static void
 | 
			
		||||
	onParam(void* data, qint32 seq, quint32 id, quint32 index, quint32 next, const spa_pod* param);
 | 
			
		||||
 | 
			
		||||
	SpaHook listener;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
} // namespace qs::service::pipewire
 | 
			
		||||
							
								
								
									
										472
									
								
								src/services/pipewire/qml.cpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										472
									
								
								src/services/pipewire/qml.cpp
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,472 @@
 | 
			
		|||
#include "qml.hpp"
 | 
			
		||||
 | 
			
		||||
#include <qcontainerfwd.h>
 | 
			
		||||
#include <qlist.h>
 | 
			
		||||
#include <qobject.h>
 | 
			
		||||
#include <qqmllist.h>
 | 
			
		||||
#include <qtmetamacros.h>
 | 
			
		||||
#include <qtypes.h>
 | 
			
		||||
#include <qvariant.h>
 | 
			
		||||
 | 
			
		||||
#include "connection.hpp"
 | 
			
		||||
#include "link.hpp"
 | 
			
		||||
#include "metadata.hpp"
 | 
			
		||||
#include "node.hpp"
 | 
			
		||||
#include "registry.hpp"
 | 
			
		||||
 | 
			
		||||
namespace qs::service::pipewire {
 | 
			
		||||
 | 
			
		||||
void PwObjectIface::ref() {
 | 
			
		||||
	this->refcount++;
 | 
			
		||||
 | 
			
		||||
	if (this->refcount == 1) {
 | 
			
		||||
		this->object->ref();
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void PwObjectIface::unref() {
 | 
			
		||||
	if (this->refcount == 0) return;
 | 
			
		||||
	this->refcount--;
 | 
			
		||||
 | 
			
		||||
	if (this->refcount == 0) {
 | 
			
		||||
		this->object->unref();
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Pipewire::Pipewire(QObject* parent): QObject(parent) {
 | 
			
		||||
	auto* connection = PwConnection::instance();
 | 
			
		||||
 | 
			
		||||
	for (auto* node: connection->registry.nodes.values()) {
 | 
			
		||||
		this->onNodeAdded(node);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	QObject::connect(&connection->registry, &PwRegistry::nodeAdded, this, &Pipewire::onNodeAdded);
 | 
			
		||||
 | 
			
		||||
	for (auto* link: connection->registry.links.values()) {
 | 
			
		||||
		this->onLinkAdded(link);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	QObject::connect(&connection->registry, &PwRegistry::linkAdded, this, &Pipewire::onLinkAdded);
 | 
			
		||||
 | 
			
		||||
	for (auto* group: connection->registry.linkGroups) {
 | 
			
		||||
		this->onLinkGroupAdded(group);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	QObject::connect(
 | 
			
		||||
	    &connection->registry,
 | 
			
		||||
	    &PwRegistry::linkGroupAdded,
 | 
			
		||||
	    this,
 | 
			
		||||
	    &Pipewire::onLinkGroupAdded
 | 
			
		||||
	);
 | 
			
		||||
 | 
			
		||||
	// clang-format off
 | 
			
		||||
	QObject::connect(&connection->defaults, &PwDefaultsMetadata::defaultSinkChanged, this, &Pipewire::defaultAudioSinkChanged);
 | 
			
		||||
	QObject::connect(&connection->defaults, &PwDefaultsMetadata::defaultSourceChanged, this, &Pipewire::defaultAudioSourceChanged);
 | 
			
		||||
	// clang-format on
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
QQmlListProperty<PwNodeIface> Pipewire::nodes() {
 | 
			
		||||
	return QQmlListProperty<PwNodeIface>(this, nullptr, &Pipewire::nodesCount, &Pipewire::nodeAt);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
qsizetype Pipewire::nodesCount(QQmlListProperty<PwNodeIface>* property) {
 | 
			
		||||
	return static_cast<Pipewire*>(property->object)->mNodes.count(); // NOLINT
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
PwNodeIface* Pipewire::nodeAt(QQmlListProperty<PwNodeIface>* property, qsizetype index) {
 | 
			
		||||
	return static_cast<Pipewire*>(property->object)->mNodes.at(index); // NOLINT
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Pipewire::onNodeAdded(PwNode* node) {
 | 
			
		||||
	auto* iface = PwNodeIface::instance(node);
 | 
			
		||||
	QObject::connect(iface, &QObject::destroyed, this, &Pipewire::onNodeRemoved);
 | 
			
		||||
 | 
			
		||||
	this->mNodes.push_back(iface);
 | 
			
		||||
	emit this->nodesChanged();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Pipewire::onNodeRemoved(QObject* object) {
 | 
			
		||||
	auto* iface = static_cast<PwNodeIface*>(object); // NOLINT
 | 
			
		||||
	this->mNodes.removeOne(iface);
 | 
			
		||||
	emit this->nodesChanged();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
QQmlListProperty<PwLinkIface> Pipewire::links() {
 | 
			
		||||
	return QQmlListProperty<PwLinkIface>(this, nullptr, &Pipewire::linksCount, &Pipewire::linkAt);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
qsizetype Pipewire::linksCount(QQmlListProperty<PwLinkIface>* property) {
 | 
			
		||||
	return static_cast<Pipewire*>(property->object)->mLinks.count(); // NOLINT
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
PwLinkIface* Pipewire::linkAt(QQmlListProperty<PwLinkIface>* property, qsizetype index) {
 | 
			
		||||
	return static_cast<Pipewire*>(property->object)->mLinks.at(index); // NOLINT
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Pipewire::onLinkAdded(PwLink* link) {
 | 
			
		||||
	auto* iface = PwLinkIface::instance(link);
 | 
			
		||||
	QObject::connect(iface, &QObject::destroyed, this, &Pipewire::onLinkRemoved);
 | 
			
		||||
 | 
			
		||||
	this->mLinks.push_back(iface);
 | 
			
		||||
	emit this->linksChanged();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Pipewire::onLinkRemoved(QObject* object) {
 | 
			
		||||
	auto* iface = static_cast<PwLinkIface*>(object); // NOLINT
 | 
			
		||||
	this->mLinks.removeOne(iface);
 | 
			
		||||
	emit this->linksChanged();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
QQmlListProperty<PwLinkGroupIface> Pipewire::linkGroups() {
 | 
			
		||||
	return QQmlListProperty<PwLinkGroupIface>(
 | 
			
		||||
	    this,
 | 
			
		||||
	    nullptr,
 | 
			
		||||
	    &Pipewire::linkGroupsCount,
 | 
			
		||||
	    &Pipewire::linkGroupAt
 | 
			
		||||
	);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
qsizetype Pipewire::linkGroupsCount(QQmlListProperty<PwLinkGroupIface>* property) {
 | 
			
		||||
	return static_cast<Pipewire*>(property->object)->mLinkGroups.count(); // NOLINT
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
PwLinkGroupIface*
 | 
			
		||||
Pipewire::linkGroupAt(QQmlListProperty<PwLinkGroupIface>* property, qsizetype index) {
 | 
			
		||||
	return static_cast<Pipewire*>(property->object)->mLinkGroups.at(index); // NOLINT
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Pipewire::onLinkGroupAdded(PwLinkGroup* linkGroup) {
 | 
			
		||||
	auto* iface = PwLinkGroupIface::instance(linkGroup);
 | 
			
		||||
	QObject::connect(iface, &QObject::destroyed, this, &Pipewire::onLinkGroupRemoved);
 | 
			
		||||
 | 
			
		||||
	this->mLinkGroups.push_back(iface);
 | 
			
		||||
	emit this->linkGroupsChanged();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Pipewire::onLinkGroupRemoved(QObject* object) {
 | 
			
		||||
	auto* iface = static_cast<PwLinkGroupIface*>(object); // NOLINT
 | 
			
		||||
	this->mLinkGroups.removeOne(iface);
 | 
			
		||||
	emit this->linkGroupsChanged();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
PwNodeIface* Pipewire::defaultAudioSink() const { // NOLINT
 | 
			
		||||
	auto* connection = PwConnection::instance();
 | 
			
		||||
	auto name = connection->defaults.defaultSink();
 | 
			
		||||
 | 
			
		||||
	for (auto* node: connection->registry.nodes.values()) {
 | 
			
		||||
		if (name == node->name) {
 | 
			
		||||
			return PwNodeIface::instance(node);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nullptr;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
PwNodeIface* Pipewire::defaultAudioSource() const { // NOLINT
 | 
			
		||||
	auto* connection = PwConnection::instance();
 | 
			
		||||
	auto name = connection->defaults.defaultSource();
 | 
			
		||||
 | 
			
		||||
	for (auto* node: connection->registry.nodes.values()) {
 | 
			
		||||
		if (name == node->name) {
 | 
			
		||||
			return PwNodeIface::instance(node);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nullptr;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
PwNodeIface* PwNodeLinkTracker::node() const { return this->mNode; }
 | 
			
		||||
 | 
			
		||||
void PwNodeLinkTracker::setNode(PwNodeIface* node) {
 | 
			
		||||
	if (node == this->mNode) return;
 | 
			
		||||
 | 
			
		||||
	if (this->mNode != nullptr) {
 | 
			
		||||
		if (node == nullptr) {
 | 
			
		||||
			QObject::disconnect(&PwConnection::instance()->registry, nullptr, this, nullptr);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		QObject::disconnect(this->mNode, nullptr, this, nullptr);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (node != nullptr) {
 | 
			
		||||
		if (this->mNode == nullptr) {
 | 
			
		||||
			QObject::connect(
 | 
			
		||||
			    &PwConnection::instance()->registry,
 | 
			
		||||
			    &PwRegistry::linkGroupAdded,
 | 
			
		||||
			    this,
 | 
			
		||||
			    &PwNodeLinkTracker::onLinkGroupCreated
 | 
			
		||||
			);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		QObject::connect(node, &QObject::destroyed, this, &PwNodeLinkTracker::onNodeDestroyed);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	this->mNode = node;
 | 
			
		||||
	this->updateLinks();
 | 
			
		||||
	emit this->nodeChanged();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void PwNodeLinkTracker::updateLinks() {
 | 
			
		||||
	// done first to avoid unref->reref of nodes
 | 
			
		||||
	auto newLinks = QVector<PwLinkGroupIface*>();
 | 
			
		||||
	if (this->mNode != nullptr) {
 | 
			
		||||
		auto* connection = PwConnection::instance();
 | 
			
		||||
 | 
			
		||||
		for (auto* link: connection->registry.linkGroups) {
 | 
			
		||||
			if ((!this->mNode->isSink() && link->outputNode() == this->mNode->id())
 | 
			
		||||
			    || (this->mNode->isSink() && link->inputNode() == this->mNode->id()))
 | 
			
		||||
			{
 | 
			
		||||
				auto* iface = PwLinkGroupIface::instance(link);
 | 
			
		||||
 | 
			
		||||
				// do not connect twice
 | 
			
		||||
				if (!this->mLinkGroups.contains(iface)) {
 | 
			
		||||
					QObject::connect(
 | 
			
		||||
					    iface,
 | 
			
		||||
					    &QObject::destroyed,
 | 
			
		||||
					    this,
 | 
			
		||||
					    &PwNodeLinkTracker::onLinkGroupDestroyed
 | 
			
		||||
					);
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				newLinks.push_back(iface);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for (auto* iface: this->mLinkGroups) {
 | 
			
		||||
		// only disconnect no longer used nodes
 | 
			
		||||
		if (!newLinks.contains(iface)) {
 | 
			
		||||
			QObject::disconnect(iface, nullptr, this, nullptr);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	this->mLinkGroups = newLinks;
 | 
			
		||||
	emit this->linkGroupsChanged();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
QQmlListProperty<PwLinkGroupIface> PwNodeLinkTracker::linkGroups() {
 | 
			
		||||
	return QQmlListProperty<PwLinkGroupIface>(
 | 
			
		||||
	    this,
 | 
			
		||||
	    nullptr,
 | 
			
		||||
	    &PwNodeLinkTracker::linkGroupsCount,
 | 
			
		||||
	    &PwNodeLinkTracker::linkGroupAt
 | 
			
		||||
	);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
qsizetype PwNodeLinkTracker::linkGroupsCount(QQmlListProperty<PwLinkGroupIface>* property) {
 | 
			
		||||
	return static_cast<PwNodeLinkTracker*>(property->object)->mLinkGroups.count(); // NOLINT
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
PwLinkGroupIface*
 | 
			
		||||
PwNodeLinkTracker::linkGroupAt(QQmlListProperty<PwLinkGroupIface>* property, qsizetype index) {
 | 
			
		||||
	return static_cast<PwNodeLinkTracker*>(property->object)->mLinkGroups.at(index); // NOLINT
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void PwNodeLinkTracker::onNodeDestroyed() {
 | 
			
		||||
	this->mNode = nullptr;
 | 
			
		||||
	this->updateLinks();
 | 
			
		||||
	emit this->nodeChanged();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void PwNodeLinkTracker::onLinkGroupCreated(PwLinkGroup* linkGroup) {
 | 
			
		||||
	if ((!this->mNode->isSink() && linkGroup->outputNode() == this->mNode->id())
 | 
			
		||||
	    || (this->mNode->isSink() && linkGroup->inputNode() == this->mNode->id()))
 | 
			
		||||
	{
 | 
			
		||||
		auto* iface = PwLinkGroupIface::instance(linkGroup);
 | 
			
		||||
		QObject::connect(iface, &QObject::destroyed, this, &PwNodeLinkTracker::onLinkGroupDestroyed);
 | 
			
		||||
		this->mLinkGroups.push_back(iface);
 | 
			
		||||
		emit this->linkGroupsChanged();
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void PwNodeLinkTracker::onLinkGroupDestroyed(QObject* object) {
 | 
			
		||||
	if (this->mLinkGroups.removeOne(object)) {
 | 
			
		||||
		emit this->linkGroupsChanged();
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
PwNodeAudioIface::PwNodeAudioIface(PwNodeBoundAudio* boundData, QObject* parent)
 | 
			
		||||
    : QObject(parent)
 | 
			
		||||
    , boundData(boundData) {
 | 
			
		||||
	// clang-format off
 | 
			
		||||
	QObject::connect(boundData, &PwNodeBoundAudio::mutedChanged, this, &PwNodeAudioIface::mutedChanged);
 | 
			
		||||
	QObject::connect(boundData, &PwNodeBoundAudio::channelsChanged, this, &PwNodeAudioIface::channelsChanged);
 | 
			
		||||
	QObject::connect(boundData, &PwNodeBoundAudio::volumesChanged, this, &PwNodeAudioIface::volumesChanged);
 | 
			
		||||
	// clang-format on
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool PwNodeAudioIface::isMuted() const { return this->boundData->isMuted(); }
 | 
			
		||||
 | 
			
		||||
void PwNodeAudioIface::setMuted(bool muted) { this->boundData->setMuted(muted); }
 | 
			
		||||
 | 
			
		||||
float PwNodeAudioIface::averageVolume() const { return this->boundData->averageVolume(); }
 | 
			
		||||
 | 
			
		||||
void PwNodeAudioIface::setAverageVolume(float volume) { this->boundData->setAverageVolume(volume); }
 | 
			
		||||
 | 
			
		||||
QVector<PwAudioChannel::Enum> PwNodeAudioIface::channels() const {
 | 
			
		||||
	return this->boundData->channels();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
QVector<float> PwNodeAudioIface::volumes() const { return this->boundData->volumes(); }
 | 
			
		||||
 | 
			
		||||
void PwNodeAudioIface::setVolumes(const QVector<float>& volumes) {
 | 
			
		||||
	this->boundData->setVolumes(volumes);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
PwNodeIface::PwNodeIface(PwNode* node): PwObjectIface(node), mNode(node) {
 | 
			
		||||
	QObject::connect(node, &PwNode::propertiesChanged, this, &PwNodeIface::propertiesChanged);
 | 
			
		||||
 | 
			
		||||
	if (auto* audioBoundData = dynamic_cast<PwNodeBoundAudio*>(node->boundData)) {
 | 
			
		||||
		this->audioIface = new PwNodeAudioIface(audioBoundData, this);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
PwNode* PwNodeIface::node() const { return this->mNode; }
 | 
			
		||||
 | 
			
		||||
QString PwNodeIface::name() const { return this->mNode->name; }
 | 
			
		||||
 | 
			
		||||
quint32 PwNodeIface::id() const { return this->mNode->id; }
 | 
			
		||||
 | 
			
		||||
QString PwNodeIface::description() const { return this->mNode->description; }
 | 
			
		||||
 | 
			
		||||
QString PwNodeIface::nickname() const { return this->mNode->nick; }
 | 
			
		||||
 | 
			
		||||
bool PwNodeIface::isSink() const { return this->mNode->isSink; }
 | 
			
		||||
 | 
			
		||||
bool PwNodeIface::isStream() const { return this->mNode->isStream; }
 | 
			
		||||
 | 
			
		||||
QVariantMap PwNodeIface::properties() const {
 | 
			
		||||
	auto map = QVariantMap();
 | 
			
		||||
	for (auto [k, v]: this->mNode->properties.asKeyValueRange()) {
 | 
			
		||||
		map.insert(k, QVariant::fromValue(v));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return map;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
PwNodeAudioIface* PwNodeIface::audio() const { return this->audioIface; }
 | 
			
		||||
 | 
			
		||||
PwNodeIface* PwNodeIface::instance(PwNode* node) {
 | 
			
		||||
	auto v = node->property("iface");
 | 
			
		||||
	if (v.canConvert<PwNodeIface*>()) {
 | 
			
		||||
		return v.value<PwNodeIface*>();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	auto* instance = new PwNodeIface(node);
 | 
			
		||||
	node->setProperty("iface", QVariant::fromValue(instance));
 | 
			
		||||
 | 
			
		||||
	return instance;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
PwLinkIface::PwLinkIface(PwLink* link): PwObjectIface(link), mLink(link) {
 | 
			
		||||
	QObject::connect(link, &PwLink::stateChanged, this, &PwLinkIface::stateChanged);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
PwLink* PwLinkIface::link() const { return this->mLink; }
 | 
			
		||||
 | 
			
		||||
quint32 PwLinkIface::id() const { return this->mLink->id; }
 | 
			
		||||
 | 
			
		||||
PwNodeIface* PwLinkIface::target() const {
 | 
			
		||||
	return PwNodeIface::instance(
 | 
			
		||||
	    PwConnection::instance()->registry.nodes.value(this->mLink->inputNode())
 | 
			
		||||
	);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
PwNodeIface* PwLinkIface::source() const {
 | 
			
		||||
	return PwNodeIface::instance(
 | 
			
		||||
	    PwConnection::instance()->registry.nodes.value(this->mLink->outputNode())
 | 
			
		||||
	);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
PwLinkState::Enum PwLinkIface::state() const { return this->mLink->state(); }
 | 
			
		||||
 | 
			
		||||
PwLinkIface* PwLinkIface::instance(PwLink* link) {
 | 
			
		||||
	auto v = link->property("iface");
 | 
			
		||||
	if (v.canConvert<PwLinkIface*>()) {
 | 
			
		||||
		return v.value<PwLinkIface*>();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	auto* instance = new PwLinkIface(link);
 | 
			
		||||
	link->setProperty("iface", QVariant::fromValue(instance));
 | 
			
		||||
 | 
			
		||||
	return instance;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
PwLinkGroupIface::PwLinkGroupIface(PwLinkGroup* group): QObject(group), mGroup(group) {
 | 
			
		||||
	QObject::connect(group, &PwLinkGroup::stateChanged, this, &PwLinkGroupIface::stateChanged);
 | 
			
		||||
	QObject::connect(group, &QObject::destroyed, this, [this]() { delete this; });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void PwLinkGroupIface::ref() { this->mGroup->ref(); }
 | 
			
		||||
 | 
			
		||||
void PwLinkGroupIface::unref() { this->mGroup->unref(); }
 | 
			
		||||
 | 
			
		||||
PwLinkGroup* PwLinkGroupIface::group() const { return this->mGroup; }
 | 
			
		||||
 | 
			
		||||
PwNodeIface* PwLinkGroupIface::target() const {
 | 
			
		||||
	return PwNodeIface::instance(
 | 
			
		||||
	    PwConnection::instance()->registry.nodes.value(this->mGroup->inputNode())
 | 
			
		||||
	);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
PwNodeIface* PwLinkGroupIface::source() const {
 | 
			
		||||
	return PwNodeIface::instance(
 | 
			
		||||
	    PwConnection::instance()->registry.nodes.value(this->mGroup->outputNode())
 | 
			
		||||
	);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
PwLinkState::Enum PwLinkGroupIface::state() const { return this->mGroup->state(); }
 | 
			
		||||
 | 
			
		||||
PwLinkGroupIface* PwLinkGroupIface::instance(PwLinkGroup* group) {
 | 
			
		||||
	auto v = group->property("iface");
 | 
			
		||||
	if (v.canConvert<PwLinkGroupIface*>()) {
 | 
			
		||||
		return v.value<PwLinkGroupIface*>();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	auto* instance = new PwLinkGroupIface(group);
 | 
			
		||||
	group->setProperty("iface", QVariant::fromValue(instance));
 | 
			
		||||
 | 
			
		||||
	return instance;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
PwObjectTracker::~PwObjectTracker() { this->clearList(); }
 | 
			
		||||
 | 
			
		||||
QList<QObject*> PwObjectTracker::objects() const { return this->trackedObjects; }
 | 
			
		||||
 | 
			
		||||
void PwObjectTracker::setObjects(const QList<QObject*>& objects) {
 | 
			
		||||
	// +1 ref before removing old refs to avoid an unbind->bind.
 | 
			
		||||
	for (auto* object: objects) {
 | 
			
		||||
		if (auto* pwObject = dynamic_cast<PwObjectRefIface*>(object)) {
 | 
			
		||||
			pwObject->ref();
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	this->clearList();
 | 
			
		||||
 | 
			
		||||
	// connect destroy
 | 
			
		||||
	for (auto* object: objects) {
 | 
			
		||||
		if (auto* pwObject = dynamic_cast<PwObjectRefIface*>(object)) {
 | 
			
		||||
			QObject::connect(object, &QObject::destroyed, this, &PwObjectTracker::objectDestroyed);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	this->trackedObjects = objects;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void PwObjectTracker::clearList() {
 | 
			
		||||
	for (auto* object: this->trackedObjects) {
 | 
			
		||||
		if (auto* pwObject = dynamic_cast<PwObjectRefIface*>(object)) {
 | 
			
		||||
			pwObject->unref();
 | 
			
		||||
			QObject::disconnect(object, nullptr, this, nullptr);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	this->trackedObjects.clear();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void PwObjectTracker::objectDestroyed(QObject* object) {
 | 
			
		||||
	this->trackedObjects.removeOne(object);
 | 
			
		||||
	emit this->objectsChanged();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
} // namespace qs::service::pipewire
 | 
			
		||||
							
								
								
									
										368
									
								
								src/services/pipewire/qml.hpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										368
									
								
								src/services/pipewire/qml.hpp
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,368 @@
 | 
			
		|||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include <qcontainerfwd.h>
 | 
			
		||||
#include <qobject.h>
 | 
			
		||||
#include <qqmlintegration.h>
 | 
			
		||||
#include <qqmllist.h>
 | 
			
		||||
#include <qtclasshelpermacros.h>
 | 
			
		||||
#include <qtmetamacros.h>
 | 
			
		||||
#include <qtypes.h>
 | 
			
		||||
 | 
			
		||||
#include "link.hpp"
 | 
			
		||||
#include "node.hpp"
 | 
			
		||||
#include "registry.hpp"
 | 
			
		||||
 | 
			
		||||
namespace qs::service::pipewire {
 | 
			
		||||
 | 
			
		||||
class PwNodeIface;
 | 
			
		||||
class PwLinkIface;
 | 
			
		||||
class PwLinkGroupIface;
 | 
			
		||||
 | 
			
		||||
class PwObjectRefIface {
 | 
			
		||||
public:
 | 
			
		||||
	PwObjectRefIface() = default;
 | 
			
		||||
	virtual ~PwObjectRefIface() = default;
 | 
			
		||||
	Q_DISABLE_COPY_MOVE(PwObjectRefIface);
 | 
			
		||||
 | 
			
		||||
	virtual void ref() = 0;
 | 
			
		||||
	virtual void unref() = 0;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class PwObjectIface
 | 
			
		||||
    : public QObject
 | 
			
		||||
    , public PwObjectRefIface {
 | 
			
		||||
	Q_OBJECT;
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
	explicit PwObjectIface(PwBindableObject* object): QObject(object), object(object) {};
 | 
			
		||||
	// destructor should ONLY be called by the pw object destructor, making an unref unnecessary
 | 
			
		||||
	~PwObjectIface() override = default;
 | 
			
		||||
	Q_DISABLE_COPY_MOVE(PwObjectIface);
 | 
			
		||||
 | 
			
		||||
	void ref() override;
 | 
			
		||||
	void unref() override;
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
	quint32 refcount = 0;
 | 
			
		||||
	PwBindableObject* object;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
///! Contains links to all pipewire objects.
 | 
			
		||||
class Pipewire: public QObject {
 | 
			
		||||
	Q_OBJECT;
 | 
			
		||||
	// clang-format off
 | 
			
		||||
	/// All pipewire nodes.
 | 
			
		||||
	Q_PROPERTY(QQmlListProperty<PwNodeIface> nodes READ nodes NOTIFY nodesChanged);
 | 
			
		||||
	/// All pipewire links.
 | 
			
		||||
	Q_PROPERTY(QQmlListProperty<PwLinkIface> links READ links NOTIFY linksChanged);
 | 
			
		||||
	/// All pipewire link groups.
 | 
			
		||||
	Q_PROPERTY(QQmlListProperty<PwLinkGroupIface> linkGroups READ linkGroups NOTIFY linkGroupsChanged);
 | 
			
		||||
	/// The default audio sink or `null`.
 | 
			
		||||
	Q_PROPERTY(PwNodeIface* defaultAudioSink READ defaultAudioSink NOTIFY defaultAudioSinkChanged);
 | 
			
		||||
	/// The default audio source or `null`.
 | 
			
		||||
	Q_PROPERTY(PwNodeIface* defaultAudioSource READ defaultAudioSource NOTIFY defaultAudioSourceChanged);
 | 
			
		||||
	// clang-format on
 | 
			
		||||
	QML_ELEMENT;
 | 
			
		||||
	QML_SINGLETON;
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
	explicit Pipewire(QObject* parent = nullptr);
 | 
			
		||||
 | 
			
		||||
	[[nodiscard]] QQmlListProperty<PwNodeIface> nodes();
 | 
			
		||||
	[[nodiscard]] QQmlListProperty<PwLinkIface> links();
 | 
			
		||||
	[[nodiscard]] QQmlListProperty<PwLinkGroupIface> linkGroups();
 | 
			
		||||
	[[nodiscard]] PwNodeIface* defaultAudioSink() const;
 | 
			
		||||
	[[nodiscard]] PwNodeIface* defaultAudioSource() const;
 | 
			
		||||
 | 
			
		||||
signals:
 | 
			
		||||
	void nodesChanged();
 | 
			
		||||
	void linksChanged();
 | 
			
		||||
	void linkGroupsChanged();
 | 
			
		||||
	void defaultAudioSinkChanged();
 | 
			
		||||
	void defaultAudioSourceChanged();
 | 
			
		||||
 | 
			
		||||
private slots:
 | 
			
		||||
	void onNodeAdded(PwNode* node);
 | 
			
		||||
	void onNodeRemoved(QObject* object);
 | 
			
		||||
	void onLinkAdded(PwLink* link);
 | 
			
		||||
	void onLinkRemoved(QObject* object);
 | 
			
		||||
	void onLinkGroupAdded(PwLinkGroup* group);
 | 
			
		||||
	void onLinkGroupRemoved(QObject* object);
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
	static qsizetype nodesCount(QQmlListProperty<PwNodeIface>* property);
 | 
			
		||||
	static PwNodeIface* nodeAt(QQmlListProperty<PwNodeIface>* property, qsizetype index);
 | 
			
		||||
	static qsizetype linksCount(QQmlListProperty<PwLinkIface>* property);
 | 
			
		||||
	static PwLinkIface* linkAt(QQmlListProperty<PwLinkIface>* property, qsizetype index);
 | 
			
		||||
	static qsizetype linkGroupsCount(QQmlListProperty<PwLinkGroupIface>* property);
 | 
			
		||||
	static PwLinkGroupIface*
 | 
			
		||||
	linkGroupAt(QQmlListProperty<PwLinkGroupIface>* property, qsizetype index);
 | 
			
		||||
 | 
			
		||||
	QVector<PwNodeIface*> mNodes;
 | 
			
		||||
	QVector<PwLinkIface*> mLinks;
 | 
			
		||||
	QVector<PwLinkGroupIface*> mLinkGroups;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
///! Tracks all link connections to a given node.
 | 
			
		||||
class PwNodeLinkTracker: public QObject {
 | 
			
		||||
	Q_OBJECT;
 | 
			
		||||
	// clang-format off
 | 
			
		||||
	/// The node to track connections to.
 | 
			
		||||
	Q_PROPERTY(PwNodeIface* node READ node WRITE setNode NOTIFY nodeChanged);
 | 
			
		||||
	/// Link groups connected to the given node.
 | 
			
		||||
	///
 | 
			
		||||
	/// If the node is a sink, links which target the node will be tracked.
 | 
			
		||||
	/// If the node is a source, links which source the node will be tracked.
 | 
			
		||||
	Q_PROPERTY(QQmlListProperty<PwLinkGroupIface> linkGroups READ linkGroups NOTIFY linkGroupsChanged);
 | 
			
		||||
	// clang-format on
 | 
			
		||||
	QML_ELEMENT;
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
	explicit PwNodeLinkTracker(QObject* parent = nullptr): QObject(parent) {}
 | 
			
		||||
 | 
			
		||||
	[[nodiscard]] PwNodeIface* node() const;
 | 
			
		||||
	void setNode(PwNodeIface* node);
 | 
			
		||||
 | 
			
		||||
	[[nodiscard]] QQmlListProperty<PwLinkGroupIface> linkGroups();
 | 
			
		||||
 | 
			
		||||
signals:
 | 
			
		||||
	void nodeChanged();
 | 
			
		||||
	void linkGroupsChanged();
 | 
			
		||||
 | 
			
		||||
private slots:
 | 
			
		||||
	void onNodeDestroyed();
 | 
			
		||||
	void onLinkGroupCreated(PwLinkGroup* linkGroup);
 | 
			
		||||
	void onLinkGroupDestroyed(QObject* object);
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
	static qsizetype linkGroupsCount(QQmlListProperty<PwLinkGroupIface>* property);
 | 
			
		||||
	static PwLinkGroupIface*
 | 
			
		||||
	linkGroupAt(QQmlListProperty<PwLinkGroupIface>* property, qsizetype index);
 | 
			
		||||
 | 
			
		||||
	void updateLinks();
 | 
			
		||||
 | 
			
		||||
	PwNodeIface* mNode = nullptr;
 | 
			
		||||
	QVector<PwLinkGroupIface*> mLinkGroups;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
///! Audio specific properties of pipewire nodes.
 | 
			
		||||
class PwNodeAudioIface: public QObject {
 | 
			
		||||
	Q_OBJECT;
 | 
			
		||||
	/// If the node is currently muted. Setting this property changes the mute state.
 | 
			
		||||
	///
 | 
			
		||||
	/// **This property is invalid unless the node is [bound](../pwobjecttracker).**
 | 
			
		||||
	Q_PROPERTY(bool muted READ isMuted WRITE setMuted NOTIFY mutedChanged);
 | 
			
		||||
	/// The average volume over all channels of the node.
 | 
			
		||||
	/// Setting this property modifies the volume of all channels proportionately.
 | 
			
		||||
	///
 | 
			
		||||
	/// **This property is invalid unless the node is [bound](../pwobjecttracker).**
 | 
			
		||||
	Q_PROPERTY(float volume READ averageVolume WRITE setAverageVolume NOTIFY volumesChanged);
 | 
			
		||||
	/// The audio channels present on the node.
 | 
			
		||||
	///
 | 
			
		||||
	/// **This property is invalid unless the node is [bound](../pwobjecttracker).**
 | 
			
		||||
	Q_PROPERTY(QVector<PwAudioChannel::Enum> channels READ channels NOTIFY channelsChanged);
 | 
			
		||||
	/// The volumes of each audio channel individually. Each entry corrosponds to
 | 
			
		||||
	/// the channel at the same index in `channels`. `volumes` and `channels` will always be
 | 
			
		||||
	/// the same length.
 | 
			
		||||
	///
 | 
			
		||||
	/// **This property is invalid unless the node is [bound](../pwobjecttracker).**
 | 
			
		||||
	Q_PROPERTY(QVector<float> volumes READ volumes WRITE setVolumes NOTIFY volumesChanged);
 | 
			
		||||
	QML_NAMED_ELEMENT(PwNodeAudio);
 | 
			
		||||
	QML_UNCREATABLE("PwNodeAudio cannot be created directly");
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
	explicit PwNodeAudioIface(PwNodeBoundAudio* boundData, QObject* parent);
 | 
			
		||||
 | 
			
		||||
	[[nodiscard]] bool isMuted() const;
 | 
			
		||||
	void setMuted(bool muted);
 | 
			
		||||
 | 
			
		||||
	[[nodiscard]] float averageVolume() const;
 | 
			
		||||
	void setAverageVolume(float volume);
 | 
			
		||||
 | 
			
		||||
	[[nodiscard]] QVector<PwAudioChannel::Enum> channels() const;
 | 
			
		||||
 | 
			
		||||
	[[nodiscard]] QVector<float> volumes() const;
 | 
			
		||||
	void setVolumes(const QVector<float>& volumes);
 | 
			
		||||
 | 
			
		||||
signals:
 | 
			
		||||
	void mutedChanged();
 | 
			
		||||
	void channelsChanged();
 | 
			
		||||
	void volumesChanged();
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
	PwNodeBoundAudio* boundData;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
///! A node in the pipewire connection graph.
 | 
			
		||||
class PwNodeIface: public PwObjectIface {
 | 
			
		||||
	Q_OBJECT;
 | 
			
		||||
	/// The pipewire object id of the node.
 | 
			
		||||
	///
 | 
			
		||||
	/// Mainly useful for debugging. you can inspect the node directly
 | 
			
		||||
	/// with `pw-cli i <id>`.
 | 
			
		||||
	Q_PROPERTY(quint32 id READ id CONSTANT);
 | 
			
		||||
	/// The node's name, corrosponding to the object's `node.name` property.
 | 
			
		||||
	Q_PROPERTY(QString name READ name CONSTANT);
 | 
			
		||||
	/// The node's description, corrosponding to the object's `node.description` property.
 | 
			
		||||
	///
 | 
			
		||||
	/// May be empty. Generally more human readable than `name`.
 | 
			
		||||
	Q_PROPERTY(QString description READ description CONSTANT);
 | 
			
		||||
	/// The node's nickname, corrosponding to the object's `node.nickname` property.
 | 
			
		||||
	///
 | 
			
		||||
	/// May be empty. Generally but not always more human readable than `description`.
 | 
			
		||||
	Q_PROPERTY(QString nickname READ nickname CONSTANT);
 | 
			
		||||
	/// If `true`, then the node accepts audio input from other nodes,
 | 
			
		||||
	/// if `false` the node outputs audio to other nodes.
 | 
			
		||||
	Q_PROPERTY(bool isSink READ isSink CONSTANT);
 | 
			
		||||
	/// If `true` then the node is likely to be a program, if false it is liekly to be hardware.
 | 
			
		||||
	Q_PROPERTY(bool isStream READ isStream CONSTANT);
 | 
			
		||||
	/// The property set present on the node, as an object containing key-value pairs.
 | 
			
		||||
	/// You can inspect this directly with `pw-cli i <id>`.
 | 
			
		||||
	///
 | 
			
		||||
	/// A few properties of note, which may or may not be present:
 | 
			
		||||
	/// - `application.name` - A suggested human readable name for the node.
 | 
			
		||||
	/// - `application.icon-name` - The name of an icon recommended to display for the node.
 | 
			
		||||
	/// - `media.name` - A description of the currently playing media.
 | 
			
		||||
	///   (more likely to be present than `media.title` and `media.artist`)
 | 
			
		||||
	/// - `media.title` - The title of the currently playing media.
 | 
			
		||||
	/// - `media.artist` - The artist of the currently playing media.
 | 
			
		||||
	///
 | 
			
		||||
	/// **This property is invalid unless the node is [bound](../pwobjecttracker).**
 | 
			
		||||
	Q_PROPERTY(QVariantMap properties READ properties NOTIFY propertiesChanged);
 | 
			
		||||
	/// Extra information present only if the node sends or receives audio.
 | 
			
		||||
	Q_PROPERTY(PwNodeAudioIface* audio READ audio CONSTANT);
 | 
			
		||||
	QML_NAMED_ELEMENT(PwNode);
 | 
			
		||||
	QML_UNCREATABLE("PwNodes cannot be created directly");
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
	explicit PwNodeIface(PwNode* node);
 | 
			
		||||
 | 
			
		||||
	[[nodiscard]] PwNode* node() const;
 | 
			
		||||
	[[nodiscard]] quint32 id() const;
 | 
			
		||||
	[[nodiscard]] QString name() const;
 | 
			
		||||
	[[nodiscard]] QString description() const;
 | 
			
		||||
	[[nodiscard]] QString nickname() const;
 | 
			
		||||
	[[nodiscard]] bool isSink() const;
 | 
			
		||||
	[[nodiscard]] bool isStream() const;
 | 
			
		||||
	[[nodiscard]] QVariantMap properties() const;
 | 
			
		||||
	[[nodiscard]] PwNodeAudioIface* audio() const;
 | 
			
		||||
 | 
			
		||||
	static PwNodeIface* instance(PwNode* node);
 | 
			
		||||
 | 
			
		||||
signals:
 | 
			
		||||
	void propertiesChanged();
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
	PwNode* mNode;
 | 
			
		||||
	PwNodeAudioIface* audioIface = nullptr;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
///! A connection between pipewire nodes.
 | 
			
		||||
/// Note that there is one link per *channel* of a connection between nodes.
 | 
			
		||||
/// You usually want [PwLinkGroup](../pwlinkgroup).
 | 
			
		||||
class PwLinkIface: public PwObjectIface {
 | 
			
		||||
	Q_OBJECT;
 | 
			
		||||
	/// The pipewire object id of the link.
 | 
			
		||||
	///
 | 
			
		||||
	/// Mainly useful for debugging. you can inspect the link directly
 | 
			
		||||
	/// with `pw-cli i <id>`.
 | 
			
		||||
	Q_PROPERTY(quint32 id READ id CONSTANT);
 | 
			
		||||
	/// The node that is *receiving* information. (the sink)
 | 
			
		||||
	Q_PROPERTY(PwNodeIface* target READ target CONSTANT);
 | 
			
		||||
	/// The node that is *sending* information. (the source)
 | 
			
		||||
	Q_PROPERTY(PwNodeIface* source READ source CONSTANT);
 | 
			
		||||
	/// The current state of the link.
 | 
			
		||||
	///
 | 
			
		||||
	/// **This property is invalid unless the link is [bound](../pwobjecttracker).**
 | 
			
		||||
	Q_PROPERTY(PwLinkState::Enum state READ state NOTIFY stateChanged);
 | 
			
		||||
	QML_NAMED_ELEMENT(PwLink);
 | 
			
		||||
	QML_UNCREATABLE("PwLinks cannot be created directly");
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
	explicit PwLinkIface(PwLink* link);
 | 
			
		||||
 | 
			
		||||
	[[nodiscard]] PwLink* link() const;
 | 
			
		||||
	[[nodiscard]] quint32 id() const;
 | 
			
		||||
	[[nodiscard]] PwNodeIface* target() const;
 | 
			
		||||
	[[nodiscard]] PwNodeIface* source() const;
 | 
			
		||||
	[[nodiscard]] PwLinkState::Enum state() const;
 | 
			
		||||
 | 
			
		||||
	static PwLinkIface* instance(PwLink* link);
 | 
			
		||||
 | 
			
		||||
signals:
 | 
			
		||||
	void stateChanged();
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
	PwLink* mLink;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
///! A group of connections between pipewire nodes.
 | 
			
		||||
/// A group of connections between pipewire nodes, one per source->target pair.
 | 
			
		||||
class PwLinkGroupIface
 | 
			
		||||
    : public QObject
 | 
			
		||||
    , public PwObjectRefIface {
 | 
			
		||||
	Q_OBJECT;
 | 
			
		||||
	/// The node that is *receiving* information. (the sink)
 | 
			
		||||
	Q_PROPERTY(PwNodeIface* target READ target CONSTANT);
 | 
			
		||||
	/// The node that is *sending* information. (the source)
 | 
			
		||||
	Q_PROPERTY(PwNodeIface* source READ source CONSTANT);
 | 
			
		||||
	/// The current state of the link group.
 | 
			
		||||
	///
 | 
			
		||||
	/// **This property is invalid unless the link is [bound](../pwobjecttracker).**
 | 
			
		||||
	Q_PROPERTY(PwLinkState::Enum state READ state NOTIFY stateChanged);
 | 
			
		||||
	QML_NAMED_ELEMENT(PwLinkGroup);
 | 
			
		||||
	QML_UNCREATABLE("PwLinkGroups cannot be created directly");
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
	explicit PwLinkGroupIface(PwLinkGroup* group);
 | 
			
		||||
	// destructor should ONLY be called by the pw object destructor, making an unref unnecessary
 | 
			
		||||
	~PwLinkGroupIface() override = default;
 | 
			
		||||
	Q_DISABLE_COPY_MOVE(PwLinkGroupIface);
 | 
			
		||||
 | 
			
		||||
	void ref() override;
 | 
			
		||||
	void unref() override;
 | 
			
		||||
 | 
			
		||||
	[[nodiscard]] PwLinkGroup* group() const;
 | 
			
		||||
	[[nodiscard]] PwNodeIface* target() const;
 | 
			
		||||
	[[nodiscard]] PwNodeIface* source() const;
 | 
			
		||||
	[[nodiscard]] PwLinkState::Enum state() const;
 | 
			
		||||
 | 
			
		||||
	static PwLinkGroupIface* instance(PwLinkGroup* group);
 | 
			
		||||
 | 
			
		||||
signals:
 | 
			
		||||
	void stateChanged();
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
	PwLinkGroup* mGroup;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
///! Binds pipewire objects.
 | 
			
		||||
/// If the object list of at least one PwObjectTracker contains a given pipewire object,
 | 
			
		||||
/// it will become *bound* and you will be able to interact with bound-only properties.
 | 
			
		||||
class PwObjectTracker: public QObject {
 | 
			
		||||
	Q_OBJECT;
 | 
			
		||||
	/// The list of objects to bind.
 | 
			
		||||
	Q_PROPERTY(QList<QObject*> objects READ objects WRITE setObjects NOTIFY objectsChanged);
 | 
			
		||||
	QML_ELEMENT;
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
	explicit PwObjectTracker(QObject* parent = nullptr): QObject(parent) {}
 | 
			
		||||
	~PwObjectTracker() override;
 | 
			
		||||
	Q_DISABLE_COPY_MOVE(PwObjectTracker);
 | 
			
		||||
 | 
			
		||||
	[[nodiscard]] QList<QObject*> objects() const;
 | 
			
		||||
	void setObjects(const QList<QObject*>& objects);
 | 
			
		||||
 | 
			
		||||
signals:
 | 
			
		||||
	void objectsChanged();
 | 
			
		||||
 | 
			
		||||
private slots:
 | 
			
		||||
	void objectDestroyed(QObject* object);
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
	void clearList();
 | 
			
		||||
 | 
			
		||||
	QList<QObject*> trackedObjects;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
} // namespace qs::service::pipewire
 | 
			
		||||
							
								
								
									
										193
									
								
								src/services/pipewire/registry.cpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										193
									
								
								src/services/pipewire/registry.cpp
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,193 @@
 | 
			
		|||
#include "registry.hpp"
 | 
			
		||||
#include <cstring>
 | 
			
		||||
 | 
			
		||||
#include <pipewire/core.h>
 | 
			
		||||
#include <pipewire/extensions/metadata.h>
 | 
			
		||||
#include <pipewire/link.h>
 | 
			
		||||
#include <pipewire/node.h>
 | 
			
		||||
#include <pipewire/proxy.h>
 | 
			
		||||
#include <qdebug.h>
 | 
			
		||||
#include <qlogging.h>
 | 
			
		||||
#include <qloggingcategory.h>
 | 
			
		||||
#include <qobject.h>
 | 
			
		||||
#include <qtmetamacros.h>
 | 
			
		||||
#include <qtypes.h>
 | 
			
		||||
 | 
			
		||||
#include "core.hpp"
 | 
			
		||||
#include "link.hpp"
 | 
			
		||||
#include "metadata.hpp"
 | 
			
		||||
#include "node.hpp"
 | 
			
		||||
 | 
			
		||||
namespace qs::service::pipewire {
 | 
			
		||||
 | 
			
		||||
Q_LOGGING_CATEGORY(logRegistry, "quickshell.service.pipewire.registry", QtWarningMsg);
 | 
			
		||||
 | 
			
		||||
PwBindableObject::~PwBindableObject() {
 | 
			
		||||
	if (this->id != 0) {
 | 
			
		||||
		qCFatal(logRegistry) << "Destroyed pipewire object" << this
 | 
			
		||||
		                     << "without causing safeDestroy. THIS IS UNDEFINED BEHAVIOR.";
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void PwBindableObject::init(PwRegistry* registry, quint32 id, quint32 perms) {
 | 
			
		||||
	this->id = id;
 | 
			
		||||
	this->perms = perms;
 | 
			
		||||
	this->registry = registry;
 | 
			
		||||
	this->setParent(registry);
 | 
			
		||||
	qCDebug(logRegistry) << "Creating object" << this;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void PwBindableObject::safeDestroy() {
 | 
			
		||||
	this->unbind();
 | 
			
		||||
	qCDebug(logRegistry) << "Destroying object" << this;
 | 
			
		||||
	emit this->destroying(this);
 | 
			
		||||
	this->id = 0;
 | 
			
		||||
	delete this;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void PwBindableObject::debugId(QDebug& debug) const {
 | 
			
		||||
	auto saver = QDebugStateSaver(debug);
 | 
			
		||||
	debug.nospace() << this->id << "/" << (this->object == nullptr ? "unbound" : "bound");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void PwBindableObject::ref() {
 | 
			
		||||
	this->refcount++;
 | 
			
		||||
	if (this->refcount == 1) this->bind();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void PwBindableObject::unref() {
 | 
			
		||||
	this->refcount--;
 | 
			
		||||
	if (this->refcount == 0) this->unbind();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void PwBindableObject::bind() {
 | 
			
		||||
	qCDebug(logRegistry) << "Bound object" << this;
 | 
			
		||||
	this->bindHooks();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void PwBindableObject::unbind() {
 | 
			
		||||
	if (this->object == nullptr) return;
 | 
			
		||||
	qCDebug(logRegistry) << "Unbinding object" << this;
 | 
			
		||||
	this->unbindHooks();
 | 
			
		||||
	pw_proxy_destroy(this->object);
 | 
			
		||||
	this->object = nullptr;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
QDebug operator<<(QDebug debug, const PwBindableObject* object) {
 | 
			
		||||
	if (object == nullptr) {
 | 
			
		||||
		debug << "PwBindableObject(0x0)";
 | 
			
		||||
	} else {
 | 
			
		||||
		auto saver = QDebugStateSaver(debug);
 | 
			
		||||
		// 0 if not present, start of class name if present
 | 
			
		||||
		auto idx = QString(object->metaObject()->className()).lastIndexOf(':') + 1;
 | 
			
		||||
		debug.nospace() << (object->metaObject()->className() + idx) << '(' // NOLINT
 | 
			
		||||
		                << static_cast<const void*>(object) << ", id=";
 | 
			
		||||
		object->debugId(debug);
 | 
			
		||||
		debug << ')';
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return debug;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
PwBindableObjectRef::PwBindableObjectRef(PwBindableObject* object) { this->setObject(object); }
 | 
			
		||||
 | 
			
		||||
PwBindableObjectRef::~PwBindableObjectRef() { this->setObject(nullptr); }
 | 
			
		||||
 | 
			
		||||
void PwBindableObjectRef::setObject(PwBindableObject* object) {
 | 
			
		||||
	if (this->mObject != nullptr) {
 | 
			
		||||
		this->mObject->unref();
 | 
			
		||||
		QObject::disconnect(this->mObject, nullptr, this, nullptr);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	this->mObject = object;
 | 
			
		||||
 | 
			
		||||
	if (object != nullptr) {
 | 
			
		||||
		this->mObject->ref();
 | 
			
		||||
		QObject::connect(object, &QObject::destroyed, this, &PwBindableObjectRef::onObjectDestroyed);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void PwBindableObjectRef::onObjectDestroyed() {
 | 
			
		||||
	// allow references to it so consumers can disconnect themselves
 | 
			
		||||
	emit this->objectDestroyed();
 | 
			
		||||
	this->mObject = nullptr;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void PwRegistry::init(PwCore& 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);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const pw_registry_events PwRegistry::EVENTS = {
 | 
			
		||||
    .version = PW_VERSION_REGISTRY_EVENTS,
 | 
			
		||||
    .global = &PwRegistry::onGlobal,
 | 
			
		||||
    .global_remove = &PwRegistry::onGlobalRemoved,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
void PwRegistry::onGlobal(
 | 
			
		||||
    void* data,
 | 
			
		||||
    quint32 id,
 | 
			
		||||
    quint32 permissions,
 | 
			
		||||
    const char* type,
 | 
			
		||||
    quint32 /*version*/,
 | 
			
		||||
    const spa_dict* props
 | 
			
		||||
) {
 | 
			
		||||
	auto* self = static_cast<PwRegistry*>(data);
 | 
			
		||||
 | 
			
		||||
	if (strcmp(type, PW_TYPE_INTERFACE_Metadata) == 0) {
 | 
			
		||||
		auto* meta = new PwMetadata();
 | 
			
		||||
		meta->init(self, id, permissions);
 | 
			
		||||
		meta->initProps(props);
 | 
			
		||||
 | 
			
		||||
		self->metadata.emplace(id, meta);
 | 
			
		||||
		meta->bind();
 | 
			
		||||
	} else if (strcmp(type, PW_TYPE_INTERFACE_Link) == 0) {
 | 
			
		||||
		auto* link = new PwLink();
 | 
			
		||||
		link->init(self, id, permissions);
 | 
			
		||||
		link->initProps(props);
 | 
			
		||||
 | 
			
		||||
		self->links.emplace(id, link);
 | 
			
		||||
		self->addLinkToGroup(link);
 | 
			
		||||
		emit self->linkAdded(link);
 | 
			
		||||
	} else if (strcmp(type, PW_TYPE_INTERFACE_Node) == 0) {
 | 
			
		||||
		auto* node = new PwNode();
 | 
			
		||||
		node->init(self, id, permissions);
 | 
			
		||||
		node->initProps(props);
 | 
			
		||||
 | 
			
		||||
		self->nodes.emplace(id, node);
 | 
			
		||||
		emit self->nodeAdded(node);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void PwRegistry::onGlobalRemoved(void* data, quint32 id) {
 | 
			
		||||
	auto* self = static_cast<PwRegistry*>(data);
 | 
			
		||||
 | 
			
		||||
	if (auto* meta = self->metadata.value(id)) {
 | 
			
		||||
		self->metadata.remove(id);
 | 
			
		||||
		meta->safeDestroy();
 | 
			
		||||
	} else if (auto* link = self->links.value(id)) {
 | 
			
		||||
		self->links.remove(id);
 | 
			
		||||
		link->safeDestroy();
 | 
			
		||||
	} else if (auto* node = self->nodes.value(id)) {
 | 
			
		||||
		self->nodes.remove(id);
 | 
			
		||||
		node->safeDestroy();
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void PwRegistry::addLinkToGroup(PwLink* link) {
 | 
			
		||||
	for (auto* group: this->linkGroups) {
 | 
			
		||||
		if (group->tryAddLink(link)) return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	auto* group = new PwLinkGroup(link);
 | 
			
		||||
	QObject::connect(group, &QObject::destroyed, this, &PwRegistry::onLinkGroupDestroyed);
 | 
			
		||||
	this->linkGroups.push_back(group);
 | 
			
		||||
	emit this->linkGroupAdded(group);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void PwRegistry::onLinkGroupDestroyed(QObject* object) {
 | 
			
		||||
	auto* group = static_cast<PwLinkGroup*>(object); // NOLINT
 | 
			
		||||
	this->linkGroups.removeOne(group);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
} // namespace qs::service::pipewire
 | 
			
		||||
							
								
								
									
										160
									
								
								src/services/pipewire/registry.hpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										160
									
								
								src/services/pipewire/registry.hpp
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,160 @@
 | 
			
		|||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include <pipewire/core.h>
 | 
			
		||||
#include <pipewire/proxy.h>
 | 
			
		||||
#include <qcontainerfwd.h>
 | 
			
		||||
#include <qdebug.h>
 | 
			
		||||
#include <qhash.h>
 | 
			
		||||
#include <qloggingcategory.h>
 | 
			
		||||
#include <qobject.h>
 | 
			
		||||
#include <qtclasshelpermacros.h>
 | 
			
		||||
#include <qtmetamacros.h>
 | 
			
		||||
#include <qtypes.h>
 | 
			
		||||
 | 
			
		||||
#include "core.hpp"
 | 
			
		||||
 | 
			
		||||
namespace qs::service::pipewire {
 | 
			
		||||
 | 
			
		||||
Q_DECLARE_LOGGING_CATEGORY(logRegistry);
 | 
			
		||||
 | 
			
		||||
class PwRegistry;
 | 
			
		||||
class PwMetadata;
 | 
			
		||||
class PwNode;
 | 
			
		||||
class PwLink;
 | 
			
		||||
class PwLinkGroup;
 | 
			
		||||
 | 
			
		||||
class PwBindableObject: public QObject {
 | 
			
		||||
	Q_OBJECT;
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
	PwBindableObject() = default;
 | 
			
		||||
	~PwBindableObject() override;
 | 
			
		||||
	Q_DISABLE_COPY_MOVE(PwBindableObject);
 | 
			
		||||
 | 
			
		||||
	// constructors and destructors can't do virtual calls.
 | 
			
		||||
	virtual void init(PwRegistry* registry, quint32 id, quint32 perms);
 | 
			
		||||
	virtual void initProps(const spa_dict* /*props*/) {}
 | 
			
		||||
	virtual void safeDestroy();
 | 
			
		||||
 | 
			
		||||
	quint32 id = 0;
 | 
			
		||||
	quint32 perms = 0;
 | 
			
		||||
 | 
			
		||||
	void debugId(QDebug& debug) const;
 | 
			
		||||
	void ref();
 | 
			
		||||
	void unref();
 | 
			
		||||
 | 
			
		||||
signals:
 | 
			
		||||
	// goes with safeDestroy
 | 
			
		||||
	void destroying(PwBindableObject* self);
 | 
			
		||||
 | 
			
		||||
protected:
 | 
			
		||||
	virtual void bind();
 | 
			
		||||
	void unbind();
 | 
			
		||||
	virtual void bindHooks() {};
 | 
			
		||||
	virtual void unbindHooks() {};
 | 
			
		||||
 | 
			
		||||
	quint32 refcount = 0;
 | 
			
		||||
	pw_proxy* object = nullptr;
 | 
			
		||||
	PwRegistry* registry = nullptr;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
QDebug operator<<(QDebug debug, const PwBindableObject* object);
 | 
			
		||||
 | 
			
		||||
template <typename T, const char* INTERFACE, quint32 VERSION>
 | 
			
		||||
class PwBindable: public PwBindableObject {
 | 
			
		||||
public:
 | 
			
		||||
	T* proxy() {
 | 
			
		||||
		return reinterpret_cast<T*>(this->object); // NOLINT
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
protected:
 | 
			
		||||
	void bind() override {
 | 
			
		||||
		if (this->object != nullptr) return;
 | 
			
		||||
		auto* object =
 | 
			
		||||
		    pw_registry_bind(this->registry->object, this->id, INTERFACE, VERSION, 0); // NOLINT
 | 
			
		||||
		this->object = static_cast<pw_proxy*>(object);
 | 
			
		||||
		this->PwBindableObject::bind();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	friend class PwRegistry;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class PwBindableObjectRef: public QObject {
 | 
			
		||||
	Q_OBJECT;
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
	explicit PwBindableObjectRef(PwBindableObject* object = nullptr);
 | 
			
		||||
	~PwBindableObjectRef() override;
 | 
			
		||||
	Q_DISABLE_COPY_MOVE(PwBindableObjectRef);
 | 
			
		||||
 | 
			
		||||
signals:
 | 
			
		||||
	void objectDestroyed();
 | 
			
		||||
 | 
			
		||||
private slots:
 | 
			
		||||
	void onObjectDestroyed();
 | 
			
		||||
 | 
			
		||||
protected:
 | 
			
		||||
	void setObject(PwBindableObject* object);
 | 
			
		||||
 | 
			
		||||
	PwBindableObject* mObject = nullptr;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
template <typename T>
 | 
			
		||||
class PwBindableRef: public PwBindableObjectRef {
 | 
			
		||||
public:
 | 
			
		||||
	explicit PwBindableRef(T* object = nullptr): PwBindableObjectRef(object) {}
 | 
			
		||||
 | 
			
		||||
	void setObject(T* object) { this->PwBindableObjectRef::setObject(object); }
 | 
			
		||||
 | 
			
		||||
	T* object() { return this->mObject; }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class PwRegistry
 | 
			
		||||
    : public QObject
 | 
			
		||||
    , public PwObject<pw_registry> {
 | 
			
		||||
	Q_OBJECT;
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
	void init(PwCore& core);
 | 
			
		||||
 | 
			
		||||
	//QHash<quint32, PwClient*> clients;
 | 
			
		||||
	QHash<quint32, PwMetadata*> metadata;
 | 
			
		||||
	QHash<quint32, PwNode*> nodes;
 | 
			
		||||
	QHash<quint32, PwLink*> links;
 | 
			
		||||
	QVector<PwLinkGroup*> linkGroups;
 | 
			
		||||
 | 
			
		||||
signals:
 | 
			
		||||
	void nodeAdded(PwNode* node);
 | 
			
		||||
	void linkAdded(PwLink* link);
 | 
			
		||||
	void linkGroupAdded(PwLinkGroup* group);
 | 
			
		||||
	void metadataUpdate(
 | 
			
		||||
	    PwMetadata* owner,
 | 
			
		||||
	    quint32 subject,
 | 
			
		||||
	    const char* key,
 | 
			
		||||
	    const char* type,
 | 
			
		||||
	    const char* value
 | 
			
		||||
	);
 | 
			
		||||
 | 
			
		||||
private slots:
 | 
			
		||||
	void onLinkGroupDestroyed(QObject* object);
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
	static const pw_registry_events EVENTS;
 | 
			
		||||
 | 
			
		||||
	static void onGlobal(
 | 
			
		||||
	    void* data,
 | 
			
		||||
	    quint32 id,
 | 
			
		||||
	    quint32 permissions,
 | 
			
		||||
	    const char* type,
 | 
			
		||||
	    quint32 version,
 | 
			
		||||
	    const spa_dict* props
 | 
			
		||||
	);
 | 
			
		||||
 | 
			
		||||
	static void onGlobalRemoved(void* data, quint32 id);
 | 
			
		||||
 | 
			
		||||
	void addLinkToGroup(PwLink* link);
 | 
			
		||||
 | 
			
		||||
	SpaHook listener;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
} // namespace qs::service::pipewire
 | 
			
		||||
| 
						 | 
				
			
			@ -1,4 +1,4 @@
 | 
			
		|||
name = "Quickshell.Service.SystemTray"
 | 
			
		||||
name = "Quickshell.Services.SystemTray"
 | 
			
		||||
description = "Types for implementing a system tray"
 | 
			
		||||
headers = [ "qml.hpp" ]
 | 
			
		||||
-----
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue