feat(host): emit desktop audio exclusion status events
This commit is contained in:
@@ -17,6 +17,14 @@ use serde::Serialize;
|
||||
|
||||
static JSON_ENABLED: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
/// First wire version for the desktop-audio-exclusion status family.
|
||||
///
|
||||
/// Existing event tags predate explicit versioning. These events are consumed
|
||||
/// across the PixelPass/PeerSpeak process boundary and are landing before the
|
||||
/// PeerSpeak parser, so their version is carried on every record rather than
|
||||
/// inferred from either binary's package version.
|
||||
pub(crate) const AUDIO_EXCLUSION_EVENT_VERSION: u8 = 1;
|
||||
|
||||
/// Turn JSON event output on. Called once at startup from `--output json`.
|
||||
pub fn set_json(enabled: bool) {
|
||||
JSON_ENABLED.store(enabled, Ordering::Relaxed);
|
||||
@@ -63,6 +71,21 @@ pub enum Event<'a> {
|
||||
/// stream went away. Under `--strict-audio`, `lost` means viewers currently
|
||||
/// hear silence; without it, viewers fall back to whole-desktop audio.
|
||||
AppAudio { state: AppAudioState },
|
||||
/// One otherwise-eligible playback stream could not be linked safely.
|
||||
StreamUnsupported {
|
||||
version: u8,
|
||||
stream_serial: u64,
|
||||
reason: &'a str,
|
||||
},
|
||||
/// The configured AEC identity never appeared before its validation
|
||||
/// deadline. Fan-out remains fail-closed.
|
||||
AecFailed { version: u8, module_index: u64 },
|
||||
/// A previously validated AEC identity disappeared. Every owned fan-out
|
||||
/// link is revoked before this transition is reported.
|
||||
AecRevoked { version: u8, module_index: u64 },
|
||||
/// An echo-cancel group other than the configured PeerSpeak instance is
|
||||
/// present and excluded from fan-out.
|
||||
ForeignAecWarning { version: u8, link_group: &'a str },
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -79,6 +102,58 @@ pub enum AppAudioState {
|
||||
Lost,
|
||||
}
|
||||
|
||||
/// Owned form of the audio-exclusion status family. The PipeWire observer can
|
||||
/// enqueue this through an unbounded sender without borrowing its snapshot;
|
||||
/// a Tokio-side forwarder then converts it to the public JSON [`Event`].
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum AudioExclusionEvent {
|
||||
StreamUnsupported {
|
||||
stream_serial: u64,
|
||||
reason: &'static str,
|
||||
},
|
||||
AecFailed {
|
||||
module_index: u64,
|
||||
},
|
||||
AecRevoked {
|
||||
module_index: u64,
|
||||
},
|
||||
ForeignAecWarning {
|
||||
link_group: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl AudioExclusionEvent {
|
||||
fn as_event(&self) -> Event<'_> {
|
||||
match self {
|
||||
Self::StreamUnsupported {
|
||||
stream_serial,
|
||||
reason,
|
||||
} => Event::StreamUnsupported {
|
||||
version: AUDIO_EXCLUSION_EVENT_VERSION,
|
||||
stream_serial: *stream_serial,
|
||||
reason,
|
||||
},
|
||||
Self::AecFailed { module_index } => Event::AecFailed {
|
||||
version: AUDIO_EXCLUSION_EVENT_VERSION,
|
||||
module_index: *module_index,
|
||||
},
|
||||
Self::AecRevoked { module_index } => Event::AecRevoked {
|
||||
version: AUDIO_EXCLUSION_EVENT_VERSION,
|
||||
module_index: *module_index,
|
||||
},
|
||||
Self::ForeignAecWarning { link_group } => Event::ForeignAecWarning {
|
||||
version: AUDIO_EXCLUSION_EVENT_VERSION,
|
||||
link_group,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Emit this owned status record through the stable stdout protocol.
|
||||
pub(crate) fn emit(&self) {
|
||||
emit(self.as_event());
|
||||
}
|
||||
}
|
||||
|
||||
/// Emit one event as a JSON line on stdout, flushed. No-op unless JSON
|
||||
/// output was enabled with [`set_json`], so call sites can sprinkle these
|
||||
/// unconditionally without branching.
|
||||
@@ -118,4 +193,56 @@ mod tests {
|
||||
.unwrap();
|
||||
assert_eq!(lost, r#"{"event":"app_audio","state":"lost"}"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn audio_exclusion_event_wire_shapes_are_versioned_and_exact() {
|
||||
let unsupported = serde_json::to_string(
|
||||
&AudioExclusionEvent::StreamUnsupported {
|
||||
stream_serial: 4_294_967_297,
|
||||
reason: "link-creation-failed",
|
||||
}
|
||||
.as_event(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
unsupported,
|
||||
r#"{"event":"stream_unsupported","version":1,"stream_serial":4294967297,"reason":"link-creation-failed"}"#
|
||||
);
|
||||
|
||||
let failed = serde_json::to_string(
|
||||
&AudioExclusionEvent::AecFailed {
|
||||
module_index: 536_870_919,
|
||||
}
|
||||
.as_event(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
failed,
|
||||
r#"{"event":"aec_failed","version":1,"module_index":536870919}"#
|
||||
);
|
||||
|
||||
let revoked = serde_json::to_string(
|
||||
&AudioExclusionEvent::AecRevoked {
|
||||
module_index: 536_870_919,
|
||||
}
|
||||
.as_event(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
revoked,
|
||||
r#"{"event":"aec_revoked","version":1,"module_index":536870919}"#
|
||||
);
|
||||
|
||||
let warning = serde_json::to_string(
|
||||
&AudioExclusionEvent::ForeignAecWarning {
|
||||
link_group: "echo-cancel-9999-13".to_string(),
|
||||
}
|
||||
.as_event(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
warning,
|
||||
r#"{"event":"foreign_aec_warning","version":1,"link_group":"echo-cancel-9999-13"}"#
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user