feat(host): recover desktop audio fanout after sink replacement
This commit is contained in:
@@ -133,6 +133,19 @@ pub(super) trait MutationProjectionSink: Send {
|
||||
now_us: u64,
|
||||
links: &mut dyn LinkMutation,
|
||||
);
|
||||
|
||||
/// Retarget a running controller to the exact serial of a recreated
|
||||
/// connection-owned capture sink, then reconcile immediately against the
|
||||
/// observer's current coherent projection. The adapter invokes this only
|
||||
/// on its PipeWire loop thread, keeping old-link revocation and new-link
|
||||
/// creation ordered with ordinary registry observations.
|
||||
fn replace_capture_sink(
|
||||
&mut self,
|
||||
capture_sink: Serial,
|
||||
projection: &Projection,
|
||||
now_us: u64,
|
||||
links: &mut dyn LinkMutation,
|
||||
);
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
@@ -339,6 +352,24 @@ impl MutationProjectionSink for FanoutController {
|
||||
self.emit_status(event);
|
||||
}
|
||||
}
|
||||
|
||||
fn replace_capture_sink(
|
||||
&mut self,
|
||||
capture_sink: Serial,
|
||||
projection: &Projection,
|
||||
now_us: u64,
|
||||
links: &mut dyn LinkMutation,
|
||||
) {
|
||||
if self.capture_sink == capture_sink {
|
||||
return;
|
||||
}
|
||||
self.capture_sink = capture_sink;
|
||||
// This is an explicit owner control message rather than a model event,
|
||||
// so it must not be coalesced behind the next registry callback. The
|
||||
// normal projection path is still the single place that evaluates,
|
||||
// plans, revokes stale proxies, and determines all-links-ACTIVE.
|
||||
self.on_projection(projection, EventKind::Graph, now_us, links);
|
||||
}
|
||||
}
|
||||
|
||||
/// Plan every candidate in the decision universe against one capture sink.
|
||||
@@ -712,6 +743,36 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replacing_the_capture_sink_drops_stale_links_and_recaptures() {
|
||||
let (mut graph, app, old_sink, _) = stereo_graph();
|
||||
let new_sink = graph.native_virtual_node("pixelpass_capture_new", MediaRole::Sink, 46);
|
||||
let new_fl = graph.port_on_channel(new_sink, PortDirection::In, false, Some("FL"));
|
||||
let new_fr = graph.port_on_channel(new_sink, PortDirection::In, false, Some("FR"));
|
||||
let projection = projection(&graph);
|
||||
let mut links = FakeLinks::default();
|
||||
let mut controller = FanoutController::new(old_sink, AecConfig::Off);
|
||||
|
||||
controller.on_projection(&projection, EventKind::Graph, 1_000, &mut links);
|
||||
assert_eq!(links.creates, 2);
|
||||
assert_eq!(controller.states()[&app], StreamCaptureState::Captured);
|
||||
|
||||
controller.replace_capture_sink(new_sink.serial, &projection, 2_000, &mut links);
|
||||
|
||||
assert_eq!(links.drops, 2, "every old-sink proxy must be dropped");
|
||||
assert_eq!(links.creates, 4, "both replacement links must be created");
|
||||
assert_eq!(controller.states()[&app], StreamCaptureState::Captured);
|
||||
assert_eq!(
|
||||
links
|
||||
.held
|
||||
.iter()
|
||||
.map(|link| link.input.port_id)
|
||||
.collect::<BTreeSet<_>>(),
|
||||
BTreeSet::from([new_fl, new_fr]),
|
||||
"only replacement-sink ports may remain held"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serial_revalidation_rejects_a_recycled_node_id_before_mutation() {
|
||||
let (mut graph, app, sink, _) = stereo_graph();
|
||||
|
||||
Reference in New Issue
Block a user