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-braces-around-statements,
 | 
				
			||||||
  -readability-redundant-access-specifiers,
 | 
					  -readability-redundant-access-specifiers,
 | 
				
			||||||
  -readability-else-after-return,
 | 
					  -readability-else-after-return,
 | 
				
			||||||
 | 
					  -readability-container-data-pointer,
 | 
				
			||||||
  tidyfox-*,
 | 
					  tidyfox-*,
 | 
				
			||||||
CheckOptions:
 | 
					CheckOptions:
 | 
				
			||||||
  performance-for-range-copy.WarnOnAllAutoCopies: true
 | 
					  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_GLOBAL_SHORTCUTS "Hyprland Global Shortcuts" ON)
 | 
				
			||||||
option(HYPRLAND_FOCUS_GRAB "Hyprland Focus Grabbing" ON)
 | 
					option(HYPRLAND_FOCUS_GRAB "Hyprland Focus Grabbing" ON)
 | 
				
			||||||
option(SERVICE_STATUS_NOTIFIER "StatusNotifierItem service" ON)
 | 
					option(SERVICE_STATUS_NOTIFIER "StatusNotifierItem service" ON)
 | 
				
			||||||
 | 
					option(SERVICE_PIPEWIRE "PipeWire service" ON)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
message(STATUS "Quickshell configuration")
 | 
					message(STATUS "Quickshell configuration")
 | 
				
			||||||
message(STATUS "  NVIDIA workarounds: ${NVIDIA_COMPAT}")
 | 
					message(STATUS "  NVIDIA workarounds: ${NVIDIA_COMPAT}")
 | 
				
			||||||
| 
						 | 
					@ -30,6 +31,7 @@ if (WAYLAND)
 | 
				
			||||||
endif ()
 | 
					endif ()
 | 
				
			||||||
message(STATUS "  Services")
 | 
					message(STATUS "  Services")
 | 
				
			||||||
message(STATUS "    StatusNotifier: ${SERVICE_STATUS_NOTIFIER}")
 | 
					message(STATUS "    StatusNotifier: ${SERVICE_STATUS_NOTIFIER}")
 | 
				
			||||||
 | 
					message(STATUS "    PipeWire: ${SERVICE_PIPEWIRE}")
 | 
				
			||||||
message(STATUS "  Hyprland: ${HYPRLAND}")
 | 
					message(STATUS "  Hyprland: ${HYPRLAND}")
 | 
				
			||||||
if (HYPRLAND)
 | 
					if (HYPRLAND)
 | 
				
			||||||
	message(STATUS "    Focus Grabbing: ${HYPRLAND_FOCUS_GRAB}")
 | 
						message(STATUS "    Focus Grabbing: ${HYPRLAND_FOCUS_GRAB}")
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -24,6 +24,7 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  debug ? false,
 | 
					  debug ? false,
 | 
				
			||||||
  enableWayland ? true,
 | 
					  enableWayland ? true,
 | 
				
			||||||
 | 
					  enablePipewire ? true,
 | 
				
			||||||
  nvidiaCompat ? false,
 | 
					  nvidiaCompat ? false,
 | 
				
			||||||
  svgSupport ? true, # you almost always want this
 | 
					  svgSupport ? true, # you almost always want this
 | 
				
			||||||
}: buildStdenv.mkDerivation {
 | 
					}: buildStdenv.mkDerivation {
 | 
				
			||||||
| 
						 | 
					@ -46,7 +47,8 @@
 | 
				
			||||||
    qt6.qtdeclarative
 | 
					    qt6.qtdeclarative
 | 
				
			||||||
  ]
 | 
					  ]
 | 
				
			||||||
  ++ (lib.optionals enableWayland [ qt6.qtwayland wayland ])
 | 
					  ++ (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";
 | 
					  QTWAYLANDSCANNER = lib.optionalString enableWayland "${qt6.qtwayland}/libexec/qtwaylandscanner";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -62,7 +64,8 @@
 | 
				
			||||||
  cmakeFlags = [
 | 
					  cmakeFlags = [
 | 
				
			||||||
    "-DGIT_REVISION=${gitRev}"
 | 
					    "-DGIT_REVISION=${gitRev}"
 | 
				
			||||||
  ] ++ lib.optional (!enableWayland) "-DWAYLAND=OFF"
 | 
					  ] ++ 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";
 | 
					  buildPhase = "ninjaBuildPhase";
 | 
				
			||||||
  enableParallelBuilding = true;
 | 
					  enableParallelBuilding = true;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										2
									
								
								docs
									
										
									
									
									
								
							
							
						
						
									
										2
									
								
								docs
									
										
									
									
									
								
							| 
						 | 
					@ -1 +1 @@
 | 
				
			||||||
Subproject commit 149b784a5a4c40ada67cb9f6af5a5350678ab6d4
 | 
					Subproject commit ff5da84a8b258a9b2caaf978ddb6de23635d8903
 | 
				
			||||||
| 
						 | 
					@ -1,3 +1,7 @@
 | 
				
			||||||
if (SERVICE_STATUS_NOTIFIER)
 | 
					if (SERVICE_STATUS_NOTIFIER)
 | 
				
			||||||
	add_subdirectory(status_notifier)
 | 
						add_subdirectory(status_notifier)
 | 
				
			||||||
endif()
 | 
					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"
 | 
					description = "Types for implementing a system tray"
 | 
				
			||||||
headers = [ "qml.hpp" ]
 | 
					headers = [ "qml.hpp" ]
 | 
				
			||||||
-----
 | 
					-----
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue