fix(host): close unsupported fanout gates

This commit is contained in:
2026-08-21 19:45:18 -04:00
parent 6be07ef706
commit 956534f63c
7 changed files with 427 additions and 20 deletions
+36 -8
View File
@@ -201,9 +201,15 @@ pub enum Reason {
/// A `port.exclusive` port — fan-out will be refused (v3.4 §6.2). Local
/// to the node; does not propagate.
PortExclusive,
/// An encoded/passthrough stream — a second link would corrupt it.
/// No usable configured Format param has arrived. Fan-out cannot prove a
/// second link is safe. Local to the node; does not propagate.
FormatUnknown,
/// An encoded stream — a second raw-audio link would refuse or corrupt.
/// Local to the node; does not propagate.
Passthrough,
Encoded,
/// An IEC958/S/PDIF passthrough stream. Local to the node; does not
/// propagate.
Iec958Passthrough,
}
impl Reason {
@@ -219,10 +225,25 @@ impl Reason {
Self::UnresolvedOwner => "unresolved-owner",
Self::GraphNotReady => "graph-not-ready",
Self::PortExclusive => "port-exclusive",
Self::Passthrough => "passthrough",
Self::FormatUnknown => "format-unknown",
Self::Encoded => "encoded",
Self::Iec958Passthrough => "iec958-passthrough",
}
}
/// A clean, otherwise-eligible stream whose known format/port shape this
/// fan-out mode cannot link safely. These reasons are user-visible
/// `stream_unsupported` statuses; taint roots and observation gating are
/// intentional exclusions, not failures. `FormatUnknown` is deliberately
/// omitted because the initial Node-info callback can precede the Format
/// reply; reporting that transient would produce a false warning.
pub(super) fn reports_stream_unsupported(self) -> bool {
matches!(
self,
Self::PortExclusive | Self::Encoded | Self::Iec958Passthrough
)
}
/// Lower wins. A node can acquire taint several ways in one recompute
/// and the reported reason must not depend on traversal order, or the
/// audit output is unstable and the fixture tests are flaky. Explicit
@@ -241,7 +262,9 @@ impl Reason {
// because it is only consulted for untainted candidates.
Self::GraphNotReady => 8,
Self::PortExclusive => 9,
Self::Passthrough => 10,
Self::FormatUnknown => 10,
Self::Encoded => 11,
Self::Iec958Passthrough => 12,
}
}
@@ -1057,13 +1080,18 @@ fn build_decisions(
/// clean. These do not propagate — an exclusive-port stream is unlinkable,
/// not hazardous.
fn local_exclusion(snapshot: &GraphSnapshot, node: &NodeSnapshot) -> Option<Reason> {
if node.props.passthrough {
return Some(Reason::Passthrough);
}
if snapshot.ports_of(node.id).any(|port| port.exclusive) {
return Some(Reason::PortExclusive);
}
None
if node.props.passthrough {
return Some(Reason::Iec958Passthrough);
}
match node.props.stream_format {
snapshot::StreamFormat::Raw => None,
snapshot::StreamFormat::Unknown => Some(Reason::FormatUnknown),
snapshot::StreamFormat::Encoded => Some(Reason::Encoded),
snapshot::StreamFormat::Iec958 => Some(Reason::Iec958Passthrough),
}
}
/// Sticky bookkeeping for the next recompute: every tainted owner, with
+25 -2
View File
@@ -52,6 +52,25 @@ pub enum MediaRole {
Other,
}
/// The configured format of an application playback stream.
///
/// The production observer reads this from the Node's `SPA_PARAM_Format`.
/// Fixtures default to [`Self::Raw`] because almost every graph fixture models
/// ordinary PCM playback; the observer explicitly uses [`Self::Unknown`]
/// until PipeWire supplies a format. Unknown is fail-closed at eligibility.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum StreamFormat {
/// Ordinary PCM audio, safe to fan out subject to the other predicates.
#[default]
Raw,
/// No usable configured format has been observed yet.
Unknown,
/// Encoded audio other than IEC958 passthrough.
Encoded,
/// IEC958/S/PDIF passthrough.
Iec958,
}
impl MediaRole {
pub fn parse(media_class: Option<&str>) -> Self {
match media_class {
@@ -136,9 +155,13 @@ pub struct NodeProps {
/// module-created streams this is pipewire-pulse's own PID, which is
/// why [`super::ExclusionCtx::pipewire_pulse_pid`] exists.
pub process_id: Option<u32>,
/// The stream negotiated an encoded/passthrough format; a second link
/// would refuse or corrupt it (v3.4 §6.2).
/// `node.passthrough`; an explicit producer/session-manager refusal flag.
/// Kept separate from [`Self::stream_format`] so the two Phase-6 refusal
/// predicates cannot accidentally mask one another.
pub passthrough: bool,
/// The Node's configured `SPA_PARAM_Format`, classified at the observer
/// boundary. Encoded and IEC958 streams are distinct refusal predicates.
pub stream_format: StreamFormat,
/// `device.id` — the snapshot-local PipeWire Device this node belongs
/// to. This is retained separately from [`Self::session_device`]: the
/// latter says the node is a positively-classified passive hardware
+1 -1
View File
@@ -599,7 +599,7 @@ fn port_exclusive_and_passthrough_are_local_exclusions() {
&[("ok", ok)],
&[
("exclusive", exclusive, "port-exclusive"),
("passthrough", passthrough, "passthrough"),
("passthrough", passthrough, "iec958-passthrough"),
],
);
// Neither is hazardous — an unlinkable stream must not taint anything.