283 lines
6.5 KiB
C++
283 lines
6.5 KiB
C++
#include "pipewire.hpp"
|
|
|
|
#include <algorithm>
|
|
|
|
#include <QJsonArray>
|
|
#include <QJsonDocument>
|
|
#include <QJsonObject>
|
|
|
|
namespace Pipewire
|
|
{
|
|
|
|
QString Node::get_display_name() const
|
|
{
|
|
if (!application_name.isEmpty()) return application_name;
|
|
if (!node_name.isEmpty()) return node_name;
|
|
return "Unknown App";
|
|
}
|
|
|
|
Graph::Graph()
|
|
{
|
|
refresh();
|
|
}
|
|
|
|
bool Graph::refresh()
|
|
{
|
|
QProcess process;
|
|
process.start("pw-dump");
|
|
if (!process.waitForFinished()) {
|
|
return false;
|
|
}
|
|
|
|
QJsonParseError error;
|
|
QJsonDocument json = QJsonDocument::fromJson(process.readAllStandardOutput(), &error);
|
|
if (error.error != QJsonParseError::NoError) {
|
|
return false;
|
|
}
|
|
|
|
m_nodes.clear();
|
|
m_ports.clear();
|
|
m_links.clear();
|
|
|
|
for (const QJsonValue& value : json.array()) {
|
|
QJsonObject obj = value.toObject();
|
|
QString type = obj["type"].toString();
|
|
int id = obj["id"].toInt();
|
|
QJsonObject props = obj["info"].toObject()["props"].toObject();
|
|
|
|
if (type == "PipeWire:Interface:Node") {
|
|
m_nodes[id] = {
|
|
id,
|
|
props["node.name"].toString(),
|
|
props["application.name"].toString(),
|
|
props["media.class"].toString()
|
|
};
|
|
} else if (type == "PipeWire:Interface:Port") {
|
|
m_ports[id] = {
|
|
id,
|
|
props["node.id"].toInt(),
|
|
props["port.direction"].toString(),
|
|
props["audio.channel"].toString()
|
|
};
|
|
} else if (type == "PipeWire:Interface:Link") {
|
|
m_links[id] = {
|
|
id,
|
|
props["link.output.node"].toInt(),
|
|
props["link.input.node"].toInt()
|
|
};
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
std::vector<Node> Graph::get_input_apps() const
|
|
{
|
|
std::vector<Node> apps;
|
|
for (const auto& [id, node] : m_nodes) {
|
|
if (node.media_class == "Stream/Input/Audio") {
|
|
apps.push_back(node);
|
|
}
|
|
}
|
|
return apps;
|
|
}
|
|
|
|
std::optional<Node> Graph::get_node_by_name(const QString& name) const
|
|
{
|
|
for (const auto& [id, node] : m_nodes) {
|
|
if (node.node_name == name) return node;
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
|
|
std::vector<Port> Graph::get_ports_for_node(int node_id, const QString& direction) const
|
|
{
|
|
std::vector<Port> ports;
|
|
for (const auto& [id, port] : m_ports) {
|
|
if (port.node_id == node_id && port.direction == direction) {
|
|
ports.push_back(port);
|
|
}
|
|
}
|
|
return ports;
|
|
}
|
|
|
|
std::vector<Link> Graph::get_links_from_node(int output_node_id) const
|
|
{
|
|
std::vector<Link> links;
|
|
for (const auto& [id, link] : m_links) {
|
|
if (link.output_node_id == output_node_id) {
|
|
links.push_back(link);
|
|
}
|
|
}
|
|
return links;
|
|
}
|
|
|
|
std::vector<Node> Graph::get_all_sinks() const
|
|
{
|
|
std::vector<Node> sinks;
|
|
for (const auto& [id, node] : m_nodes) {
|
|
if (node.media_class == "Audio/Sink") {
|
|
sinks.push_back(node);
|
|
}
|
|
}
|
|
return sinks;
|
|
}
|
|
|
|
Player::~Player()
|
|
{
|
|
stop_all();
|
|
}
|
|
|
|
void Player::play_file(const QString& file_path, const QString& target_node/* = "auto" */)
|
|
{
|
|
auto process = std::make_unique<QProcess>();
|
|
process->start("pw-play", { "--target", target_node, file_path });
|
|
m_playing_processes.push_back(std::move(process));
|
|
}
|
|
|
|
void Player::stop_all()
|
|
{
|
|
for (const auto& process : m_playing_processes) {
|
|
if (process->state() == QProcess::Running) {
|
|
process->terminate();
|
|
process->waitForFinished();
|
|
}
|
|
}
|
|
|
|
m_playing_processes.clear();
|
|
}
|
|
|
|
std::optional<Sink> Sink::create(const QString& name, Graph& graph)
|
|
{
|
|
QProcess process;
|
|
process.start("pactl", {
|
|
"load-module", "module-null-sink",
|
|
"sink_name=" + name,
|
|
"sink_properties=device.description=" + name,
|
|
});
|
|
|
|
if (!process.waitForFinished()) {
|
|
return std::nullopt;
|
|
}
|
|
|
|
QByteArray output = process.readAllStandardOutput().trimmed();
|
|
bool ok = false;
|
|
int pulse_audio_id = output.toInt(&ok);
|
|
if (!ok) {
|
|
return std::nullopt;
|
|
}
|
|
|
|
if (!graph.refresh()) {
|
|
destroy(pulse_audio_id);
|
|
return std::nullopt;
|
|
}
|
|
|
|
auto node = graph.get_node_by_name(name);
|
|
if (!node) {
|
|
destroy(pulse_audio_id);
|
|
return std::nullopt;
|
|
}
|
|
|
|
Sink sink;
|
|
sink.m_name = name;
|
|
sink.m_pulse_audio_id = pulse_audio_id;
|
|
sink.m_pipewire_id = node->id;
|
|
|
|
return sink;
|
|
}
|
|
|
|
Sink::~Sink()
|
|
{
|
|
destroy(m_pulse_audio_id);
|
|
}
|
|
|
|
Sink::Sink(Sink&& other) :
|
|
m_name(std::move(other.m_name)),
|
|
m_pulse_audio_id(std::exchange(other.m_pulse_audio_id, -1)),
|
|
m_pipewire_id(std::exchange(other.m_pipewire_id, -1))
|
|
{
|
|
}
|
|
|
|
Sink& Sink::operator=(Sink&& other)
|
|
{
|
|
if (this == &other) {
|
|
return *this;
|
|
}
|
|
|
|
destroy(m_pulse_audio_id);
|
|
|
|
m_name = std::move(other.m_name);
|
|
m_pulse_audio_id = std::exchange(other.m_pulse_audio_id, -1);
|
|
m_pipewire_id = std::exchange(other.m_pipewire_id, -1);
|
|
|
|
return *this;
|
|
}
|
|
|
|
int Sink::get_id() const
|
|
{
|
|
return m_pipewire_id;
|
|
}
|
|
|
|
QString Sink::get_name() const
|
|
{
|
|
return m_name;
|
|
}
|
|
|
|
void Sink::destroy(int pulse_audio_id)
|
|
{
|
|
if (pulse_audio_id == -1) {
|
|
return;
|
|
}
|
|
|
|
QProcess process;
|
|
process.start("pactl", { "unload-module", QString::number(pulse_audio_id) });
|
|
process.waitForFinished();
|
|
}
|
|
|
|
bool link_ports(int out_port_id, int in_port_id)
|
|
{
|
|
QProcess process;
|
|
process.start("pw-link", { QString::number(out_port_id), QString::number(in_port_id) });
|
|
return process.waitForFinished() && process.exitCode() == 0;
|
|
}
|
|
|
|
bool link_nodes(int out_node_id, int in_node_id, const Graph& graph)
|
|
{
|
|
auto out_ports = graph.get_ports_for_node(out_node_id, "out");
|
|
auto in_ports = graph.get_ports_for_node(in_node_id, "in");
|
|
|
|
for (const Port& out_port : out_ports) {
|
|
for (const Port& in_port : in_ports) {
|
|
if (out_port.channel != in_port.channel) {
|
|
continue;
|
|
}
|
|
|
|
if (!link_ports(out_port.id, in_port.id)) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool destroy_link(int link_id)
|
|
{
|
|
QProcess process;
|
|
process.start("pw-cli", { "destroy", QString::number(link_id) });
|
|
return process.waitForFinished() && process.exitCode() == 0;
|
|
}
|
|
|
|
void set_volume(const QString& target_node, int volume_percent)
|
|
{
|
|
volume_percent = std::max(0, std::min(100, volume_percent));
|
|
|
|
QProcess process;
|
|
process.startDetached("pactl", {
|
|
"set-sink-volume", target_node,
|
|
QString::number(volume_percent) + "%"
|
|
});
|
|
}
|
|
|
|
} // namespace Pipewire
|