feat(host): build desktop audio exclusion foundation
This commit is contained in:
+40
-1
@@ -34,7 +34,12 @@
|
||||
//! *is* a real Link whose output node is the sink node itself (measured).
|
||||
//! A port-granular walk would need a synthetic edge; a node-granular one
|
||||
//! does not.
|
||||
//! 3. **Owner bridges** — the intra-process hop the graph cannot see. See
|
||||
//! 3. **Hardware-device bridges** — a passive sink can feed a passive source
|
||||
//! on the same physical Device through a mixer/loopback path that PipeWire
|
||||
//! does not expose as a Link. The observer positively classifies both
|
||||
//! terminals and retains their shared `device.id`; the walk conservatively
|
||||
//! adds `sink → source` for that one Device.
|
||||
//! 4. **Owner bridges** — the intra-process hop the graph cannot see. See
|
||||
//! [`owner`]; this is the hard one.
|
||||
//!
|
||||
//! ## Stickiness
|
||||
@@ -784,6 +789,40 @@ fn downstream_edges(snapshot: &GraphSnapshot, unresolved_input: &mut BTreeSet<Se
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// A physical sound device may route playback back into capture in its
|
||||
// own mixer/firmware without publishing a PipeWire Link (HDA "Stereo
|
||||
// Mix", USB loopback channels, vendor DSPs). Control-name inspection is
|
||||
// neither portable nor proof of absence, so v1 fails closed: once a
|
||||
// passive hardware sink is tainted, passive capture terminals exported
|
||||
// by the same Device are downstream too.
|
||||
//
|
||||
// Both guards are load-bearing. `session_device` limits this to the
|
||||
// observer's positive hardware-terminal allowlist, so an app-associated
|
||||
// filter cannot invent a bridge. `device_id` limits it to one physical
|
||||
// Device, so the shared WirePlumber client does not fuse every card.
|
||||
let hardware_outputs: Vec<(Serial, snapshot::GlobalId)> = snapshot
|
||||
.nodes()
|
||||
.filter(|node| {
|
||||
node.props.session_device && matches!(node.role, MediaRole::Sink | MediaRole::Duplex)
|
||||
})
|
||||
.filter_map(|node| node.props.device_id.map(|id| (node.serial, id)))
|
||||
.collect();
|
||||
let hardware_inputs: Vec<(Serial, snapshot::GlobalId)> = snapshot
|
||||
.nodes()
|
||||
.filter(|node| {
|
||||
node.props.session_device && matches!(node.role, MediaRole::Source | MediaRole::Duplex)
|
||||
})
|
||||
.filter_map(|node| node.props.device_id.map(|id| (node.serial, id)))
|
||||
.collect();
|
||||
for (from, output_device) in hardware_outputs {
|
||||
for &(to, input_device) in &hardware_inputs {
|
||||
if from != to && output_device == input_device {
|
||||
edges.entry(from).or_default().push(to);
|
||||
receivers.insert(to);
|
||||
}
|
||||
}
|
||||
}
|
||||
for targets in edges.values_mut() {
|
||||
targets.sort_unstable();
|
||||
targets.dedup();
|
||||
|
||||
Reference in New Issue
Block a user