Milestone1

This commit is contained in:
Joey Yakimowich-Payne 2026-01-27 15:24:29 -07:00
commit 4addf989cc
17 changed files with 2876 additions and 0 deletions

View file

@ -0,0 +1,528 @@
#include "pipewirecontroller.h"
#include <QDebug>
#include <QMutexLocker>
#include <QByteArray>
#include <QElapsedTimer>
#include <QThread>
#include <cstring>
#include <cstdlib>
#include <pipewire/pipewire.h>
#include <pipewire/keys.h>
#include <pipewire/properties.h>
#include <spa/param/props.h>
#include <spa/utils/dict.h>
#include <spa/utils/type-info.h>
namespace Potato {
static QString toQString(const char *value)
{
if (!value) {
return QString();
}
return QString::fromUtf8(QByteArray::fromRawData(value, static_cast<int>(strlen(value))));
}
void registryEventGlobal(void *data, uint32_t id, uint32_t permissions,
const char *type, uint32_t version,
const struct spa_dict *props)
{
Q_UNUSED(permissions)
Q_UNUSED(version)
auto *self = static_cast<PipeWireController*>(data);
if (strcmp(type, PW_TYPE_INTERFACE_Node) == 0) {
self->handleNodeInfo(id, props);
} else if (strcmp(type, PW_TYPE_INTERFACE_Port) == 0) {
self->handlePortInfo(id, props);
} else if (strcmp(type, PW_TYPE_INTERFACE_Link) == 0) {
self->handleLinkInfo(id, props);
}
}
void registryEventGlobalRemove(void *data, uint32_t id)
{
auto *self = static_cast<PipeWireController*>(data);
{
QMutexLocker lock(&self->m_nodesMutex);
if (self->m_nodes.contains(id)) {
self->m_nodes.remove(id);
emit self->nodeRemoved(id);
return;
}
if (self->m_ports.contains(id)) {
self->m_ports.remove(id);
return;
}
if (self->m_links.contains(id)) {
self->m_links.remove(id);
emit self->linkRemoved(id);
return;
}
}
}
void coreEventDone(void *data, uint32_t id, int seq)
{
Q_UNUSED(data)
Q_UNUSED(id)
Q_UNUSED(seq)
}
void coreEventError(void *data, uint32_t id, int seq, int res, const char *message)
{
Q_UNUSED(id)
Q_UNUSED(seq)
auto *self = static_cast<PipeWireController*>(data);
QString errorMsg = QString("PipeWire error (code ")
+ QString::number(res)
+ QString("): ")
+ toQString(message);
qWarning() << errorMsg;
emit self->errorOccurred(errorMsg);
if (res == -EPIPE) {
self->m_connected.storeRelaxed(false);
emit self->connectionLost();
}
}
static const struct pw_registry_events registry_events = []() {
struct pw_registry_events events{};
events.version = PW_VERSION_REGISTRY_EVENTS;
events.global = registryEventGlobal;
events.global_remove = registryEventGlobalRemove;
return events;
}();
static const struct pw_core_events core_events = []() {
struct pw_core_events events{};
events.version = PW_VERSION_CORE_EVENTS;
events.done = coreEventDone;
events.error = coreEventError;
return events;
}();
PipeWireController::PipeWireController(QObject *parent)
: QObject(parent)
{
m_registryListener = new spa_hook;
m_coreListener = new spa_hook;
}
PipeWireController::~PipeWireController()
{
shutdown();
delete m_registryListener;
delete m_coreListener;
}
bool PipeWireController::initialize()
{
if (m_initialized.loadRelaxed()) {
qWarning() << "PipeWireController already initialized";
return true;
}
pw_init(nullptr, nullptr);
m_threadLoop = pw_thread_loop_new("Potato-PW", nullptr);
if (!m_threadLoop) {
qCritical() << "Failed to create PipeWire thread loop";
emit errorOccurred("Failed to create PipeWire thread loop");
return false;
}
lock();
m_context = pw_context_new(pw_thread_loop_get_loop(m_threadLoop), nullptr, 0);
if (!m_context) {
unlock();
qCritical() << "Failed to create PipeWire context";
emit errorOccurred("Failed to create PipeWire context");
return false;
}
m_core = pw_context_connect(m_context, nullptr, 0);
if (!m_core) {
unlock();
qCritical() << "Failed to connect to PipeWire daemon";
emit errorOccurred("Failed to connect to PipeWire daemon. Is PipeWire running?");
return false;
}
pw_core_add_listener(m_core, m_coreListener, &core_events, this);
m_registry = pw_core_get_registry(m_core, PW_VERSION_REGISTRY, 0);
if (!m_registry) {
unlock();
qCritical() << "Failed to get PipeWire registry";
emit errorOccurred("Failed to get PipeWire registry");
return false;
}
pw_registry_add_listener(m_registry, m_registryListener, &registry_events, this);
unlock();
if (pw_thread_loop_start(m_threadLoop) < 0) {
qCritical() << "Failed to start PipeWire thread loop";
emit errorOccurred("Failed to start PipeWire thread loop");
return false;
}
m_initialized.storeRelaxed(true);
m_connected.storeRelaxed(true);
qInfo() << "PipeWire controller initialized successfully";
return true;
}
void PipeWireController::shutdown()
{
if (!m_initialized.loadRelaxed()) {
return;
}
if (m_threadLoop) {
pw_thread_loop_stop(m_threadLoop);
}
lock();
if (m_registry) {
pw_proxy_destroy(reinterpret_cast<struct pw_proxy*>(m_registry));
m_registry = nullptr;
}
if (m_core) {
pw_core_disconnect(m_core);
m_core = nullptr;
}
unlock();
if (m_context) {
pw_context_destroy(m_context);
m_context = nullptr;
}
if (m_threadLoop) {
pw_thread_loop_destroy(m_threadLoop);
m_threadLoop = nullptr;
}
pw_deinit();
m_initialized.storeRelaxed(false);
m_connected.storeRelaxed(false);
qInfo() << "PipeWire controller shut down";
}
bool PipeWireController::isConnected() const
{
return m_connected.loadRelaxed();
}
QVector<NodeInfo> PipeWireController::nodes() const
{
QMutexLocker lock(&m_nodesMutex);
return m_nodes.values().toVector();
}
NodeInfo PipeWireController::nodeById(uint32_t id) const
{
QMutexLocker lock(&m_nodesMutex);
return m_nodes.value(id);
}
QVector<LinkInfo> PipeWireController::links() const
{
QMutexLocker lock(&m_nodesMutex);
return m_links.values().toVector();
}
uint32_t PipeWireController::createLink(uint32_t outputNodeId, uint32_t outputPortId,
uint32_t inputNodeId, uint32_t inputPortId)
{
Q_UNUSED(outputNodeId)
Q_UNUSED(inputNodeId)
if (!m_connected.loadRelaxed()) {
qWarning() << "Cannot create link: not connected to PipeWire";
return 0;
}
lock();
QByteArray outNode = QByteArray::number(outputNodeId);
QByteArray outPort = QByteArray::number(outputPortId);
QByteArray inNode = QByteArray::number(inputNodeId);
QByteArray inPort = QByteArray::number(inputPortId);
struct pw_properties *props = pw_properties_new(
PW_KEY_LINK_OUTPUT_NODE, outNode.constData(),
PW_KEY_LINK_OUTPUT_PORT, outPort.constData(),
PW_KEY_LINK_INPUT_NODE, inNode.constData(),
PW_KEY_LINK_INPUT_PORT, inPort.constData(),
nullptr);
struct pw_proxy *proxy = static_cast<struct pw_proxy*>(pw_core_create_object(
m_core,
"link-factory",
PW_TYPE_INTERFACE_Link,
PW_VERSION_LINK,
&props->dict,
0));
if (!proxy) {
unlock();
qWarning() << "Failed to create link proxy";
pw_properties_free(props);
return 0;
}
unlock();
pw_properties_free(props);
uint32_t createdLinkId = 0;
QElapsedTimer timer;
timer.start();
while (timer.elapsed() < 2000) {
{
QMutexLocker lock(&m_nodesMutex);
for (auto it = m_links.cbegin(); it != m_links.cend(); ++it) {
const LinkInfo &link = it.value();
if (link.outputNodeId == outputNodeId &&
link.outputPortId == outputPortId &&
link.inputNodeId == inputNodeId &&
link.inputPortId == inputPortId) {
createdLinkId = link.id;
break;
}
}
}
if (createdLinkId != 0) {
break;
}
QThread::msleep(10);
}
if (createdLinkId != 0) {
qInfo() << "Link created:" << createdLinkId;
} else {
qWarning() << "Link created but ID not found in registry";
}
return createdLinkId;
}
bool PipeWireController::destroyLink(uint32_t linkId)
{
if (!m_connected.loadRelaxed()) {
qWarning() << "Cannot destroy link: not connected to PipeWire";
return false;
}
LinkInfo linkInfo;
{
QMutexLocker lock(&m_nodesMutex);
if (!m_links.contains(linkId)) {
qWarning() << "Link not found:" << linkId;
return false;
}
linkInfo = m_links.value(linkId);
}
lock();
struct pw_proxy *proxy = static_cast<struct pw_proxy*>(
pw_registry_bind(m_registry, linkId, PW_TYPE_INTERFACE_Link, PW_VERSION_LINK, 0));
if (proxy) {
pw_proxy_destroy(proxy);
}
unlock();
{
QMutexLocker lock(&m_nodesMutex);
m_links.remove(linkId);
}
emit linkRemoved(linkId);
qInfo() << "Link destroyed:" << linkId;
return true;
}
QString PipeWireController::dumpGraph() const
{
QMutexLocker lock(&m_nodesMutex);
QString dump;
dump += QString("=== PipeWire Graph Dump ===\n");
dump += QString("Nodes: %1\n").arg(m_nodes.size());
dump += QString("Ports: %1\n").arg(m_ports.size());
dump += QString("Links: %1\n\n").arg(m_links.size());
dump += QString("=== Nodes ===\n");
for (const auto &node : m_nodes) {
dump += QString("Node %1: %2\n").arg(node.id).arg(node.name);
dump += QString(" Description: %1\n").arg(node.description);
dump += QString(" Stable ID: %1\n").arg(node.stableId);
dump += QString(" Input ports: %1\n").arg(node.inputPorts.size());
dump += QString(" Output ports: %1\n").arg(node.outputPorts.size());
}
dump += QString("\n=== Links ===\n");
for (const auto &link : m_links) {
dump += QString("Link %1: Node %2:%3 -> Node %4:%5\n")
.arg(link.id)
.arg(link.outputNodeId).arg(link.outputPortId)
.arg(link.inputNodeId).arg(link.inputPortId);
}
return dump;
}
void PipeWireController::handleNodeInfo(uint32_t id, const struct spa_dict *props)
{
if (!props) {
return;
}
NodeInfo node;
node.id = id;
const char *name = spa_dict_lookup(props, PW_KEY_NODE_NAME);
const char *description = spa_dict_lookup(props, PW_KEY_NODE_DESCRIPTION);
const char *mediaClass = spa_dict_lookup(props, PW_KEY_MEDIA_CLASS);
const char *appName = spa_dict_lookup(props, PW_KEY_APP_NAME);
node.name = name ? toQString(name) : QString("Unknown");
node.description = description ? toQString(description) : node.name;
node.stableId = node.name;
QString mediaClassStr = mediaClass ? toQString(mediaClass) : QString();
QString appNameStr = appName ? toQString(appName) : QString();
node.mediaClass = NodeInfo::mediaClassFromString(mediaClassStr);
node.type = NodeInfo::typeFromProperties(mediaClassStr, appNameStr);
{
QMutexLocker lock(&m_nodesMutex);
bool isNewNode = !m_nodes.contains(id);
m_nodes.insert(id, node);
if (isNewNode) {
emit nodeAdded(node);
qDebug() << "Node added:" << node.id << node.name;
} else {
emit nodeChanged(node);
qDebug() << "Node changed:" << node.id << node.name;
}
}
}
void PipeWireController::handlePortInfo(uint32_t id, const struct spa_dict *props)
{
if (!props) {
return;
}
const char *name = spa_dict_lookup(props, PW_KEY_PORT_NAME);
const char *directionStr = spa_dict_lookup(props, PW_KEY_PORT_DIRECTION);
const char *nodeIdStr = spa_dict_lookup(props, PW_KEY_NODE_ID);
uint32_t direction = 0;
if (directionStr) {
if (strcmp(directionStr, "in") == 0) {
direction = PW_DIRECTION_INPUT;
} else if (strcmp(directionStr, "out") == 0) {
direction = PW_DIRECTION_OUTPUT;
}
}
QString portName = name ? toQString(name)
: QString("port_") + QString::number(id);
PortInfo port(id, portName, direction);
{
QMutexLocker lock(&m_nodesMutex);
m_ports.insert(id, port);
if (nodeIdStr) {
uint32_t nodeId = static_cast<uint32_t>(atoi(nodeIdStr));
if (m_nodes.contains(nodeId)) {
NodeInfo &node = m_nodes[nodeId];
if (direction == PW_DIRECTION_INPUT) {
node.inputPorts.append(port);
} else if (direction == PW_DIRECTION_OUTPUT) {
node.outputPorts.append(port);
}
}
}
}
qDebug() << "Port added:" << id << portName << "direction:" << direction;
}
void PipeWireController::handleLinkInfo(uint32_t id, const struct spa_dict *props)
{
if (!props) {
return;
}
const char *outputNodeStr = spa_dict_lookup(props, PW_KEY_LINK_OUTPUT_NODE);
const char *outputPortStr = spa_dict_lookup(props, PW_KEY_LINK_OUTPUT_PORT);
const char *inputNodeStr = spa_dict_lookup(props, PW_KEY_LINK_INPUT_NODE);
const char *inputPortStr = spa_dict_lookup(props, PW_KEY_LINK_INPUT_PORT);
uint32_t outputNode = outputNodeStr ? static_cast<uint32_t>(atoi(outputNodeStr)) : 0;
uint32_t outputPort = outputPortStr ? static_cast<uint32_t>(atoi(outputPortStr)) : 0;
uint32_t inputNode = inputNodeStr ? static_cast<uint32_t>(atoi(inputNodeStr)) : 0;
uint32_t inputPort = inputPortStr ? static_cast<uint32_t>(atoi(inputPortStr)) : 0;
LinkInfo link(id, outputNode, outputPort, inputNode, inputPort);
{
QMutexLocker lock(&m_nodesMutex);
m_links.insert(id, link);
}
emit linkAdded(link);
qDebug() << "Link added:" << id << "from" << outputNode << ":" << outputPort
<< "to" << inputNode << ":" << inputPort;
}
void PipeWireController::lock()
{
if (m_threadLoop) {
pw_thread_loop_lock(m_threadLoop);
}
}
void PipeWireController::unlock()
{
if (m_threadLoop) {
pw_thread_loop_unlock(m_threadLoop);
}
}
} // namespace Potato