feat(host): fan out desktop audio through owned links
This commit is contained in:
+63
-4
@@ -7,9 +7,14 @@
|
||||
//! mutate; matching, serial validation, and metadata writes stay ordered on the
|
||||
//! PipeWire main-loop thread.
|
||||
|
||||
use super::aec::AecConfig;
|
||||
use super::audio::parse_object_serial;
|
||||
use super::fanout::FanoutController;
|
||||
use super::health;
|
||||
use super::observer::Readiness;
|
||||
use super::observer::adapter::RegistryObserverHandle;
|
||||
use super::owned_thread::OwnedThread;
|
||||
use super::taint::snapshot::{GlobalId, Serial};
|
||||
use crate::repair::plan as repair_plan;
|
||||
use anyhow::{Context, Result, bail};
|
||||
use pipewire::proxy::ProxyT;
|
||||
@@ -38,6 +43,10 @@ const GRAPH_QUIESCE_BUDGET: Duration = Duration::from_secs(2);
|
||||
const PULSE_MONITOR_READY_BUDGET: Duration = Duration::from_secs(3);
|
||||
const PULSE_PROBE_BUDGET: Duration = Duration::from_millis(500);
|
||||
const PULSE_PROBE_INTERVAL: Duration = Duration::from_millis(20);
|
||||
/// The observer has its own sticky 2 s readiness deadline. This outer budget
|
||||
/// includes thread startup and lets us turn either a timeout or a missing
|
||||
/// capture-sink observation into a failed `DesktopExcluding` construction.
|
||||
const FANOUT_READY_BUDGET: Duration = Duration::from_secs(3);
|
||||
|
||||
const GRAPH_STARTING: u8 = 0;
|
||||
const GRAPH_RUNNING: u8 = 1;
|
||||
@@ -129,10 +138,13 @@ pub(super) struct AudioGraphOwner {
|
||||
/// This is the load-bearing type boundary for the phase-0d
|
||||
/// `DesktopExcluding` plan: owning this value proves the sink exists, but the
|
||||
/// only operations available are reading its monitor name and shutting down
|
||||
/// its actor connection. The link manager added in phase 6 will feed it through
|
||||
/// retained native PipeWire links; legacy `Routing` remains a separate type in
|
||||
/// `host::audio`.
|
||||
/// its graph connections. Phase 6 feeds it through a separate observer
|
||||
/// connection that retains non-lingering native PipeWire links; legacy
|
||||
/// `Routing` remains a separate type in `host::audio`.
|
||||
pub(super) struct BareCaptureSink {
|
||||
// Drop order is load-bearing: close the fan-out observer connection (and
|
||||
// therefore every non-lingering link) before the sink-owning connection.
|
||||
fanout: Option<RegistryObserverHandle>,
|
||||
graph_owner: Option<AudioGraphOwner>,
|
||||
monitor_name: String,
|
||||
}
|
||||
@@ -140,11 +152,25 @@ pub(super) struct BareCaptureSink {
|
||||
impl BareCaptureSink {
|
||||
pub(super) async fn start(health: health::Reporter) -> Result<Self> {
|
||||
let spec = CaptureSinkSpec::for_pid(std::process::id());
|
||||
let (graph_owner, _event_rx) = AudioGraphOwner::start(None, spec, health)
|
||||
let (graph_owner, _event_rx) = AudioGraphOwner::start(None, spec, health.clone())
|
||||
.await
|
||||
.context("failed to start the bare connection-owned capture sink")?;
|
||||
let identity = graph_owner.identity().clone();
|
||||
let fanout = RegistryObserverHandle::spawn_with_mutation_sink(
|
||||
Box::new(FanoutController::new(
|
||||
Serial(identity.serial),
|
||||
AecConfig::Off,
|
||||
)),
|
||||
health,
|
||||
)
|
||||
.context("failed to start the desktop-excluding fan-out observer")?;
|
||||
if let Err(error) = wait_for_fanout_ready(&fanout, &identity).await {
|
||||
drop(fanout);
|
||||
graph_owner.shutdown().await;
|
||||
return Err(error).context("desktop-excluding fan-out did not become ready");
|
||||
}
|
||||
Ok(Self {
|
||||
fanout: Some(fanout),
|
||||
graph_owner: Some(graph_owner),
|
||||
monitor_name: identity.monitor_name,
|
||||
})
|
||||
@@ -162,6 +188,11 @@ impl BareCaptureSink {
|
||||
}
|
||||
|
||||
pub(super) async fn shutdown(mut self) {
|
||||
// Joining the observer closes its PipeWire connection and drops every
|
||||
// retained non-lingering link before the capture sink can disappear.
|
||||
if let Some(fanout) = self.fanout.take() {
|
||||
drop(fanout);
|
||||
}
|
||||
if let Some(graph_owner) = self.graph_owner.take()
|
||||
&& !graph_owner.shutdown().await
|
||||
{
|
||||
@@ -170,6 +201,34 @@ impl BareCaptureSink {
|
||||
}
|
||||
}
|
||||
|
||||
async fn wait_for_fanout_ready(
|
||||
observer: &RegistryObserverHandle,
|
||||
identity: &SinkIdentity,
|
||||
) -> Result<()> {
|
||||
let deadline = Instant::now() + FANOUT_READY_BUDGET;
|
||||
loop {
|
||||
if let Some(projection) = observer.latest() {
|
||||
if projection.readiness == Readiness::TimedOut {
|
||||
bail!("registry observer reached its sticky readiness timeout");
|
||||
}
|
||||
let sink_is_current = projection
|
||||
.snapshot
|
||||
.node(Serial(identity.serial))
|
||||
.is_some_and(|node| node.id == GlobalId(identity.global_id));
|
||||
if projection.graph_ready && sink_is_current {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
if Instant::now() >= deadline {
|
||||
bail!(
|
||||
"capture sink serial {} was not present in a graph-ready projection within {FANOUT_READY_BUDGET:?}",
|
||||
identity.serial
|
||||
);
|
||||
}
|
||||
tokio::time::sleep(PULSE_PROBE_INTERVAL).await;
|
||||
}
|
||||
}
|
||||
|
||||
impl AudioGraphOwner {
|
||||
pub(super) async fn start(
|
||||
filter_name: Option<String>,
|
||||
|
||||
Reference in New Issue
Block a user