diff --git a/src/gui/GraphEditorWidget.cpp b/src/gui/GraphEditorWidget.cpp index fec1f70..e76dbf9 100644 --- a/src/gui/GraphEditorWidget.cpp +++ b/src/gui/GraphEditorWidget.cpp @@ -227,7 +227,6 @@ void GraphEditorWidget::syncGraph() refreshNodeMeter(node.id, node); } } - const QVector links = m_controller->links(); for (const auto &link : links) { onLinkAdded(link); @@ -241,6 +240,8 @@ void GraphEditorWidget::refreshGraph() m_ignoreDelete.clear(); m_connectionToLinkId.clear(); m_linkIdToConnection.clear(); + m_nodeLinkCounts.clear(); + m_linksById.clear(); m_model->reset(); syncGraph(); @@ -268,6 +269,7 @@ void GraphEditorWidget::onNodeChanged(const Potato::NodeInfo &node) } refreshNodeMeter(node.id, node); + updateNodeMeterState(node.id, node); } void GraphEditorWidget::onNodeRemoved(uint32_t nodeId) @@ -275,6 +277,7 @@ void GraphEditorWidget::onNodeRemoved(uint32_t nodeId) m_model->removePipeWireNode(nodeId); m_controller->removeNodeMeter(nodeId); + m_nodeLinkCounts.remove(nodeId); if (m_nodeMeterRows.contains(nodeId)) { QWidget *row = m_nodeMeterRows.take(nodeId); @@ -295,6 +298,23 @@ void GraphEditorWidget::onLinkAdded(const Potato::LinkInfo &link) } const QString key = connectionKey(connectionId); + const bool alreadyTracked = m_linksById.contains(link.id); + if (!alreadyTracked) { + m_linksById.insert(link.id, link); + if (!isMeterNode(link.outputNodeId) && !isMeterNode(link.inputNodeId)) { + m_nodeLinkCounts[link.outputNodeId] = m_nodeLinkCounts.value(link.outputNodeId, 0) + 1; + m_nodeLinkCounts[link.inputNodeId] = m_nodeLinkCounts.value(link.inputNodeId, 0) + 1; + + const Potato::NodeInfo outNode = m_controller->nodeById(link.outputNodeId); + if (outNode.id != 0) { + updateNodeMeterState(outNode.id, outNode); + } + const Potato::NodeInfo inNode = m_controller->nodeById(link.inputNodeId); + if (inNode.id != 0) { + updateNodeMeterState(inNode.id, inNode); + } + } + } if (m_connectionToLinkId.contains(key)) { return; } @@ -310,16 +330,22 @@ void GraphEditorWidget::onLinkAdded(const Potato::LinkInfo &link) void GraphEditorWidget::onLinkRemoved(uint32_t linkId) { - if (!m_linkIdToConnection.contains(linkId)) { - m_model->removePipeWireConnection(linkId); + if (m_ignoreLinkRemoved.contains(linkId)) { + m_ignoreLinkRemoved.remove(linkId); return; } - const QString key = m_linkIdToConnection.value(linkId); - m_ignoreDelete.insert(key); - m_linkIdToConnection.remove(linkId); - m_connectionToLinkId.remove(key); - m_model->removePipeWireConnection(linkId); + if (m_linkIdToConnection.contains(linkId)) { + const QString key = m_linkIdToConnection.value(linkId); + m_ignoreDelete.insert(key); + m_linkIdToConnection.remove(linkId); + m_connectionToLinkId.remove(key); + m_model->removePipeWireConnection(linkId); + } else { + m_model->removePipeWireConnection(linkId); + } + + handleLinkRemoved(linkId); } void GraphEditorWidget::onConnectionCreated(QtNodes::ConnectionId const connectionId) @@ -362,6 +388,17 @@ void GraphEditorWidget::onConnectionCreated(QtNodes::ConnectionId const connecti m_connectionToLinkId.insert(key, linkId); m_linkIdToConnection.insert(linkId, key); + + const Potato::LinkInfo link(linkId, outInfo->id, outputPortId, inInfo->id, inputPortId); + if (!m_linksById.contains(linkId)) { + m_linksById.insert(linkId, link); + if (!isMeterNode(link.outputNodeId) && !isMeterNode(link.inputNodeId)) { + m_nodeLinkCounts[link.outputNodeId] = m_nodeLinkCounts.value(link.outputNodeId, 0) + 1; + m_nodeLinkCounts[link.inputNodeId] = m_nodeLinkCounts.value(link.inputNodeId, 0) + 1; + updateNodeMeterState(outInfo->id, *outInfo); + updateNodeMeterState(inInfo->id, *inInfo); + } + } } void GraphEditorWidget::onConnectionDeleted(QtNodes::ConnectionId const connectionId) @@ -379,6 +416,8 @@ void GraphEditorWidget::onConnectionDeleted(QtNodes::ConnectionId const connecti const uint32_t linkId = m_connectionToLinkId.value(key); m_connectionToLinkId.remove(key); m_linkIdToConnection.remove(linkId); + m_ignoreLinkRemoved.insert(linkId); + handleLinkRemoved(linkId); m_controller->destroyLink(linkId); } @@ -425,20 +464,6 @@ void GraphEditorWidget::refreshNodeMeter(uint32_t nodeId, const Potato::NodeInfo return; } - if (!node.name.isEmpty()) { - bool captureSink = node.mediaClass == Potato::MediaClass::AudioSink - || node.mediaClass == Potato::MediaClass::AudioDuplex; - if (!captureSink) { - for (const auto &port : node.outputPorts) { - if (port.name.contains("monitor", Qt::CaseInsensitive)) { - captureSink = true; - break; - } - } - } - m_controller->ensureNodeMeter(nodeId, node.name, captureSink); - } - auto *row = new QWidget(m_meterList); auto *rowLayout = new QHBoxLayout(row); rowLayout->setContentsMargins(0, 0, 0, 0); @@ -466,6 +491,103 @@ void GraphEditorWidget::refreshNodeMeter(uint32_t nodeId, const Potato::NodeInfo m_nodeMeterRows.insert(nodeId, row); m_nodeMeterLabels.insert(nodeId, label); updateNodeMeterLabel(label); + updateNodeMeterState(nodeId, node); +} + +void GraphEditorWidget::updateNodeMeterState(uint32_t nodeId, const Potato::NodeInfo &node) +{ + const int linkCount = activeLinkCount(nodeId); + if (linkCount <= 0) { + m_controller->removeNodeMeter(nodeId); + return; + } + + if (node.name.isEmpty()) { + return; + } + + bool captureSink = node.mediaClass == Potato::MediaClass::AudioSink + || node.mediaClass == Potato::MediaClass::AudioDuplex; + if (!captureSink) { + for (const auto &port : node.outputPorts) { + if (port.name.contains("monitor", Qt::CaseInsensitive)) { + captureSink = true; + break; + } + } + } + + m_controller->ensureNodeMeter(nodeId, node.name, captureSink); +} + +void GraphEditorWidget::handleLinkRemoved(uint32_t linkId) +{ + Potato::LinkInfo link; + bool haveLink = false; + if (m_linksById.contains(linkId)) { + link = m_linksById.take(linkId); + haveLink = true; + } else { + const QVector links = m_controller->links(); + for (const auto &candidate : links) { + if (candidate.id == linkId) { + link = candidate; + haveLink = true; + break; + } + } + } + + if (!haveLink) { + return; + } + + if (isMeterNode(link.outputNodeId) || isMeterNode(link.inputNodeId)) { + return; + } + + const auto decrement = [this](uint32_t nodeId) { + if (nodeId == 0) { + return; + } + const int current = m_nodeLinkCounts.value(nodeId, 0); + if (current <= 1) { + m_nodeLinkCounts.remove(nodeId); + } else { + m_nodeLinkCounts[nodeId] = current - 1; + } + const Potato::NodeInfo node = m_controller->nodeById(nodeId); + if (node.id != 0) { + updateNodeMeterState(node.id, node); + } + }; + + decrement(link.outputNodeId); + decrement(link.inputNodeId); +} + +bool GraphEditorWidget::isMeterNode(uint32_t nodeId) const +{ + const Potato::NodeInfo node = m_controller->nodeById(nodeId); + if (node.name.startsWith("Potato-Meter") || node.name.startsWith("Potato-Node-Meter")) { + return true; + } + return false; +} + +int GraphEditorWidget::activeLinkCount(uint32_t nodeId) const +{ + int count = 0; + for (auto it = m_linksById.cbegin(); it != m_linksById.cend(); ++it) { + const Potato::LinkInfo &link = it.value(); + if (isMeterNode(link.outputNodeId) || isMeterNode(link.inputNodeId)) { + continue; + } + if (link.outputNodeId == nodeId || link.inputNodeId == nodeId) { + ++count; + } + } + return count; } void GraphEditorWidget::updateNodeMeterLabel(QLabel *label) diff --git a/src/gui/GraphEditorWidget.h b/src/gui/GraphEditorWidget.h index cead265..0a7ecb3 100644 --- a/src/gui/GraphEditorWidget.h +++ b/src/gui/GraphEditorWidget.h @@ -41,6 +41,10 @@ private: void refreshGraph(); void updateLayoutState(); void updateNodeMeterLabel(QLabel *label); + void updateNodeMeterState(uint32_t nodeId, const Potato::NodeInfo &node); + void handleLinkRemoved(uint32_t linkId); + bool isMeterNode(uint32_t nodeId) const; + int activeLinkCount(uint32_t nodeId) const; QString connectionKey(const QtNodes::ConnectionId &connectionId) const; bool eventFilter(QObject *object, QEvent *event) override; @@ -52,6 +56,7 @@ private: QSet m_ignoreCreate; QSet m_ignoreDelete; + QSet m_ignoreLinkRemoved; QMap m_connectionToLinkId; QMap m_linkIdToConnection; AudioLevelMeter *m_meter = nullptr; @@ -62,4 +67,6 @@ private: QMap m_nodeMeters; QMap m_nodeMeterRows; QMap m_nodeMeterLabels; + QMap m_nodeLinkCounts; + QMap m_linksById; }; diff --git a/src/pipewire/pipewirecontroller.cpp b/src/pipewire/pipewirecontroller.cpp index a1078b8..58c2079 100644 --- a/src/pipewire/pipewirecontroller.cpp +++ b/src/pipewire/pipewirecontroller.cpp @@ -472,14 +472,19 @@ void PipeWireController::ensureNodeMeter(uint32_t nodeId, const QString &targetN void PipeWireController::removeNodeMeter(uint32_t nodeId) { - QMutexLocker lock(&m_meterMutex); - if (!m_nodeMeters.contains(nodeId)) { - return; + NodeMeter *meter = nullptr; + { + QMutexLocker lock(&m_meterMutex); + if (!m_nodeMeters.contains(nodeId)) { + return; + } + meter = m_nodeMeters.take(nodeId); } - NodeMeter *meter = m_nodeMeters.take(nodeId); if (meter && meter->stream) { + lock(); pw_stream_destroy(meter->stream); + unlock(); } delete meter; } @@ -494,6 +499,19 @@ uint32_t PipeWireController::createLink(uint32_t outputNodeId, uint32_t outputPo qWarning() << "Cannot create link: not connected to PipeWire"; return 0; } + + { + 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) { + return link.id; + } + } + } lock(); @@ -591,15 +609,21 @@ bool PipeWireController::destroyLink(uint32_t linkId) unlock(); - { - QMutexLocker lock(&m_nodesMutex); - m_links.remove(linkId); + QElapsedTimer timer; + timer.start(); + while (timer.elapsed() < 2000) { + { + QMutexLocker lock(&m_nodesMutex); + if (!m_links.contains(linkId)) { + qInfo() << "Link destroyed:" << linkId; + return true; + } + } + QThread::msleep(10); } - - emit linkRemoved(linkId); - - qInfo() << "Link destroyed:" << linkId; - return true; + + qWarning() << "Link destroy requested but ID still present" << linkId; + return false; } QString PipeWireController::dumpGraph() const