Fix node details

This commit is contained in:
Joey Yakimowich-Payne 2026-02-06 08:41:55 -07:00
commit 9ac56d0d0b
2 changed files with 146 additions and 20 deletions

View file

@ -881,6 +881,9 @@ void GraphEditorWidget::showNodeContextMenu(const QPoint &screenPos,
createRuleAction = menu.addAction(QStringLiteral("Create Rule...")); createRuleAction = menu.addAction(QStringLiteral("Create Rule..."));
} }
menu.addSeparator();
QAction *detailsAction = menu.addAction(QStringLiteral("Node Details"));
menu.addSeparator(); menu.addSeparator();
QAction *pasteAction = menu.addAction(QStringLiteral("Paste")); QAction *pasteAction = menu.addAction(QStringLiteral("Paste"));
pasteAction->setShortcut(QKeySequence::Paste); pasteAction->setShortcut(QKeySequence::Paste);
@ -900,6 +903,10 @@ void GraphEditorWidget::showNodeContextMenu(const QPoint &screenPos,
duplicateSelection(); duplicateSelection();
} else if (chosen == deleteAction && m_client) { } else if (chosen == deleteAction && m_client) {
deleteSelection(); deleteSelection();
} else if (chosen == detailsAction) {
m_selectedNodeId = qtNodeId;
updateNodeDetailsPanel(qtNodeId);
m_sidebar->setCurrentWidget(m_nodeDetailsScroll);
} else if (chosen == pasteAction) { } else if (chosen == pasteAction) {
pasteSelection(QPointF(0, 0)); pasteSelection(QPointF(0, 0));
} else if (chosen == createRuleAction) { } else if (chosen == createRuleAction) {
@ -1936,12 +1943,10 @@ void GraphEditorWidget::onSelectionChanged() {
if (selected == m_selectedNodeId) if (selected == m_selectedNodeId)
return; return;
if (selected != 0) { m_selectedNodeId = selected;
m_selectedNodeId = selected; if (selected != 0 && m_sidebar->currentWidget() == m_nodeDetailsScroll) {
updateNodeDetailsPanel(selected); updateNodeDetailsPanel(selected);
m_sidebar->setCurrentWidget(m_nodeDetailsScroll); } else if (selected == 0 && m_sidebar->currentWidget() == m_nodeDetailsScroll) {
} else {
m_selectedNodeId = 0;
clearNodeDetailsPanel(); clearNodeDetailsPanel();
} }
} }
@ -2099,6 +2104,9 @@ void GraphEditorWidget::updateNodeDetailsPanel(QtNodes::NodeId nodeId) {
} }
layout->addWidget(targetCombo); layout->addWidget(targetCombo);
connect(loopbackCheck, &QCheckBox::toggled, targetCombo,
&QWidget::setEnabled);
if (vnResult.ok() && vnResult.value.loopback) { if (vnResult.ok() && vnResult.value.loopback) {
loopbackCheck->setChecked(true); loopbackCheck->setChecked(true);
int idx = targetCombo->findData( int idx = targetCombo->findData(
@ -2106,9 +2114,6 @@ void GraphEditorWidget::updateNodeDetailsPanel(QtNodes::NodeId nodeId) {
if (idx >= 0) if (idx >= 0)
targetCombo->setCurrentIndex(idx); targetCombo->setCurrentIndex(idx);
} }
connect(loopbackCheck, &QCheckBox::toggled, targetCombo,
&QWidget::setEnabled);
} }
layout->addSpacing(12); layout->addSpacing(12);

View file

@ -10,6 +10,7 @@
#include <unordered_set> #include <unordered_set>
#include <utility> #include <utility>
#include <pipewire/impl-module.h>
#include <pipewire/keys.h> #include <pipewire/keys.h>
#include <pipewire/link.h> #include <pipewire/link.h>
#include <pipewire/pipewire.h> #include <pipewire/pipewire.h>
@ -104,6 +105,7 @@ bool MatchesRule(const NodeInfo& node, const RuleMatch& match) {
struct StreamData { struct StreamData {
pw_stream* stream = nullptr; pw_stream* stream = nullptr;
pw_impl_module* module = nullptr;
spa_hook listener{}; spa_hook listener{};
pw_thread_loop* loop = nullptr; pw_thread_loop* loop = nullptr;
bool is_source = false; bool is_source = false;
@ -349,6 +351,8 @@ struct Client::Impl {
std::unordered_map<uint32_t, std::unique_ptr<NodeProxyData>> node_proxies; std::unordered_map<uint32_t, std::unique_ptr<NodeProxyData>> node_proxies;
std::unordered_map<uint32_t, uint32_t> node_channel_counts; std::unordered_map<uint32_t, uint32_t> node_channel_counts;
std::unordered_set<uint32_t> loopback_internal_nodes;
std::unordered_map<uint32_t, MeterState> meter_states; std::unordered_map<uint32_t, MeterState> meter_states;
std::unordered_set<uint32_t> metered_nodes; std::unordered_set<uint32_t> metered_nodes;
MeterState master_meter; MeterState master_meter;
@ -789,6 +793,7 @@ void Client::Impl::ClearCache() {
nodes.clear(); nodes.clear();
ports.clear(); ports.clear();
links.clear(); links.clear();
loopback_internal_nodes.clear();
pending_auto_links.clear(); pending_auto_links.clear();
auto_link_claimed_pairs.clear(); auto_link_claimed_pairs.clear();
policy_sync_pending = false; policy_sync_pending = false;
@ -873,6 +878,103 @@ Result<uint32_t> Client::Impl::CreateVirtualStreamLocked(std::string_view name,
} }
} }
if (options.behavior == VirtualBehavior::kLoopback && options.target_node) {
std::string args;
args += "{ node.description = \"" + display_name + "\"";
args += " node.name = \"" + stream_name + "\"";
args += " audio.channels = " + std::to_string(options.format.channels);
args += " audio.rate = " + std::to_string(options.format.rate);
args += " capture.props = {";
args += " target.object = \"" + *options.target_node + "\"";
args += " stream.capture.sink = true";
args += " node.passive = true";
args += " node.dont-reconnect = true";
args += " }";
args += " playback.props = {";
args += " node.name = \"" + stream_name + "\"";
args += " node.description = \"" + display_name + "\"";
args += " media.class = \"" + media_class_value + "\"";
args += " node.virtual = true";
args += " }";
args += " }";
pw_impl_module* mod = pw_context_load_module(context,
"libpipewire-module-loopback", args.c_str(), nullptr);
if (!mod) {
return {Status::Error(StatusCode::kUnavailable, "failed to load loopback module"), 0};
}
auto sync_status = SyncLocked();
if (!sync_status.ok()) {
pw_impl_module_destroy(mod);
return {sync_status, 0};
}
uint32_t node_id = SPA_ID_INVALID;
{
std::lock_guard<std::mutex> lock(cache_mutex);
for (const auto& entry : nodes) {
if (entry.second.name == stream_name) {
node_id = entry.first;
break;
}
}
}
if (node_id == SPA_ID_INVALID) {
int wait_attempts = 0;
while (node_id == SPA_ID_INVALID && wait_attempts < 3) {
int wait_res = pw_thread_loop_timed_wait(thread_loop, kSyncWaitSeconds);
if (wait_res == -ETIMEDOUT) {
break;
}
auto sync2 = SyncLocked();
(void)sync2;
std::lock_guard<std::mutex> lock(cache_mutex);
for (const auto& entry : nodes) {
if (entry.second.name == stream_name) {
node_id = entry.first;
break;
}
}
++wait_attempts;
}
}
if (node_id == SPA_ID_INVALID) {
pw_impl_module_destroy(mod);
return {Status::Error(StatusCode::kTimeout, "loopback node did not appear in registry"), 0};
}
std::string capture_name = "input." + stream_name;
{
std::lock_guard<std::mutex> lock(cache_mutex);
for (const auto& entry : nodes) {
if (entry.second.name == capture_name) {
loopback_internal_nodes.insert(entry.first);
break;
}
}
}
auto stream_data = std::make_unique<StreamData>();
stream_data->module = mod;
stream_data->loop = thread_loop;
stream_data->is_source = is_source;
stream_data->loopback = true;
stream_data->target_node = *options.target_node;
stream_data->name = stream_name;
stream_data->rate = options.format.rate;
stream_data->channels = options.format.channels;
stream_data->node_id = node_id;
stream_data->ready = true;
{
std::lock_guard<std::mutex> lock(cache_mutex);
virtual_streams.emplace(node_id, std::move(stream_data));
}
return {Status::Ok(), node_id};
}
pw_properties* props = pw_properties_new(PW_KEY_MEDIA_TYPE, "Audio", pw_properties* props = pw_properties_new(PW_KEY_MEDIA_TYPE, "Audio",
PW_KEY_MEDIA_CATEGORY, media_category, PW_KEY_MEDIA_CATEGORY, media_category,
PW_KEY_MEDIA_ROLE, "Music", PW_KEY_MEDIA_ROLE, "Music",
@ -888,9 +990,6 @@ Result<uint32_t> Client::Impl::CreateVirtualStreamLocked(std::string_view name,
if (node_group) { if (node_group) {
pw_properties_set(props, PW_KEY_NODE_GROUP, node_group); pw_properties_set(props, PW_KEY_NODE_GROUP, node_group);
} }
if (options.behavior == VirtualBehavior::kLoopback && options.target_node) {
pw_properties_set(props, PW_KEY_TARGET_OBJECT, options.target_node->c_str());
}
pw_stream* stream = pw_stream_new(core, stream_name.c_str(), props); pw_stream* stream = pw_stream_new(core, stream_name.c_str(), props);
if (!stream) { if (!stream) {
@ -901,7 +1000,7 @@ Result<uint32_t> Client::Impl::CreateVirtualStreamLocked(std::string_view name,
stream_data->stream = stream; stream_data->stream = stream;
stream_data->loop = thread_loop; stream_data->loop = thread_loop;
stream_data->is_source = is_source; stream_data->is_source = is_source;
stream_data->loopback = options.behavior == VirtualBehavior::kLoopback; stream_data->loopback = false;
if (options.target_node) { if (options.target_node) {
stream_data->target_node = *options.target_node; stream_data->target_node = *options.target_node;
} }
@ -926,9 +1025,6 @@ Result<uint32_t> Client::Impl::CreateVirtualStreamLocked(std::string_view name,
enum pw_direction direction = is_source ? PW_DIRECTION_OUTPUT : PW_DIRECTION_INPUT; enum pw_direction direction = is_source ? PW_DIRECTION_OUTPUT : PW_DIRECTION_INPUT;
enum pw_stream_flags flags = PW_STREAM_FLAG_MAP_BUFFERS; enum pw_stream_flags flags = PW_STREAM_FLAG_MAP_BUFFERS;
if (options.behavior == VirtualBehavior::kLoopback && options.target_node) {
flags = static_cast<pw_stream_flags>(flags | PW_STREAM_FLAG_AUTOCONNECT);
}
int res = pw_stream_connect(stream, direction, PW_ID_ANY, flags, params, 1); int res = pw_stream_connect(stream, direction, PW_ID_ANY, flags, params, 1);
if (res < 0) { if (res < 0) {
pw_stream_destroy(stream); pw_stream_destroy(stream);
@ -1042,7 +1138,11 @@ void Client::Impl::DisconnectLocked() {
} }
for (auto& entry : streams) { for (auto& entry : streams) {
StreamData* stream_data = entry.second.get(); StreamData* stream_data = entry.second.get();
if (stream_data && stream_data->stream) { if (!stream_data) continue;
if (stream_data->module) {
pw_impl_module_destroy(stream_data->module);
stream_data->module = nullptr;
} else if (stream_data->stream) {
pw_stream_disconnect(stream_data->stream); pw_stream_disconnect(stream_data->stream);
pw_stream_destroy(stream_data->stream); pw_stream_destroy(stream_data->stream);
stream_data->stream = nullptr; stream_data->stream = nullptr;
@ -1827,6 +1927,9 @@ Result<std::vector<NodeInfo>> Client::ListNodes() {
std::vector<NodeInfo> items; std::vector<NodeInfo> items;
items.reserve(impl_->nodes.size()); items.reserve(impl_->nodes.size());
for (const auto& entry : impl_->nodes) { for (const auto& entry : impl_->nodes) {
if (impl_->loopback_internal_nodes.count(entry.first)) {
continue;
}
NodeInfo info = entry.second; NodeInfo info = entry.second;
if (!info.is_virtual && if (!info.is_virtual &&
impl_->virtual_streams.find(entry.first) != impl_->virtual_streams.find(entry.first) !=
@ -1933,10 +2036,28 @@ Status Client::RemoveNode(NodeId node) {
owned_stream = std::move(it->second); owned_stream = std::move(it->second);
impl_->virtual_streams.erase(it); impl_->virtual_streams.erase(it);
} }
if (owned_stream && owned_stream->stream) { if (owned_stream) {
pw_stream_disconnect(owned_stream->stream); if (owned_stream->module) {
pw_stream_destroy(owned_stream->stream); std::string capture_name = "input." + owned_stream->name;
owned_stream->stream = nullptr; {
std::lock_guard<std::mutex> lock(impl_->cache_mutex);
for (auto it = impl_->loopback_internal_nodes.begin();
it != impl_->loopback_internal_nodes.end(); ++it) {
auto node_it = impl_->nodes.find(*it);
if (node_it != impl_->nodes.end() &&
node_it->second.name == capture_name) {
impl_->loopback_internal_nodes.erase(it);
break;
}
}
}
pw_impl_module_destroy(owned_stream->module);
owned_stream->module = nullptr;
} else if (owned_stream->stream) {
pw_stream_disconnect(owned_stream->stream);
pw_stream_destroy(owned_stream->stream);
owned_stream->stream = nullptr;
}
} }
pw_thread_loop_unlock(impl_->thread_loop); pw_thread_loop_unlock(impl_->thread_loop);
impl_->AutoSave(); impl_->AutoSave();