feat(screenshare): pass --strict-audio + surface app_audio drop warning (A23 P2)
Consumes the new pixelpass --strict-audio mode + app_audio events (pixelpass
85fdebe) to close Codex's A23 P2: the per-app pick alone was best-effort —
pixelpass would fall back to whole-desktop audio before the app routed and again
if it stopped, both reintroducing the call echo.
- host_args appends --strict-audio alongside --app=<name>, so pixelpass never
mirrors the desktop for our share: viewers hear only the chosen app or silence.
- Parse the app_audio JSON event (routed/lost) into PixelpassEvent; spawn_host
takes an optional notices channel and the stdout drain forwards events on it.
- Core spawns a forwarder (only when an app is selected) mapping routed/lost to
UiEvent::ShareAudioActive(bool); the channel/task self-terminate on host EOF.
- App tracks share_audio_dropped and shows a transient warning under the "Sharing
your screen" badge when the chosen app's audio stops ("viewers hear silence
until it plays again"). Reset on start/stop/room-leave.
In-process mpsc only — no wire/GOSSIP_PROTO change. 435 lib tests (+2: app_audio
parse + the dropped-flag state machine), clippy --all-targets + release clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+75
-4
@@ -588,6 +588,11 @@ pub struct AppState {
|
||||
/// the picker (and re-confirming) during that startup window. Cleared on
|
||||
/// `ScreenShareStarted`, `ScreenShareStopped`, or any `Error`.
|
||||
share_starting: bool,
|
||||
/// While sharing a specific app's audio (A23 strict mode): `true` when that
|
||||
/// app's audio has stopped, so viewers currently hear silence. Drives a
|
||||
/// transient warning. Always `false` for whole-desktop shares (pixelpass
|
||||
/// emits no `app_audio` events then) and when not sharing.
|
||||
share_audio_dropped: bool,
|
||||
/// Whether the Chat drawer is open (drawer layout only).
|
||||
drawer_chat_open: bool,
|
||||
/// Raw mic level (normalized RMS, `0.0..=1.0`) for the settings meter.
|
||||
@@ -661,6 +666,7 @@ impl AppState {
|
||||
self.share_audio_apps.clear();
|
||||
self.share_audio_selection = None;
|
||||
self.share_starting = false;
|
||||
self.share_audio_dropped = false;
|
||||
}
|
||||
|
||||
fn custom_sound_path(&self, sound: Sound) -> &str {
|
||||
@@ -786,6 +792,7 @@ impl Default for AppState {
|
||||
share_audio_apps: Vec::new(),
|
||||
share_audio_selection: None,
|
||||
share_starting: false,
|
||||
share_audio_dropped: false,
|
||||
drawer_chat_open: false,
|
||||
mic_level: 0.0,
|
||||
mic_test_active: false,
|
||||
@@ -1375,6 +1382,7 @@ fn update(state: &mut AppState, message: AppMessage) -> Task<AppMessage> {
|
||||
UiEvent::ScreenShareStarted => {
|
||||
state.self_sharing = true;
|
||||
state.share_starting = false;
|
||||
state.share_audio_dropped = false;
|
||||
// Defensive: ensure no picker lingers across a successful start.
|
||||
state.share_picker_open = false;
|
||||
state.status_message = "Sharing your screen".to_string();
|
||||
@@ -1382,8 +1390,13 @@ fn update(state: &mut AppState, message: AppMessage) -> Task<AppMessage> {
|
||||
UiEvent::ScreenShareStopped => {
|
||||
state.self_sharing = false;
|
||||
state.share_starting = false;
|
||||
state.share_audio_dropped = false;
|
||||
state.status_message = "Screen share stopped".to_string();
|
||||
}
|
||||
UiEvent::ShareAudioActive(active) => {
|
||||
// Per-app audio routed/lost (only while sharing a chosen app).
|
||||
state.share_audio_dropped = !active;
|
||||
}
|
||||
UiEvent::IdentityStatus { node_id, persisted, error } => {
|
||||
state.self_node_id = Some(node_id);
|
||||
state.identity_persisted = persisted;
|
||||
@@ -4069,16 +4082,32 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
|
||||
};
|
||||
el
|
||||
},
|
||||
// Live "you're sharing" badge — only present while sharing.
|
||||
// Live "you're sharing" badge — only present while sharing. When
|
||||
// sharing a specific app whose audio has dropped (A23 strict
|
||||
// mode), a warning line is added: viewers hear silence, not the
|
||||
// call, until that app plays again.
|
||||
{
|
||||
let el: Element<'_, AppMessage> = if state.self_sharing {
|
||||
row![
|
||||
let badge = row![
|
||||
icon(IconKind::Live, 14.0, color_red),
|
||||
text("Sharing your screen").size(13).color(color_red),
|
||||
]
|
||||
.spacing(6)
|
||||
.align_y(iced::alignment::Vertical::Center)
|
||||
.into()
|
||||
.align_y(iced::alignment::Vertical::Center);
|
||||
if state.share_audio_dropped {
|
||||
column![
|
||||
badge,
|
||||
text(
|
||||
"⚠ Shared app's audio stopped — viewers hear silence until it plays again"
|
||||
)
|
||||
.size(11)
|
||||
.color(color_yellow),
|
||||
]
|
||||
.spacing(3)
|
||||
.into()
|
||||
} else {
|
||||
badge.into()
|
||||
}
|
||||
} else {
|
||||
iced::widget::Space::new().width(0.0).height(0.0).into()
|
||||
};
|
||||
@@ -6317,6 +6346,7 @@ mod tests {
|
||||
state.share_audio_apps = vec!["Firefox".to_string()];
|
||||
state.share_audio_selection = Some("Firefox".to_string());
|
||||
state.share_starting = true;
|
||||
state.share_audio_dropped = true;
|
||||
state.clip_status.lock().unwrap().playing_id = Some(attachment_id);
|
||||
|
||||
state.reset_room_state();
|
||||
@@ -6341,6 +6371,7 @@ mod tests {
|
||||
assert!(state.share_audio_apps.is_empty());
|
||||
assert!(state.share_audio_selection.is_none());
|
||||
assert!(!state.share_starting);
|
||||
assert!(!state.share_audio_dropped);
|
||||
|
||||
for _ in 0..50 {
|
||||
if crate::audio::clip_player::status_snapshot(&state.clip_status).playing_id.is_none() {
|
||||
@@ -6409,6 +6440,46 @@ mod tests {
|
||||
assert!(state.share_picker_open);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn share_audio_dropped_tracks_app_audio_events() {
|
||||
// While sharing a specific app, app_audio lost/routed toggles the warning
|
||||
// flag; start and stop both reset it so it can't linger across sessions.
|
||||
let mut state = AppState::default();
|
||||
|
||||
// Start sharing — flag is clear.
|
||||
let _ = update(
|
||||
&mut state,
|
||||
AppMessage::UiEventReceived(UiEvent::ScreenShareStarted),
|
||||
);
|
||||
assert!(!state.share_audio_dropped);
|
||||
|
||||
// The chosen app's audio stops → warning on.
|
||||
let _ = update(
|
||||
&mut state,
|
||||
AppMessage::UiEventReceived(UiEvent::ShareAudioActive(false)),
|
||||
);
|
||||
assert!(state.share_audio_dropped);
|
||||
|
||||
// It plays again → warning off.
|
||||
let _ = update(
|
||||
&mut state,
|
||||
AppMessage::UiEventReceived(UiEvent::ShareAudioActive(true)),
|
||||
);
|
||||
assert!(!state.share_audio_dropped);
|
||||
|
||||
// Drop again, then stop sharing → flag reset regardless.
|
||||
let _ = update(
|
||||
&mut state,
|
||||
AppMessage::UiEventReceived(UiEvent::ShareAudioActive(false)),
|
||||
);
|
||||
assert!(state.share_audio_dropped);
|
||||
let _ = update(
|
||||
&mut state,
|
||||
AppMessage::UiEventReceived(UiEvent::ScreenShareStopped),
|
||||
);
|
||||
assert!(!state.share_audio_dropped);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn peer_gate_persists_when_on_and_clears_when_off() {
|
||||
let mut config = AppConfig::default();
|
||||
|
||||
Reference in New Issue
Block a user