Skip to content

services/pipewire: expose type of node #41

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/services/pipewire/defaults.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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;
}
Expand Down
39 changes: 26 additions & 13 deletions src/services/pipewire/node.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qobject.h>
#include <qstringliteral.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include <spa/node/keys.h>
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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;
}
}

Expand Down Expand Up @@ -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);
}
}
Expand Down
71 changes: 65 additions & 6 deletions src/services/pipewire/node.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include <pipewire/node.h>
#include <pipewire/type.h>
#include <qcontainerfwd.h>
#include <qflags.h>
#include <qmap.h>
#include <qobject.h>
#include <qqmlintegration.h>
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -169,9 +229,8 @@ class PwNode: public PwBindable<pw_node, PW_TYPE_INTERFACE_Node, PW_VERSION_NODE
QString nick;
QMap<QString, QString> properties;

PwNodeType type = PwNodeType::Untracked;
bool isSink = false;
bool isStream = false;
PwNodeType::Flags type = PwNodeType::Untracked;

bool ready = false;

PwNodeBoundData* boundData = nullptr;
Expand Down
6 changes: 4 additions & 2 deletions src/services/pipewire/qml.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
Expand Down
3 changes: 3 additions & 0 deletions src/services/pipewire/qml.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 <id>`.
///
Expand Down Expand Up @@ -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;

Expand Down