service/pipewire: add pipewire module

This commit is contained in:
outfoxxed 2024-05-19 02:23:11 -07:00
parent bba8cb8a7d
commit 3e80c4a4fd
Signed by: outfoxxed
GPG Key ID: 4C88A185FB89301E
21 changed files with 2476 additions and 4 deletions

View File

@ -36,6 +36,7 @@ Checks: >
-readability-braces-around-statements,
-readability-redundant-access-specifiers,
-readability-else-after-return,
-readability-container-data-pointer,
tidyfox-*,
CheckOptions:
performance-for-range-copy.WarnOnAllAutoCopies: true

View File

@ -18,6 +18,7 @@ option(HYPRLAND "Support hyprland specific features" ON)
option(HYPRLAND_GLOBAL_SHORTCUTS "Hyprland Global Shortcuts" ON)
option(HYPRLAND_FOCUS_GRAB "Hyprland Focus Grabbing" ON)
option(SERVICE_STATUS_NOTIFIER "StatusNotifierItem service" ON)
option(SERVICE_PIPEWIRE "PipeWire service" ON)
message(STATUS "Quickshell configuration")
message(STATUS " NVIDIA workarounds: ${NVIDIA_COMPAT}")
@ -30,6 +31,7 @@ if (WAYLAND)
endif ()
message(STATUS " Services")
message(STATUS " StatusNotifier: ${SERVICE_STATUS_NOTIFIER}")
message(STATUS " PipeWire: ${SERVICE_PIPEWIRE}")
message(STATUS " Hyprland: ${HYPRLAND}")
if (HYPRLAND)
message(STATUS " Focus Grabbing: ${HYPRLAND_FOCUS_GRAB}")

View File

@ -24,6 +24,7 @@
debug ? false,
enableWayland ? true,
enablePipewire ? true,
nvidiaCompat ? false,
svgSupport ? true, # you almost always want this
}: buildStdenv.mkDerivation {
@ -46,7 +47,8 @@
qt6.qtdeclarative
]
++ (lib.optionals enableWayland [ qt6.qtwayland wayland ])
++ (lib.optionals svgSupport [ qt6.qtsvg ]);
++ (lib.optionals svgSupport [ qt6.qtsvg ])
++ (lib.optionals enablePipewire [ pipewire ]);
QTWAYLANDSCANNER = lib.optionalString enableWayland "${qt6.qtwayland}/libexec/qtwaylandscanner";
@ -62,7 +64,8 @@
cmakeFlags = [
"-DGIT_REVISION=${gitRev}"
] ++ lib.optional (!enableWayland) "-DWAYLAND=OFF"
++ lib.optional nvidiaCompat "-DNVIDIA_COMPAT=ON";
++ lib.optional nvidiaCompat "-DNVIDIA_COMPAT=ON"
++ lib.optional (!enablePipewire) "-DSERVICE_PIPEWIRE=OFF";
buildPhase = "ninjaBuildPhase";
enableParallelBuilding = true;

2
docs

@ -1 +1 @@
Subproject commit 149b784a5a4c40ada67cb9f6af5a5350678ab6d4
Subproject commit ff5da84a8b258a9b2caaf978ddb6de23635d8903

View File

@ -1,3 +1,7 @@
if (SERVICE_STATUS_NOTIFIER)
add_subdirectory(status_notifier)
endif()
if (SERVICE_PIPEWIRE)
add_subdirectory(pipewire)
endif()

View 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)

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View File

@ -1,4 +1,4 @@
name = "Quickshell.Service.SystemTray"
name = "Quickshell.Services.SystemTray"
description = "Types for implementing a system tray"
headers = [ "qml.hpp" ]
-----