Compare commits
3 commits
ccbc25880b
...
fbaec141c0
Author | SHA1 | Date | |
---|---|---|---|
fbaec141c0 | |||
fdc78ae16f | |||
f889f08901 |
11 changed files with 589 additions and 152 deletions
|
@ -1,7 +1,9 @@
|
|||
#pragma once
|
||||
#include <type_traits>
|
||||
|
||||
#include <qobject.h>
|
||||
#include <qtclasshelpermacros.h>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
// NOLINTBEGIN
|
||||
#define DROP_EMIT(object, func) \
|
||||
|
@ -211,3 +213,34 @@ public:
|
|||
|
||||
GuardedEmitBlocker block() { return GuardedEmitBlocker(&this->blocked); }
|
||||
};
|
||||
|
||||
template <auto member, auto destroyedSlot, auto changedSignal>
|
||||
class SimpleObjectHandleOps {
|
||||
using Traits = MemberPointerTraits<decltype(member)>;
|
||||
|
||||
public:
|
||||
static bool setObject(Traits::Class* parent, Traits::Type value) {
|
||||
if (value == parent->*member) return false;
|
||||
|
||||
if (parent->*member != nullptr) {
|
||||
QObject::disconnect(parent->*member, &QObject::destroyed, parent, destroyedSlot);
|
||||
}
|
||||
|
||||
parent->*member = value;
|
||||
|
||||
if (value != nullptr) {
|
||||
QObject::connect(parent->*member, &QObject::destroyed, parent, destroyedSlot);
|
||||
}
|
||||
|
||||
if constexpr (changedSignal != nullptr) {
|
||||
emit(parent->*changedSignal)();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
template <auto member, auto destroyedSlot, auto changedSignal = nullptr>
|
||||
bool setSimpleObjectHandle(auto* parent, auto* value) {
|
||||
return SimpleObjectHandleOps<member, destroyedSlot, changedSignal>::setObject(parent, value);
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ qt_add_library(quickshell-service-pipewire STATIC
|
|||
metadata.cpp
|
||||
link.cpp
|
||||
device.cpp
|
||||
defaults.cpp
|
||||
)
|
||||
|
||||
qt_add_qml_module(quickshell-service-pipewire
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
#pragma once
|
||||
|
||||
#include "core.hpp"
|
||||
#include "metadata.hpp"
|
||||
#include "defaults.hpp"
|
||||
#include "registry.hpp"
|
||||
|
||||
namespace qs::service::pipewire {
|
||||
|
@ -13,7 +13,7 @@ public:
|
|||
explicit PwConnection(QObject* parent = nullptr);
|
||||
|
||||
PwRegistry registry;
|
||||
PwDefaultsMetadata defaults {&this->registry};
|
||||
PwDefaultTracker defaults {&this->registry};
|
||||
|
||||
static PwConnection* instance();
|
||||
|
||||
|
|
291
src/services/pipewire/defaults.cpp
Normal file
291
src/services/pipewire/defaults.cpp
Normal file
|
@ -0,0 +1,291 @@
|
|||
#include "defaults.hpp"
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
|
||||
#include <qjsondocument.h>
|
||||
#include <qjsonobject.h>
|
||||
#include <qlogging.h>
|
||||
#include <qloggingcategory.h>
|
||||
#include <qobject.h>
|
||||
#include <qstringview.h>
|
||||
#include <qtmetamacros.h>
|
||||
#include <spa/utils/json.h>
|
||||
|
||||
#include "../../core/util.hpp"
|
||||
#include "metadata.hpp"
|
||||
#include "node.hpp"
|
||||
#include "registry.hpp"
|
||||
|
||||
namespace qs::service::pipewire {
|
||||
|
||||
Q_LOGGING_CATEGORY(logDefaults, "quickshell.service.pipewire.defaults", QtWarningMsg);
|
||||
|
||||
PwDefaultTracker::PwDefaultTracker(PwRegistry* registry): registry(registry) {
|
||||
QObject::connect(registry, &PwRegistry::metadataAdded, this, &PwDefaultTracker::onMetadataAdded);
|
||||
QObject::connect(registry, &PwRegistry::nodeAdded, this, &PwDefaultTracker::onNodeAdded);
|
||||
}
|
||||
|
||||
void PwDefaultTracker::onMetadataAdded(PwMetadata* metadata) {
|
||||
if (metadata->name() == "default") {
|
||||
qCDebug(logDefaults) << "Got new defaults metadata object" << metadata;
|
||||
|
||||
if (this->defaultsMetadata.object()) {
|
||||
QObject::disconnect(this->defaultsMetadata.object(), nullptr, this, nullptr);
|
||||
}
|
||||
|
||||
QObject::connect(
|
||||
metadata,
|
||||
&PwMetadata::propertyChanged,
|
||||
this,
|
||||
&PwDefaultTracker::onMetadataProperty
|
||||
);
|
||||
|
||||
this->defaultsMetadata.setObject(metadata);
|
||||
}
|
||||
}
|
||||
|
||||
void PwDefaultTracker::onMetadataProperty(const char* key, const char* type, const char* value) {
|
||||
void (PwDefaultTracker::*nodeSetter)(PwNode*) = nullptr;
|
||||
void (PwDefaultTracker::*nameSetter)(const QString&) = nullptr;
|
||||
|
||||
qCDebug(logDefaults).nospace() << "Got default metadata update for " << key << ": "
|
||||
<< QString(value);
|
||||
|
||||
if (strcmp(key, "default.audio.sink") == 0) {
|
||||
nodeSetter = &PwDefaultTracker::setDefaultSink;
|
||||
nameSetter = &PwDefaultTracker::setDefaultSinkName;
|
||||
} else if (strcmp(key, "default.audio.source") == 0) {
|
||||
nodeSetter = &PwDefaultTracker::setDefaultSource;
|
||||
nameSetter = &PwDefaultTracker::setDefaultSourceName;
|
||||
} else if (strcmp(key, "default.configured.audio.sink") == 0) {
|
||||
nodeSetter = &PwDefaultTracker::setDefaultConfiguredSink;
|
||||
nameSetter = &PwDefaultTracker::setDefaultConfiguredSinkName;
|
||||
} else if (strcmp(key, "default.configured.audio.source") == 0) {
|
||||
nodeSetter = &PwDefaultTracker::setDefaultConfiguredSource;
|
||||
nameSetter = &PwDefaultTracker::setDefaultConfiguredSourceName;
|
||||
} else return;
|
||||
|
||||
QString name;
|
||||
if (type != nullptr && value != nullptr && strcmp(type, "Spa:String:JSON") == 0) {
|
||||
auto failed = true;
|
||||
auto iter = std::array<spa_json, 2>();
|
||||
spa_json_init(&iter[0], value, strlen(value));
|
||||
|
||||
if (spa_json_enter_object(&iter[0], &iter[1]) > 0) {
|
||||
auto buf = std::array<char, 1024>();
|
||||
|
||||
if (spa_json_get_string(&iter[1], buf.data(), buf.size()) > 0) {
|
||||
if (strcmp(buf.data(), "name") == 0) {
|
||||
if (spa_json_get_string(&iter[1], buf.data(), buf.size()) > 0) {
|
||||
name = buf.data();
|
||||
failed = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (failed) {
|
||||
qCWarning(logDefaults) << "Failed to parse SPA default json:"
|
||||
<< QString::fromLocal8Bit(value);
|
||||
}
|
||||
}
|
||||
|
||||
(this->*nameSetter)(name);
|
||||
(this->*nodeSetter)(this->registry->findNodeByName(name));
|
||||
}
|
||||
|
||||
void PwDefaultTracker::onNodeAdded(PwNode* node) {
|
||||
if (node->name.isEmpty()) return;
|
||||
|
||||
if (this->mDefaultSink == nullptr && node->name == this->mDefaultSinkName) {
|
||||
this->setDefaultSink(node);
|
||||
}
|
||||
|
||||
if (this->mDefaultSource == nullptr && node->name == this->mDefaultSourceName) {
|
||||
this->setDefaultSource(node);
|
||||
}
|
||||
|
||||
if (this->mDefaultConfiguredSink == nullptr && node->name == this->mDefaultConfiguredSinkName) {
|
||||
this->setDefaultConfiguredSink(node);
|
||||
}
|
||||
|
||||
if (this->mDefaultConfiguredSource == nullptr && node->name == this->mDefaultConfiguredSourceName)
|
||||
{
|
||||
this->setDefaultConfiguredSource(node);
|
||||
}
|
||||
}
|
||||
|
||||
void PwDefaultTracker::onNodeDestroyed(QObject* node) {
|
||||
if (node == this->mDefaultSink) {
|
||||
qCInfo(logDefaults) << "Default sink destroyed.";
|
||||
this->mDefaultSink = nullptr;
|
||||
emit this->defaultSinkChanged();
|
||||
}
|
||||
|
||||
if (node == this->mDefaultSource) {
|
||||
qCInfo(logDefaults) << "Default source destroyed.";
|
||||
this->mDefaultSource = nullptr;
|
||||
emit this->defaultSourceChanged();
|
||||
}
|
||||
|
||||
if (node == this->mDefaultConfiguredSink) {
|
||||
qCInfo(logDefaults) << "Default configured sink destroyed.";
|
||||
this->mDefaultConfiguredSink = nullptr;
|
||||
emit this->defaultConfiguredSinkChanged();
|
||||
}
|
||||
|
||||
if (node == this->mDefaultConfiguredSource) {
|
||||
qCInfo(logDefaults) << "Default configured source destroyed.";
|
||||
this->mDefaultConfiguredSource = nullptr;
|
||||
emit this->defaultConfiguredSourceChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void PwDefaultTracker::changeConfiguredSink(PwNode* node) {
|
||||
if (node != nullptr) {
|
||||
if (!node->isSink) {
|
||||
qCCritical(logDefaults) << "Cannot change default sink to a node that is not a sink.";
|
||||
return;
|
||||
}
|
||||
|
||||
this->changeConfiguredSinkName(node->name);
|
||||
} else {
|
||||
this->changeConfiguredSinkName("");
|
||||
}
|
||||
}
|
||||
|
||||
void PwDefaultTracker::changeConfiguredSinkName(const QString& sink) {
|
||||
if (sink == this->mDefaultConfiguredSinkName) return;
|
||||
|
||||
if (this->setConfiguredDefault("default.configured.audio.sink", sink)) {
|
||||
this->mDefaultConfiguredSinkName = sink;
|
||||
qCInfo(logDefaults) << "Set default configured sink to" << sink;
|
||||
}
|
||||
}
|
||||
|
||||
void PwDefaultTracker::changeConfiguredSource(PwNode* node) {
|
||||
if (node != nullptr) {
|
||||
if (node->isSink) {
|
||||
qCCritical(logDefaults) << "Cannot change default source to a node that is not a source.";
|
||||
return;
|
||||
}
|
||||
|
||||
this->changeConfiguredSourceName(node->name);
|
||||
} else {
|
||||
this->changeConfiguredSourceName("");
|
||||
}
|
||||
}
|
||||
|
||||
void PwDefaultTracker::changeConfiguredSourceName(const QString& source) {
|
||||
if (source == this->mDefaultConfiguredSourceName) return;
|
||||
|
||||
if (this->setConfiguredDefault("default.configured.audio.source", source)) {
|
||||
this->mDefaultConfiguredSourceName = source;
|
||||
qCInfo(logDefaults) << "Set default configured source to" << source;
|
||||
}
|
||||
}
|
||||
|
||||
bool PwDefaultTracker::setConfiguredDefault(const char* key, const QString& value) {
|
||||
auto* meta = this->defaultsMetadata.object();
|
||||
|
||||
if (!meta || !meta->proxy()) {
|
||||
qCCritical(logDefaults) << "Cannot set default node as metadata is not ready.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (value.isEmpty()) {
|
||||
meta->setProperty(key, "Spa:String:JSON", nullptr);
|
||||
} else {
|
||||
// Spa json is a superset of json so we can avoid the awful spa json api when serializing.
|
||||
auto json = QJsonDocument({{"name", value}}).toJson(QJsonDocument::Compact);
|
||||
|
||||
meta->setProperty(key, "Spa:String:JSON", json.toStdString().c_str());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void PwDefaultTracker::setDefaultSink(PwNode* node) {
|
||||
if (node == this->mDefaultSink) return;
|
||||
qCInfo(logDefaults) << "Default sink changed to" << node;
|
||||
|
||||
setSimpleObjectHandle<
|
||||
&PwDefaultTracker::mDefaultSink,
|
||||
&PwDefaultTracker::onNodeDestroyed,
|
||||
&PwDefaultTracker::defaultSinkChanged>(this, node);
|
||||
}
|
||||
|
||||
void PwDefaultTracker::setDefaultSinkName(const QString& name) {
|
||||
if (name == this->mDefaultSinkName) return;
|
||||
qCInfo(logDefaults) << "Default sink name changed to" << name;
|
||||
this->mDefaultSinkName = name;
|
||||
emit this->defaultSinkNameChanged();
|
||||
}
|
||||
|
||||
void PwDefaultTracker::setDefaultSource(PwNode* node) {
|
||||
if (node == this->mDefaultSource) return;
|
||||
qCInfo(logDefaults) << "Default source changed to" << node;
|
||||
|
||||
setSimpleObjectHandle<
|
||||
&PwDefaultTracker::mDefaultSource,
|
||||
&PwDefaultTracker::onNodeDestroyed,
|
||||
&PwDefaultTracker::defaultSourceChanged>(this, node);
|
||||
}
|
||||
|
||||
void PwDefaultTracker::setDefaultSourceName(const QString& name) {
|
||||
if (name == this->mDefaultSourceName) return;
|
||||
qCInfo(logDefaults) << "Default source name changed to" << name;
|
||||
this->mDefaultSourceName = name;
|
||||
emit this->defaultSourceNameChanged();
|
||||
}
|
||||
|
||||
void PwDefaultTracker::setDefaultConfiguredSink(PwNode* node) {
|
||||
if (node == this->mDefaultConfiguredSink) return;
|
||||
qCInfo(logDefaults) << "Default configured sink changed to" << node;
|
||||
|
||||
setSimpleObjectHandle<
|
||||
&PwDefaultTracker::mDefaultConfiguredSink,
|
||||
&PwDefaultTracker::onNodeDestroyed,
|
||||
&PwDefaultTracker::defaultConfiguredSinkChanged>(this, node);
|
||||
}
|
||||
|
||||
void PwDefaultTracker::setDefaultConfiguredSinkName(const QString& name) {
|
||||
if (name == this->mDefaultConfiguredSinkName) return;
|
||||
qCInfo(logDefaults) << "Default configured sink name changed to" << name;
|
||||
this->mDefaultConfiguredSinkName = name;
|
||||
emit this->defaultConfiguredSinkNameChanged();
|
||||
}
|
||||
|
||||
void PwDefaultTracker::setDefaultConfiguredSource(PwNode* node) {
|
||||
if (node == this->mDefaultConfiguredSource) return;
|
||||
qCInfo(logDefaults) << "Default configured source changed to" << node;
|
||||
|
||||
setSimpleObjectHandle<
|
||||
&PwDefaultTracker::mDefaultConfiguredSource,
|
||||
&PwDefaultTracker::onNodeDestroyed,
|
||||
&PwDefaultTracker::defaultConfiguredSourceChanged>(this, node);
|
||||
}
|
||||
|
||||
void PwDefaultTracker::setDefaultConfiguredSourceName(const QString& name) {
|
||||
if (name == this->mDefaultConfiguredSourceName) return;
|
||||
qCInfo(logDefaults) << "Default configured source name changed to" << name;
|
||||
this->mDefaultConfiguredSourceName = name;
|
||||
emit this->defaultConfiguredSourceNameChanged();
|
||||
}
|
||||
|
||||
PwNode* PwDefaultTracker::defaultSink() const { return this->mDefaultSink; }
|
||||
PwNode* PwDefaultTracker::defaultSource() const { return this->mDefaultSource; }
|
||||
|
||||
PwNode* PwDefaultTracker::defaultConfiguredSink() const { return this->mDefaultConfiguredSink; }
|
||||
|
||||
const QString& PwDefaultTracker::defaultConfiguredSinkName() const {
|
||||
return this->mDefaultConfiguredSinkName;
|
||||
}
|
||||
|
||||
PwNode* PwDefaultTracker::defaultConfiguredSource() const { return this->mDefaultConfiguredSource; }
|
||||
|
||||
const QString& PwDefaultTracker::defaultConfiguredSourceName() const {
|
||||
return this->mDefaultConfiguredSourceName;
|
||||
}
|
||||
|
||||
} // namespace qs::service::pipewire
|
79
src/services/pipewire/defaults.hpp
Normal file
79
src/services/pipewire/defaults.hpp
Normal file
|
@ -0,0 +1,79 @@
|
|||
#pragma once
|
||||
|
||||
#include <qobject.h>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
#include "registry.hpp"
|
||||
|
||||
namespace qs::service::pipewire {
|
||||
|
||||
class PwDefaultTracker: public QObject {
|
||||
Q_OBJECT;
|
||||
|
||||
public:
|
||||
explicit PwDefaultTracker(PwRegistry* registry);
|
||||
|
||||
[[nodiscard]] PwNode* defaultSink() const;
|
||||
[[nodiscard]] PwNode* defaultSource() const;
|
||||
|
||||
[[nodiscard]] PwNode* defaultConfiguredSink() const;
|
||||
[[nodiscard]] const QString& defaultConfiguredSinkName() const;
|
||||
void changeConfiguredSink(PwNode* node);
|
||||
void changeConfiguredSinkName(const QString& sink);
|
||||
|
||||
[[nodiscard]] PwNode* defaultConfiguredSource() const;
|
||||
[[nodiscard]] const QString& defaultConfiguredSourceName() const;
|
||||
void changeConfiguredSource(PwNode* node);
|
||||
void changeConfiguredSourceName(const QString& source);
|
||||
|
||||
signals:
|
||||
void defaultSinkChanged();
|
||||
void defaultSinkNameChanged();
|
||||
|
||||
void defaultSourceChanged();
|
||||
void defaultSourceNameChanged();
|
||||
|
||||
void defaultConfiguredSinkChanged();
|
||||
void defaultConfiguredSinkNameChanged();
|
||||
|
||||
void defaultConfiguredSourceChanged();
|
||||
void defaultConfiguredSourceNameChanged();
|
||||
|
||||
private slots:
|
||||
void onMetadataAdded(PwMetadata* metadata);
|
||||
void onMetadataProperty(const char* key, const char* type, const char* value);
|
||||
void onNodeAdded(PwNode* node);
|
||||
void onNodeDestroyed(QObject* node);
|
||||
|
||||
private:
|
||||
void setDefaultSink(PwNode* node);
|
||||
void setDefaultSinkName(const QString& name);
|
||||
|
||||
void setDefaultSource(PwNode* node);
|
||||
void setDefaultSourceName(const QString& name);
|
||||
|
||||
void setDefaultConfiguredSink(PwNode* node);
|
||||
void setDefaultConfiguredSinkName(const QString& name);
|
||||
|
||||
void setDefaultConfiguredSource(PwNode* node);
|
||||
void setDefaultConfiguredSourceName(const QString& name);
|
||||
|
||||
bool setConfiguredDefault(const char* key, const QString& value);
|
||||
|
||||
PwRegistry* registry;
|
||||
PwBindableRef<PwMetadata> defaultsMetadata;
|
||||
|
||||
PwNode* mDefaultSink = nullptr;
|
||||
QString mDefaultSinkName;
|
||||
|
||||
PwNode* mDefaultSource = nullptr;
|
||||
QString mDefaultSourceName;
|
||||
|
||||
PwNode* mDefaultConfiguredSink = nullptr;
|
||||
QString mDefaultConfiguredSinkName;
|
||||
|
||||
PwNode* mDefaultConfiguredSource = nullptr;
|
||||
QString mDefaultConfiguredSourceName;
|
||||
};
|
||||
|
||||
} // namespace qs::service::pipewire
|
|
@ -1,14 +1,15 @@
|
|||
#include "metadata.hpp"
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
|
||||
#include <pipewire/core.h>
|
||||
#include <pipewire/extensions/metadata.h>
|
||||
#include <qlogging.h>
|
||||
#include <qloggingcategory.h>
|
||||
#include <qobject.h>
|
||||
#include <qstringview.h>
|
||||
#include <qtmetamacros.h>
|
||||
#include <qtypes.h>
|
||||
#include <spa/utils/json.h>
|
||||
#include <spa/param/param.h>
|
||||
#include <spa/utils/dict.h>
|
||||
|
||||
#include "registry.hpp"
|
||||
|
||||
|
@ -22,6 +23,14 @@ void PwMetadata::bindHooks() {
|
|||
|
||||
void PwMetadata::unbindHooks() { this->listener.remove(); }
|
||||
|
||||
void PwMetadata::initProps(const spa_dict* props) {
|
||||
if (const auto* name = spa_dict_lookup(props, PW_KEY_METADATA_NAME)) {
|
||||
this->mName = name;
|
||||
}
|
||||
}
|
||||
|
||||
const QString& PwMetadata::name() const { return this->mName; }
|
||||
|
||||
const pw_metadata_events PwMetadata::EVENTS = {
|
||||
.version = PW_VERSION_METADATA_EVENTS,
|
||||
.property = &PwMetadata::onProperty,
|
||||
|
@ -39,89 +48,26 @@ int PwMetadata::onProperty(
|
|||
<< "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.
|
||||
emit self->propertyChanged(key, type, value);
|
||||
|
||||
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
|
||||
);
|
||||
bool PwMetadata::hasSetPermission() const {
|
||||
return (this->perms & SPA_PARAM_INFO_WRITE) == SPA_PARAM_INFO_WRITE;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
if (strcmp(key, "default.audio.sink") == 0) {
|
||||
this->defaultSinkHolder.setObject(metadata);
|
||||
|
||||
auto newSink = PwDefaultsMetadata::parseNameSpaJson(value);
|
||||
qCInfo(logMeta) << "Got default sink" << newSink;
|
||||
if (newSink == this->mDefaultSink) return;
|
||||
|
||||
this->mDefaultSink = newSink;
|
||||
emit this->defaultSinkChanged();
|
||||
void PwMetadata::setProperty(const char* key, const char* type, const char* value) {
|
||||
if (this->proxy() == nullptr) {
|
||||
qCCritical(logMeta) << "Tried to change property of" << this << "which is not bound.";
|
||||
return;
|
||||
}
|
||||
|
||||
if (strcmp(key, "default.audio.source") == 0) {
|
||||
this->defaultSourceHolder.setObject(metadata);
|
||||
|
||||
auto newSource = PwDefaultsMetadata::parseNameSpaJson(value);
|
||||
qCInfo(logMeta) << "Got default source" << newSource;
|
||||
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 "";
|
||||
if (!this->hasSetPermission()) {
|
||||
qCCritical(logMeta) << "Tried to change property of" << this << "which is read-only.";
|
||||
}
|
||||
|
||||
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 "";
|
||||
pw_metadata_set_property(this->proxy(), PW_ID_CORE, key, type, value);
|
||||
}
|
||||
|
||||
} // namespace qs::service::pipewire
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
#include <pipewire/extensions/metadata.h>
|
||||
#include <pipewire/type.h>
|
||||
#include <qobject.h>
|
||||
#include <qstringview.h>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
#include "core.hpp"
|
||||
|
@ -18,45 +19,25 @@ class PwMetadata
|
|||
public:
|
||||
void bindHooks() override;
|
||||
void unbindHooks() override;
|
||||
void initProps(const spa_dict* props) override;
|
||||
|
||||
[[nodiscard]] const QString& name() const;
|
||||
[[nodiscard]] bool hasSetPermission() const;
|
||||
|
||||
// null value clears
|
||||
void setProperty(const char* key, const char* type, const char* value);
|
||||
|
||||
signals:
|
||||
void propertyChanged(const char* key, const char* type, const char* value);
|
||||
|
||||
private:
|
||||
static const pw_metadata_events EVENTS;
|
||||
static int
|
||||
onProperty(void* data, quint32 subject, const char* key, const char* type, const char* value);
|
||||
|
||||
QString mName;
|
||||
|
||||
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;
|
||||
|
||||
QString mDefaultSink;
|
||||
QString mDefaultSource;
|
||||
};
|
||||
|
||||
} // namespace qs::service::pipewire
|
||||
|
|
|
@ -10,8 +10,8 @@
|
|||
|
||||
#include "../../core/model.hpp"
|
||||
#include "connection.hpp"
|
||||
#include "defaults.hpp"
|
||||
#include "link.hpp"
|
||||
#include "metadata.hpp"
|
||||
#include "node.hpp"
|
||||
#include "registry.hpp"
|
||||
|
||||
|
@ -60,10 +60,33 @@ Pipewire::Pipewire(QObject* parent): QObject(parent) {
|
|||
&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
|
||||
QObject::connect(
|
||||
&connection->defaults,
|
||||
&PwDefaultTracker::defaultSinkChanged,
|
||||
this,
|
||||
&Pipewire::defaultAudioSinkChanged
|
||||
);
|
||||
|
||||
QObject::connect(
|
||||
&connection->defaults,
|
||||
&PwDefaultTracker::defaultSourceChanged,
|
||||
this,
|
||||
&Pipewire::defaultAudioSourceChanged
|
||||
);
|
||||
|
||||
QObject::connect(
|
||||
&connection->defaults,
|
||||
&PwDefaultTracker::defaultConfiguredSinkChanged,
|
||||
this,
|
||||
&Pipewire::defaultConfiguredAudioSinkChanged
|
||||
);
|
||||
|
||||
QObject::connect(
|
||||
&connection->defaults,
|
||||
&PwDefaultTracker::defaultConfiguredSourceChanged,
|
||||
this,
|
||||
&Pipewire::defaultConfiguredAudioSourceChanged
|
||||
);
|
||||
}
|
||||
|
||||
ObjectModel<PwNodeIface>* Pipewire::nodes() { return &this->mNodes; }
|
||||
|
@ -106,29 +129,31 @@ void Pipewire::onLinkGroupRemoved(QObject* object) {
|
|||
}
|
||||
|
||||
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;
|
||||
auto* node = PwConnection::instance()->defaults.defaultSink();
|
||||
return PwNodeIface::instance(node);
|
||||
}
|
||||
|
||||
PwNodeIface* Pipewire::defaultAudioSource() const { // NOLINT
|
||||
auto* connection = PwConnection::instance();
|
||||
auto name = connection->defaults.defaultSource();
|
||||
auto* node = PwConnection::instance()->defaults.defaultSource();
|
||||
return PwNodeIface::instance(node);
|
||||
}
|
||||
|
||||
for (auto* node: connection->registry.nodes.values()) {
|
||||
if (name == node->name) {
|
||||
return PwNodeIface::instance(node);
|
||||
}
|
||||
}
|
||||
PwNodeIface* Pipewire::defaultConfiguredAudioSink() const { // NOLINT
|
||||
auto* node = PwConnection::instance()->defaults.defaultConfiguredSink();
|
||||
return PwNodeIface::instance(node);
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
void Pipewire::setDefaultConfiguredAudioSink(PwNodeIface* node) {
|
||||
PwConnection::instance()->defaults.changeConfiguredSink(node ? node->node() : nullptr);
|
||||
}
|
||||
|
||||
PwNodeIface* Pipewire::defaultConfiguredAudioSource() const { // NOLINT
|
||||
auto* node = PwConnection::instance()->defaults.defaultConfiguredSource();
|
||||
return PwNodeIface::instance(node);
|
||||
}
|
||||
|
||||
void Pipewire::setDefaultConfiguredAudioSource(PwNodeIface* node) {
|
||||
PwConnection::instance()->defaults.changeConfiguredSource(node ? node->node() : nullptr);
|
||||
}
|
||||
|
||||
PwNodeIface* PwNodeLinkTracker::node() const { return this->mNode; }
|
||||
|
@ -305,6 +330,8 @@ QVariantMap PwNodeIface::properties() const {
|
|||
PwNodeAudioIface* PwNodeIface::audio() const { return this->audioIface; }
|
||||
|
||||
PwNodeIface* PwNodeIface::instance(PwNode* node) {
|
||||
if (node == nullptr) return nullptr;
|
||||
|
||||
auto v = node->property("iface");
|
||||
if (v.canConvert<PwNodeIface*>()) {
|
||||
return v.value<PwNodeIface*>();
|
||||
|
|
|
@ -52,16 +52,66 @@ private:
|
|||
class Pipewire: public QObject {
|
||||
Q_OBJECT;
|
||||
// clang-format off
|
||||
/// All pipewire nodes.
|
||||
/// All nodes present in pipewire.
|
||||
///
|
||||
/// This list contains every node on the system.
|
||||
/// To find a useful subset, filtering with the following properties may be helpful:
|
||||
/// - @@PwNode.isStream - if the node is an application or hardware device.
|
||||
/// - @@PwNode.isSink - if the node is a sink or source.
|
||||
/// - @@PwNode.audio - if non null the node is an audio node.
|
||||
Q_PROPERTY(ObjectModel<PwNodeIface>* nodes READ nodes CONSTANT);
|
||||
/// All pipewire links.
|
||||
/// All links present in pipewire.
|
||||
///
|
||||
/// Links connect pipewire nodes to each other, and can be used to determine
|
||||
/// their relationship.
|
||||
///
|
||||
/// If you already have a node you want to check for connections to,
|
||||
/// use @@PwNodeLinkTracker instead of filtering this list.
|
||||
///
|
||||
/// > [!INFO] Multiple links may exist between the same nodes. See @@linkGroups
|
||||
/// > for a deduplicated list containing only one entry per link between nodes.
|
||||
Q_PROPERTY(ObjectModel<PwLinkIface>* links READ links CONSTANT);
|
||||
/// All pipewire link groups.
|
||||
/// All link groups present in pipewire.
|
||||
///
|
||||
/// The same as @@links but deduplicated.
|
||||
///
|
||||
/// If you already have a node you want to check for connections to,
|
||||
/// use @@PwNodeLinkTracker instead of filtering this list.
|
||||
Q_PROPERTY(ObjectModel<PwLinkGroupIface>* linkGroups READ linkGroups CONSTANT);
|
||||
/// The default audio sink or `null`.
|
||||
/// The default audio sink (output) or `null`.
|
||||
///
|
||||
/// This is the default sink currently in use by pipewire, and the one applications
|
||||
/// are currently using.
|
||||
///
|
||||
/// To set the default sink, use @@preferredDefaultAudioSink.
|
||||
///
|
||||
/// > [!INFO] When the default sink changes, this property may breifly become null.
|
||||
/// > This depends on your hardware.
|
||||
Q_PROPERTY(PwNodeIface* defaultAudioSink READ defaultAudioSink NOTIFY defaultAudioSinkChanged);
|
||||
/// The default audio source or `null`.
|
||||
/// The default audio source (input) or `null`.
|
||||
///
|
||||
/// This is the default source currently in use by pipewire, and the one applications
|
||||
/// are currently using.
|
||||
///
|
||||
/// To set the default source, use @@preferredDefaultAudioSource.
|
||||
///
|
||||
/// > [!INFO] When the default source changes, this property may breifly become null.
|
||||
/// > This depends on your hardware.
|
||||
Q_PROPERTY(PwNodeIface* defaultAudioSource READ defaultAudioSource NOTIFY defaultAudioSourceChanged);
|
||||
/// The preferred default audio sink (output) or `null`.
|
||||
///
|
||||
/// This is a hint to pipewire telling it which sink should be the default when possible.
|
||||
/// @@defaultAudioSink may differ when it is not possible for pipewire to pick this node.
|
||||
///
|
||||
/// See @@defaultAudioSink for the current default sink, regardless of preference.
|
||||
Q_PROPERTY(PwNodeIface* preferredDefaultAudioSink READ defaultConfiguredAudioSink WRITE setDefaultConfiguredAudioSink NOTIFY defaultConfiguredAudioSinkChanged);
|
||||
/// The preferred default audio source (input) or `null`.
|
||||
///
|
||||
/// This is a hint to pipewire telling it which source should be the default when possible.
|
||||
/// @@defaultAudioSource may differ when it is not possible for pipewire to pick this node.
|
||||
///
|
||||
/// See @@defaultAudioSource for the current default source, regardless of preference.
|
||||
Q_PROPERTY(PwNodeIface* preferredDefaultAudioSource READ defaultConfiguredAudioSource WRITE setDefaultConfiguredAudioSource NOTIFY defaultConfiguredAudioSourceChanged);
|
||||
// clang-format on
|
||||
QML_ELEMENT;
|
||||
QML_SINGLETON;
|
||||
|
@ -72,13 +122,23 @@ public:
|
|||
[[nodiscard]] ObjectModel<PwNodeIface>* nodes();
|
||||
[[nodiscard]] ObjectModel<PwLinkIface>* links();
|
||||
[[nodiscard]] ObjectModel<PwLinkGroupIface>* linkGroups();
|
||||
|
||||
[[nodiscard]] PwNodeIface* defaultAudioSink() const;
|
||||
[[nodiscard]] PwNodeIface* defaultAudioSource() const;
|
||||
|
||||
[[nodiscard]] PwNodeIface* defaultConfiguredAudioSink() const;
|
||||
static void setDefaultConfiguredAudioSink(PwNodeIface* node);
|
||||
|
||||
[[nodiscard]] PwNodeIface* defaultConfiguredAudioSource() const;
|
||||
static void setDefaultConfiguredAudioSource(PwNodeIface* node);
|
||||
|
||||
signals:
|
||||
void defaultAudioSinkChanged();
|
||||
void defaultAudioSourceChanged();
|
||||
|
||||
void defaultConfiguredAudioSinkChanged();
|
||||
void defaultConfiguredAudioSourceChanged();
|
||||
|
||||
private slots:
|
||||
void onNodeAdded(PwNode* node);
|
||||
void onNodeRemoved(QObject* object);
|
||||
|
@ -191,7 +251,7 @@ class PwNodeIface: public PwObjectIface {
|
|||
Q_OBJECT;
|
||||
/// The pipewire object id of the node.
|
||||
///
|
||||
/// Mainly useful for debugging. you can inspect the node directly
|
||||
/// 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.
|
||||
|
@ -207,7 +267,8 @@ class PwNodeIface: public PwObjectIface {
|
|||
/// 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.
|
||||
/// If `true` then the node is likely to be a program, if `false` it is likely to be
|
||||
/// a hardware device.
|
||||
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>`.
|
||||
|
@ -223,6 +284,9 @@ class PwNodeIface: public PwObjectIface {
|
|||
/// > [!WARNING] 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.
|
||||
///
|
||||
/// The presence or absence of this property can be used to determine if a node
|
||||
/// manages audio, regardless of if it is bound. If non null, the node is an audio node.
|
||||
Q_PROPERTY(PwNodeAudioIface* audio READ audio CONSTANT);
|
||||
QML_NAMED_ELEMENT(PwNode);
|
||||
QML_UNCREATABLE("PwNodes cannot be created directly");
|
||||
|
@ -330,11 +394,19 @@ private:
|
|||
};
|
||||
|
||||
///! 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.
|
||||
/// PwObjectTracker binds every node given in its @@objects list.
|
||||
///
|
||||
/// #### Object Binding
|
||||
/// By default, pipewire objects are unbound. Unbound objects only have a subset of
|
||||
/// information available for use or modification. **Binding an object makes all of its
|
||||
/// properties available for use or modification if applicable.**
|
||||
///
|
||||
/// Properties that require their object be bound to use are clearly marked. You do not
|
||||
/// need to bind the object unless mentioned in the description of the property you
|
||||
/// want to use.
|
||||
class PwObjectTracker: public QObject {
|
||||
Q_OBJECT;
|
||||
/// The list of objects to bind.
|
||||
/// The list of objects to bind. May contain nulls.
|
||||
Q_PROPERTY(QList<QObject*> objects READ objects WRITE setObjects NOTIFY objectsChanged);
|
||||
QML_ELEMENT;
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
#include <qlogging.h>
|
||||
#include <qloggingcategory.h>
|
||||
#include <qobject.h>
|
||||
#include <qstringview.h>
|
||||
#include <qtmetamacros.h>
|
||||
#include <qtypes.h>
|
||||
|
||||
|
@ -143,7 +144,7 @@ void PwRegistry::onGlobal(
|
|||
meta->initProps(props);
|
||||
|
||||
self->metadata.emplace(id, meta);
|
||||
meta->bind();
|
||||
emit self->metadataAdded(meta);
|
||||
} else if (strcmp(type, PW_TYPE_INTERFACE_Link) == 0) {
|
||||
auto* link = new PwLink();
|
||||
link->init(self, id, permissions);
|
||||
|
@ -199,4 +200,14 @@ void PwRegistry::onLinkGroupDestroyed(QObject* object) {
|
|||
this->linkGroups.removeOne(group);
|
||||
}
|
||||
|
||||
PwNode* PwRegistry::findNodeByName(QStringView name) const {
|
||||
if (name.isEmpty()) return nullptr;
|
||||
|
||||
for (auto* node: this->nodes.values()) {
|
||||
if (node->name == name) return node;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
} // namespace qs::service::pipewire
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
#include <qhash.h>
|
||||
#include <qloggingcategory.h>
|
||||
#include <qobject.h>
|
||||
#include <qstringview.h>
|
||||
#include <qtclasshelpermacros.h>
|
||||
#include <qtmetamacros.h>
|
||||
#include <qtypes.h>
|
||||
|
@ -105,9 +106,8 @@ class PwBindableRef: public PwBindableObjectRef {
|
|||
public:
|
||||
explicit PwBindableRef(T* object = nullptr): PwBindableObjectRef(object) {}
|
||||
|
||||
T* object() { return static_cast<T*>(this->mObject); }
|
||||
void setObject(T* object) { this->PwBindableObjectRef::setObject(object); }
|
||||
|
||||
T* object() { return this->mObject; }
|
||||
};
|
||||
|
||||
class PwRegistry
|
||||
|
@ -127,17 +127,13 @@ public:
|
|||
|
||||
PwCore* core = nullptr;
|
||||
|
||||
[[nodiscard]] PwNode* findNodeByName(QStringView name) const;
|
||||
|
||||
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
|
||||
);
|
||||
void metadataAdded(PwMetadata* metadata);
|
||||
|
||||
private slots:
|
||||
void onLinkGroupDestroyed(QObject* object);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue