AudioPanel: move audio stream logic to AudioService, filter 'quickshell' audio (#1645)

This commit is contained in:
Lysec
2026-02-01 12:41:14 +01:00
parent a13374ffc6
commit 131d0a0b53
3 changed files with 116 additions and 125 deletions
+1 -124
View File
@@ -146,130 +146,7 @@ SmartPanel {
// Find application streams that are actually playing audio (connected to default sink)
// Use linkGroups to find nodes connected to the default audio sink
// Note: We need to use link IDs since source/target properties require binding
readonly property var appStreams: {
if (!Pipewire.ready || !AudioService.sink) {
return [];
}
var defaultSink = AudioService.sink;
var defaultSinkId = defaultSink.id;
var connectedStreamIds = {};
var connectedStreams = [];
// Use PwNodeLinkTracker to get properly bound link groups
if (!sinkLinkTracker.linkGroups) {
return [];
}
// Check if linkGroups is an array or ObjectModel
var linkGroupsCount = 0;
if (sinkLinkTracker.linkGroups.length !== undefined) {
linkGroupsCount = sinkLinkTracker.linkGroups.length;
} else if (sinkLinkTracker.linkGroups.count !== undefined) {
linkGroupsCount = sinkLinkTracker.linkGroups.count;
} else {
return [];
}
if (linkGroupsCount === 0) {
return [];
}
// Collect intermediate node IDs that are connected to the sink
var intermediateNodeIds = {};
// Process link groups from sinkLinkTracker
var nodesToCheck = [];
for (var i = 0; i < linkGroupsCount; i++) {
var linkGroup;
if (sinkLinkTracker.linkGroups.get) {
linkGroup = sinkLinkTracker.linkGroups.get(i);
} else {
linkGroup = sinkLinkTracker.linkGroups[i];
}
if (!linkGroup || !linkGroup.source) {
continue;
}
var sourceNode = linkGroup.source;
// If it's a stream node, add it directly
if (sourceNode.isStream && sourceNode.audio) {
if (!connectedStreamIds[sourceNode.id]) {
connectedStreamIds[sourceNode.id] = true;
connectedStreams.push(sourceNode);
}
} else {
// Not a stream - this is an intermediate node, track it
intermediateNodeIds[sourceNode.id] = true;
nodesToCheck.push(sourceNode);
}
}
// If we found intermediate nodes, we need to find streams connected to them
// Since Pipewire.linkGroups is not directly accessible, we'll use a heuristic:
// When intermediate nodes are present, include all active stream nodes
// (reasonable assumption: if audio is playing, streams are connected)
if (nodesToCheck.length > 0 || connectedStreams.length === 0) {
try {
// Get all nodes from Pipewire
var allNodes = [];
if (Pipewire.nodes) {
if (Pipewire.nodes.count !== undefined) {
var nodeCount = Pipewire.nodes.count;
for (var n = 0; n < nodeCount; n++) {
var node;
if (Pipewire.nodes.get) {
node = Pipewire.nodes.get(n);
} else {
node = Pipewire.nodes[n];
}
if (node)
allNodes.push(node);
}
} else if (Pipewire.nodes.values) {
allNodes = Pipewire.nodes.values;
}
}
// Find all stream nodes
for (var j = 0; j < allNodes.length; j++) {
var node = allNodes[j];
if (!node || !node.isStream || !node.audio) {
continue;
}
var streamId = node.id;
if (connectedStreamIds[streamId]) {
continue; // Already added
}
// When intermediate nodes are present, include all stream nodes
// This is a reasonable heuristic since if audio is playing, they're likely connected
if (Object.keys(intermediateNodeIds).length > 0) {
connectedStreamIds[streamId] = true;
connectedStreams.push(node);
} else if (connectedStreams.length === 0) {
// Fallback: if no streams found yet, include as fallback
connectedStreamIds[streamId] = true;
connectedStreams.push(node);
}
}
} catch (e)
// Error finding stream nodes - continue with what we have
{}
}
return connectedStreams;
}
// Track links to the default sink using PwNodeLinkTracker (properly binds links)
PwNodeLinkTracker {
id: sinkLinkTracker
node: AudioService.sink
}
readonly property var appStreams: AudioService.appStreams
// Use implicitHeight from content + margins to avoid binding loops
property real contentPreferredHeight: mainColumn.implicitHeight + Style.marginL * 2
@@ -271,7 +271,6 @@ ColumnLayout {
Layout.topMargin: Style.marginS
}
ColumnLayout {
visible: Settings.data.bar.displayMode === "auto_hide"
spacing: Style.marginS
+115
View File
@@ -76,6 +76,13 @@ Singleton {
// Filtered device nodes (non-stream sinks and sources)
readonly property var deviceNodes: Pipewire.ready ? Pipewire.nodes.values.reduce((acc, node) => {
if (!node.isStream) {
// Filter out quickshell nodes (unlikely to be devices, but for consistency)
const name = node.name || "";
const mediaName = (node.properties && node.properties["media.name"]) || "";
if (name === "quickshell" || mediaName === "quickshell") {
return acc;
}
if (node.isSink) {
acc.sinks.push(node);
} else if (node.audio) {
@@ -122,6 +129,114 @@ Singleton {
objects: root.source ? [root.source] : []
}
// Track links to the default sink to find active streams
PwNodeLinkTracker {
id: sinkLinkTracker
node: root.sink
}
// Find application streams that are connected to the default sink
readonly property var appStreams: {
if (!Pipewire.ready || !root.sink) {
return [];
}
var connectedStreamIds = {};
var connectedStreams = [];
// Use PwNodeLinkTracker to get properly bound link groups
if (!sinkLinkTracker.linkGroups) {
return [];
}
var linkGroupsCount = 0;
if (sinkLinkTracker.linkGroups.length !== undefined) {
linkGroupsCount = sinkLinkTracker.linkGroups.length;
} else if (sinkLinkTracker.linkGroups.count !== undefined) {
linkGroupsCount = sinkLinkTracker.linkGroups.count;
} else {
return [];
}
if (linkGroupsCount === 0) {
return [];
}
var intermediateNodeIds = {};
var nodesToCheck = [];
for (var i = 0; i < linkGroupsCount; i++) {
var linkGroup;
if (sinkLinkTracker.linkGroups.get) {
linkGroup = sinkLinkTracker.linkGroups.get(i);
} else {
linkGroup = sinkLinkTracker.linkGroups[i];
}
if (!linkGroup || !linkGroup.source) {
continue;
}
var sourceNode = linkGroup.source;
// Filter out quickshell
const name = sourceNode.name || "";
const mediaName = (sourceNode.properties && sourceNode.properties["media.name"]) || "";
if (name === "quickshell" || mediaName === "quickshell") {
continue;
}
// If it's a stream node, add it directly
if (sourceNode.isStream && sourceNode.audio) {
if (!connectedStreamIds[sourceNode.id]) {
connectedStreamIds[sourceNode.id] = true;
connectedStreams.push(sourceNode);
}
} else {
// Not a stream - this is an intermediate node, track it
intermediateNodeIds[sourceNode.id] = true;
nodesToCheck.push(sourceNode);
}
}
// If we found intermediate nodes, we need to find streams connected to them
if (nodesToCheck.length > 0 || connectedStreams.length === 0) {
try {
var allNodes = Pipewire.nodes.values || [];
// Find all stream nodes
for (var j = 0; j < allNodes.length; j++) {
var node = allNodes[j];
if (!node || !node.isStream || !node.audio) {
continue;
}
// Filter out quickshell
const nodeName = node.name || "";
const nodeMediaName = (node.properties && node.properties["media.name"]) || "";
if (nodeName === "quickshell" || nodeMediaName === "quickshell") {
continue;
}
var streamId = node.id;
if (connectedStreamIds[streamId]) {
continue;
}
if (Object.keys(intermediateNodeIds).length > 0) {
connectedStreamIds[streamId] = true;
connectedStreams.push(node);
} else if (connectedStreams.length === 0) {
connectedStreamIds[streamId] = true;
connectedStreams.push(node);
}
}
} catch (e) {}
}
return connectedStreams;
}
// Bind all devices to ensure their properties are available
PwObjectTracker {
objects: [...root.sinks, ...root.sources]