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