From ee570ec623a59561c96de2eea5a4af51385db38e Mon Sep 17 00:00:00 2001 From: nydragon Date: Sat, 7 Jun 2025 03:26:55 -0700 Subject: [PATCH] services/pipewire: expose node type --- src/services/pipewire/defaults.cpp | 4 +- src/services/pipewire/node.cpp | 39 ++++++++++------ src/services/pipewire/node.hpp | 71 +++++++++++++++++++++++++++--- src/services/pipewire/qml.cpp | 6 ++- src/services/pipewire/qml.hpp | 3 ++ 5 files changed, 100 insertions(+), 23 deletions(-) diff --git a/src/services/pipewire/defaults.cpp b/src/services/pipewire/defaults.cpp index 0333c87f..9ff37e09 100644 --- a/src/services/pipewire/defaults.cpp +++ b/src/services/pipewire/defaults.cpp @@ -146,7 +146,7 @@ void PwDefaultTracker::onNodeDestroyed(QObject* node) { void PwDefaultTracker::changeConfiguredSink(PwNode* node) { if (node != nullptr) { - if (!node->isSink) { + if (!node->type.testFlags(PwNodeType::AudioSink)) { qCCritical(logDefaults) << "Cannot change default sink to a node that is not a sink."; return; } @@ -168,7 +168,7 @@ void PwDefaultTracker::changeConfiguredSinkName(const QString& sink) { void PwDefaultTracker::changeConfiguredSource(PwNode* node) { if (node != nullptr) { - if (node->isSink) { + if (!node->type.testFlags(PwNodeType::AudioSource)) { qCCritical(logDefaults) << "Cannot change default source to a node that is not a source."; return; } diff --git a/src/services/pipewire/node.cpp b/src/services/pipewire/node.cpp index 21815b82..2aff5958 100644 --- a/src/services/pipewire/node.cpp +++ b/src/services/pipewire/node.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -85,6 +86,20 @@ QString PwAudioChannel::toString(Enum value) { } } +QString PwNodeType::toString(PwNodeType::Flags type) { + switch (type) { + case PwNodeType::VideoSource: return QStringLiteral("VideoSource"); + case PwNodeType::VideoSink: return QStringLiteral("VideoSink"); + case PwNodeType::AudioSource: return QStringLiteral("AudioSource"); + case PwNodeType::AudioSink: return QStringLiteral("AudioSink"); + case PwNodeType::AudioDuplex: return QStringLiteral("AudioDuplex"); + case PwNodeType::AudioOutStream: return QStringLiteral("AudioOutStream"); + case PwNodeType::AudioInStream: return QStringLiteral("AudioInStream"); + case PwNodeType::Untracked: return QStringLiteral("Untracked"); + default: return QStringLiteral("Invalid"); + } +} + void PwNode::bindHooks() { // Bind the device first as pw is in order, meaning the device should be bound before // we want to do anything with it. @@ -116,21 +131,19 @@ void PwNode::unbindHooks() { 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; + this->type = PwNodeType::AudioSink; } else if (strcmp(mediaClass, "Audio/Source") == 0) { - this->type = PwNodeType::Audio; - this->isSink = false; - this->isStream = false; + this->type = PwNodeType::AudioSource; + } else if (strcmp(mediaClass, "Audio/Duplex") == 0) { + this->type = PwNodeType::AudioDuplex; } else if (strcmp(mediaClass, "Stream/Output/Audio") == 0) { - this->type = PwNodeType::Audio; - this->isSink = false; - this->isStream = true; + this->type = PwNodeType::AudioOutStream; } else if (strcmp(mediaClass, "Stream/Input/Audio") == 0) { - this->type = PwNodeType::Audio; - this->isSink = true; - this->isStream = true; + this->type = PwNodeType::AudioInStream; + } else if (strcmp(mediaClass, "Video/Sink") == 0) { + this->type = PwNodeType::VideoSink; + } else if (strcmp(mediaClass, "Video/Source") == 0) { + this->type = PwNodeType::VideoSource; } } @@ -164,7 +177,7 @@ void PwNode::initProps(const spa_dict* props) { } } - if (this->type == PwNodeType::Audio) { + if (this->type.testFlags(PwNodeType::Audio)) { this->boundData = new PwNodeBoundAudio(this); } } diff --git a/src/services/pipewire/node.hpp b/src/services/pipewire/node.hpp index a18abccf..aca949f2 100644 --- a/src/services/pipewire/node.hpp +++ b/src/services/pipewire/node.hpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -86,12 +87,71 @@ class PwAudioChannel: public QObject { /// including aux and custom channel ranges. Q_INVOKABLE static QString toString(qs::service::pipewire::PwAudioChannel::Enum value); }; +///! The type of a pipewire node. +/// Use bitwise comparisons to filter for audio, video, sink, source or stream nodes +class PwNodeType: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_SINGLETON; -enum class PwNodeType : quint8 { - Untracked, - Audio, +public: + enum Flag : quint8 { + // A Pipewire node which is not being managed. + Untracked = 0b0, + // This flag is set when this node is an Audio node. + Audio = 0b1, + // This flag is set when this node is an Video node. + Video = 0b10, + // This flag is set when this node is a stream node. + Stream = 0b100, + // This flag is set when this node is producing some form of data, + // such as a microphone, screenshare or webcam. + Source = 0b1000, + // This flag is set when this node is receiving data. + Sink = 0b10000, + // A sink for audio samples, like an audio card. + // + // This is equivalent to the media class `Video/Source` and is + // composed of the @@PwNodeType.Audio and @@PwNodeType.Sink flags. + AudioSink = Audio | Sink, + // A source of audio samples like a microphone. + // + // This is quivalent to the media class `Video/Sink` and is composed + // of the @@PwNodeType.Audio and @@PwNodeType.Source flags. + AudioSource = Audio | Source, + // A node that is both a sink and a source. + // + // This is equivalent to the media class `Audio/Duplex` and is composed of the + // @@PwNodeType.Audio, @@PwNodeType.Source and @@PwNodeType.Sink flags. + AudioDuplex = Audio | Sink | Source, + // A playback stream. + // + // This is equivalent to the media class `Stream/Output/Audio` and is composed + // of the @@PwNodeType.Audio, @@PwNodeType.Sink and @@PwNodeType.Stream flags. + AudioOutStream = Audio | Sink | Stream, + // A capture stream. + // + // This is equivalent to the media class `Stream/Input/Audio` and is composed + // of the @@PwNodeType.Audio, @@PwNodeType.Source and @@PwNodeType.Stream flags. + AudioInStream = Audio | Source | Stream, + // A producer of video, like a webcam or a screenshare. + // + // This is equivalent to the media class `Video/Source` and is composed + // of the @@PwNodeType.Video and @@PwNodeType.Source flags. + VideoSource = Video | Source, + // A consumer of video, such as a program that is recieving a video stream. + // + // This is equivalent to the media class `Video/Sink` and is composed of the + // @@PwNodeType.Video and @@PwNodeType.Sink flags. + VideoSink = Video | Sink, + }; + Q_ENUM(Flag) + Q_DECLARE_FLAGS(Flags, Flag) + Q_INVOKABLE static QString toString(qs::service::pipewire::PwNodeType::Flags type); }; +Q_DECLARE_OPERATORS_FOR_FLAGS(PwNodeType::Flags) + class PwNode; struct PwVolumeProps { @@ -169,9 +229,8 @@ class PwNode: public PwBindable properties; - PwNodeType type = PwNodeType::Untracked; - bool isSink = false; - bool isStream = false; + PwNodeType::Flags type = PwNodeType::Untracked; + bool ready = false; PwNodeBoundData* boundData = nullptr; diff --git a/src/services/pipewire/qml.cpp b/src/services/pipewire/qml.cpp index 6eef238b..5d8c45e7 100644 --- a/src/services/pipewire/qml.cpp +++ b/src/services/pipewire/qml.cpp @@ -328,12 +328,14 @@ 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::isSink() const { return this->mNode->type.testFlags(PwNodeType::Sink); } -bool PwNodeIface::isStream() const { return this->mNode->isStream; } +bool PwNodeIface::isStream() const { return this->mNode->type.testFlags(PwNodeType::Stream); } bool PwNodeIface::isReady() const { return this->mNode->ready; } +PwNodeType::Flags PwNodeIface::type() const { return this->mNode->type; }; + QVariantMap PwNodeIface::properties() const { auto map = QVariantMap(); for (auto [k, v]: this->mNode->properties.asKeyValueRange()) { diff --git a/src/services/pipewire/qml.hpp b/src/services/pipewire/qml.hpp index 6313a42b..2ff7a7a3 100644 --- a/src/services/pipewire/qml.hpp +++ b/src/services/pipewire/qml.hpp @@ -287,6 +287,8 @@ class PwNodeIface: public PwObjectIface { /// 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 type of this node. Reflects Pipewire's [media.class](https://docs.pipewire.org/page_man_pipewire-props_7.html). + Q_PROPERTY(qs::service::pipewire::PwNodeType::Flags type READ type CONSTANT); /// The property set present on the node, as an object containing key-value pairs. /// You can inspect this directly with `pw-cli i `. /// @@ -324,6 +326,7 @@ class PwNodeIface: public PwObjectIface { [[nodiscard]] bool isSink() const; [[nodiscard]] bool isStream() const; [[nodiscard]] bool isReady() const; + [[nodiscard]] PwNodeType::Flags type() const; [[nodiscard]] QVariantMap properties() const; [[nodiscard]] PwNodeAudioIface* audio() const;