From 8c728dcd472ae30d8492c0ef4308d480d4cca985 Mon Sep 17 00:00:00 2001 From: joren Date: Mon, 30 Mar 2026 00:33:41 +0200 Subject: [PATCH] feat: node property inspector - Right-click node -> Properties shows details dialog - Shows: ID, name, class, volume, ports, sample rate, channels, format, quantum, rate, device, bus, API, priority - Backend parses extra PipeWire node props (clock.rate, channels, etc.) - Node type includes all new fields - stop() already had the SSE cleanup fix --- frontend/src/components/GraphCanvas.svelte | 42 ++++++++++++++++++++++ frontend/src/lib/types.ts | 9 +++++ src/graph_engine.cpp | 30 ++++++++++++++++ src/graph_types.h | 15 +++++++- src/web_server.cpp | 9 +++++ 5 files changed, 104 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/GraphCanvas.svelte b/frontend/src/components/GraphCanvas.svelte index 65f0418..dcb9000 100644 --- a/frontend/src/components/GraphCanvas.svelte +++ b/frontend/src/components/GraphCanvas.svelte @@ -27,6 +27,7 @@ let selectedEdge = $state(null); let contextMenu = $state<{ x: number; y: number; linkId: number; outputPortId: number; inputPortId: number; pinned: boolean } | null>(null); let nodeContextMenu = $state<{ x: number; y: number; nodeId: number; nodeName: string } | null>(null); + let showPropsDialog = $state(null); // node ID or null // Filters let showAudio = $state(true); @@ -658,6 +659,7 @@ {#if nodeContextMenu} {/if} + + {#if showPropsDialog !== null} + {@const nd = $nodes.find(n => n.id === showPropsDialog)} + {#if nd} +
+
+ Properties: {nd.nick || nd.name} + +
+
+ + + + + {#if nd.nick}{/if} + {#if nd.media_name}{/if} + + + + {#if nd.sample_rate > 0}{/if} + {#if nd.channels > 0}{/if} + {#if nd.format}{/if} + {#if nd.quantum > 0}{/if} + {#if nd.rate > 0}{/if} + {#if nd.device_name}{/if} + {#if nd.device_bus}{/if} + {#if nd.api}{/if} + {#if nd.priority > 0}{/if} + +
ID{nd.id}
Name{nd.name}
Nick{nd.nick}
Media Name{nd.media_name}
Class{nd.mode} / {nd.node_type}
Volume{Math.round(nd.volume * 100)}% {nd.mute ? '(muted)' : ''}
Ports{nd.port_ids.length}
Sample Rate{nd.sample_rate} Hz
Channels{nd.channels}
Format{nd.format}
Quantum{nd.quantum}
Rate{nd.rate}
Device{nd.device_name}
Bus{nd.device_bus}
API{nd.api}
Priority{nd.priority}
+
+
+ {/if} + {/if} + {#if showHideDialog}
@@ -897,6 +934,11 @@ .empty { font-size: 11px; color: #555; padding: 8px 0; text-align: center; } + .props-table { width: 100%; border-collapse: collapse; font-size: 11px; } + .props-table td { padding: 2px 6px; border-bottom: 1px solid #2a2a3e; } + .props-table td:first-child { color: #888; white-space: nowrap; width: 100px; } + .props-table td:last-child { color: #ccc; word-break: break-all; } + .port-circle { cursor: crosshair; } .port-circle:hover { filter: brightness(1.5); } .edge-path { cursor: pointer; pointer-events: stroke; } diff --git a/frontend/src/lib/types.ts b/frontend/src/lib/types.ts index c9d337a..df832a5 100644 --- a/frontend/src/lib/types.ts +++ b/frontend/src/lib/types.ts @@ -18,6 +18,15 @@ export interface Node { node_type: string; volume: number; mute: boolean; + sample_rate: number; + channels: number; + quantum: number; + rate: number; + format: string; + device_name: string; + device_bus: string; + api: string; + priority: number; port_ids: number[]; } diff --git a/src/graph_engine.cpp b/src/graph_engine.cpp index 409c7be..d08190c 100644 --- a/src/graph_engine.cpp +++ b/src/graph_engine.cpp @@ -97,6 +97,36 @@ static void on_node_info(void *data, const struct pw_node_info *info) { if (mute_str) { nobj->node.mute = pw_properties_parse_bool(mute_str); } + + // Read additional properties + const char *str; + + str = spa_dict_lookup(info->props, "default.clock.rate"); + if (str) nobj->node.sample_rate = (uint32_t)atoi(str); + + str = spa_dict_lookup(info->props, "default.clock.quantum"); + if (str) nobj->node.quantum = (uint32_t)atoi(str); + + str = spa_dict_lookup(info->props, "audio.channels"); + if (str) nobj->node.channels = (uint32_t)atoi(str); + + str = spa_dict_lookup(info->props, "audio.format"); + if (str) nobj->node.format = str; + + str = spa_dict_lookup(info->props, "device.name"); + if (str) nobj->node.device_name = str; + + str = spa_dict_lookup(info->props, "device.bus"); + if (str) nobj->node.device_bus = str; + + str = spa_dict_lookup(info->props, "api.alsa.path"); + if (str) nobj->node.api = str; + + str = spa_dict_lookup(info->props, "priority.driver"); + if (str) nobj->node.priority = atoi(str); + + str = spa_dict_lookup(info->props, "node.rate"); + if (str) nobj->node.rate = (uint32_t)atoi(str); } } diff --git a/src/graph_types.h b/src/graph_types.h index cc27d25..5b085de 100644 --- a/src/graph_types.h +++ b/src/graph_types.h @@ -67,11 +67,24 @@ struct Node { float volume; // 0.0 - 1.0 (linear) bool mute; + // Properties + uint32_t sample_rate; + uint32_t channels; + std::string format; + uint32_t quantum; + uint32_t rate; + std::string device_name; + std::string device_bus; + std::string api; + int priority; + std::vector port_ids; // child port IDs Node() : id(0), mode(PortMode::None), node_type(NodeType::None), mode2(PortMode::None), ready(false), changed(false), - volume(1.0f), mute(false) {} + volume(1.0f), mute(false), + sample_rate(0), channels(0), quantum(0), rate(0), + priority(0) {} }; struct Link; diff --git a/src/web_server.cpp b/src/web_server.cpp index 85bf1c7..d00424d 100644 --- a/src/web_server.cpp +++ b/src/web_server.cpp @@ -111,6 +111,15 @@ std::string WebServer::buildGraphJson() const { << ",\"node_type\":\"" << nodeTypeStr(n.node_type) << "\"" << ",\"volume\":" << n.volume << ",\"mute\":" << (n.mute ? "true" : "false") + << ",\"sample_rate\":" << n.sample_rate + << ",\"channels\":" << n.channels + << ",\"quantum\":" << n.quantum + << ",\"rate\":" << n.rate + << ",\"format\":\"" << escapeJson(n.format) << "\"" + << ",\"device_name\":\"" << escapeJson(n.device_name) << "\"" + << ",\"device_bus\":\"" << escapeJson(n.device_bus) << "\"" + << ",\"api\":\"" << escapeJson(n.api) << "\"" + << ",\"priority\":" << n.priority << ",\"port_ids\":["; bool first_p = true; for (uint32_t pid : n.port_ids) {