Milestone1
This commit is contained in:
parent
a1094ab7ea
commit
4addf989cc
17 changed files with 2876 additions and 0 deletions
528
src/pipewire/pipewirecontroller.cpp
Normal file
528
src/pipewire/pipewirecontroller.cpp
Normal 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, ®istry_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
|
||||
Loading…
Add table
Add a link
Reference in a new issue