Files
rustdesk/src/client/tests/audio_playback_recovery_tests.rs
fufesou 28269af9c7 fix(audio): preserve compatible playback and recover failed Windows outputs (#16150)
* fix(audio): preserve compatible playback on replacement failure

* fix(audio): use Windows events for capture worker wakeups

* fix(audio): recover Windows playback after output device failure

* reduce diffs

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(audio): reset decoder when retaining compatible playback

* fix(audio): integrate playback retention and Windows capture wakeups

* revert(audio): remove Windows capture-event notification changes

* docs(audio): explain Windows 7 capture limitations

* docs(audio): clarify capture changes introduced by #16095

* fix(audio): preserve playback through asynchronous Windows startup

Keep a compatible active output until the replacement callback confirms startup, preserving decoder progress on promotion or rollback. Handle superseding formats and simultaneous output failures without losing recovery state. Pin the scoped CPAL WASAPI event-ownership fix and add deterministic and native regressions.

* fix(audio): retain ready output across superseding formats

* fix(audio): update CPAL teardown recovery

Pin the upstream-aligned WASAPI cleanup with shared event lifetime, self-join prevention and fallible destructor diagnostics. Preserve the existing dependency graph and audio implementation.

* Use upstream-style CPAL stream teardown

* Refact: remove low value test

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(audio): retry playback when startup confirmation times out

Signed-off-by: fufesou <linlong1266@gmail.com>

* update cpal

Signed-off-by: fufesou <linlong1266@gmail.com>

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-09-16 11:55:53 +08:00

227 lines
8.8 KiB
Rust

use super::*;
use crate::client::{
audio_playback::AudioPlaybackStatus, audio_playback_recovery::STARTUP_CONFIRMATION_TIMEOUT,
};
use cpal::StreamError;
use crossbeam_queue::SegQueue;
use hbb_common::tokio::time::Instant;
use std::time::Duration;
const AFTER_COOLDOWN: Duration = Duration::from_secs(2);
type PendingOutput = (
Arc<AtomicBool>,
Arc<AudioPlaybackStatus>,
Arc<SegQueue<StreamError>>,
);
fn install_output(handler: &mut AudioHandler, dropped: Arc<AtomicBool>) {
handler.sample_rate = (INPUT_RATE, OUTPUT_RATE);
handler.device_channel = CHANNELS;
handler.audio_stream = Some(Box::new(TrackedAudioStream(dropped)));
handler.playback_status.ready.store(true, Ordering::Release);
}
fn recovery_handler() -> (AudioHandler, Arc<AtomicBool>) {
let dropped = Arc::new(AtomicBool::new(false));
let mut handler = AudioHandler::default();
handler.handle_format_with_start(format(INPUT_RATE, CHANNELS), |candidate, _| {
install_output(candidate, dropped.clone());
Ok(())
});
(handler, dropped)
}
fn begin_pending(handler: &mut AudioHandler) -> PendingOutput {
let dropped = Arc::new(AtomicBool::new(false));
let mut state = None;
handler.handle_format_with_start(format(INPUT_RATE, CHANNELS), |candidate, _| {
install_output(candidate, dropped.clone());
candidate
.playback_status
.ready
.store(false, Ordering::Release);
state = Some((
candidate.playback_status.clone(),
candidate.playback_recovery.errors.clone(),
));
Ok(())
});
let (status, errors) = state.unwrap();
(dropped, status, errors)
}
#[test]
fn unconfirmed_start_retries_without_callback_or_error() {
let dropped = Arc::new(AtomicBool::new(false));
let mut handler = AudioHandler::default();
handler.handle_format_with_start(format(INPUT_RATE, CHANNELS), |candidate, _| {
install_output(candidate, dropped.clone());
candidate
.playback_status
.ready
.store(false, Ordering::Release);
Ok(())
});
handler.recover_playback_with(Instant::now(), |_, _| {
panic!("Startup confirmation deadline has not elapsed")
});
assert!(!dropped.load(Ordering::SeqCst));
let expired = Instant::now() + STARTUP_CONFIRMATION_TIMEOUT;
let mut attempts = 0;
handler.recover_playback_with(expired, |candidate, requested| {
attempts += 1;
assert!(dropped.load(Ordering::SeqCst));
assert_eq!(requested, format(INPUT_RATE, CHANNELS));
install_output(candidate, Arc::new(AtomicBool::new(false)));
Ok(())
});
assert_eq!(attempts, 1);
handler.recover_playback_with(expired + STARTUP_CONFIRMATION_TIMEOUT, |_, _| {
panic!("Confirmed output must not be reopened")
});
}
#[test]
fn already_terminal_candidate_cannot_replace_compatible_output() {
let (mut handler, dropped) = recovery_handler();
let candidate_dropped = Arc::new(AtomicBool::new(false));
let old_buffer = handler.audio_buffer.0.clone();
handler.handle_format_with_start(format(INPUT_RATE, CHANNELS), |candidate, _| {
install_output(candidate, candidate_dropped.clone());
candidate
.playback_recovery
.errors
.push(StreamError::DeviceNotAvailable);
Ok(())
});
assert!(!dropped.load(Ordering::SeqCst));
assert!(candidate_dropped.load(Ordering::SeqCst));
assert!(Arc::ptr_eq(&old_buffer, &handler.audio_buffer.0));
handler.recover_playback_with(Instant::now() + AFTER_COOLDOWN, |_, _| {
panic!("Compatible active output must not be reopened")
});
}
#[test]
fn pending_output_keeps_playing_and_transfers_decoder_history_on_commit() {
let (mut handler, old_dropped) = recovery_handler();
let old_buffer = handler.audio_buffer.0.clone();
let (_, status, _) = begin_pending(&mut handler);
let frame = audio_frame();
let (mut reference, mut expected) = decoder(INPUT_RATE);
reference
.decode_float(&frame.data, &mut expected, false)
.unwrap();
handler.handle_frame(frame.clone());
assert!(Arc::ptr_eq(&old_buffer, &handler.audio_buffer.0));
assert!(!drain_audio(&handler).is_empty());
status.ready.store(true, Ordering::Release);
handler.recover_playback_with(Instant::now(), |_, _| panic!("Candidate already exists"));
assert!(old_dropped.load(Ordering::SeqCst));
assert!(Arc::ptr_eq(&status, &handler.playback_status));
let samples = reference
.decode_float(&frame.data, &mut expected, false)
.unwrap()
* CHANNELS as usize;
handler.handle_frame(frame);
assert_eq!(
&handler.audio_decoder.as_ref().unwrap().1[..samples],
&expected[..samples]
);
}
#[test]
fn rollback_keeps_the_restarted_decoders_accumulated_history() {
let (mut handler, old_dropped) = recovery_handler();
let frame = audio_frame();
handler.handle_frame(frame.clone());
let (candidate_dropped, _, errors) = begin_pending(&mut handler);
let (mut reference, mut expected) = decoder(INPUT_RATE);
handler.handle_frame(frame.clone());
reference
.decode_float(&frame.data, &mut expected, false)
.unwrap();
errors.push(StreamError::DeviceNotAvailable);
handler.recover_playback_with(Instant::now(), |_, _| panic!("Old output still works"));
let samples = reference
.decode_float(&frame.data, &mut expected, false)
.unwrap()
* CHANNELS as usize;
handler.handle_frame(frame);
assert!(!old_dropped.load(Ordering::SeqCst));
assert!(candidate_dropped.load(Ordering::SeqCst));
assert_eq!(
&handler.audio_decoder.as_ref().unwrap().1[..samples],
&expected[..samples]
);
}
#[test]
fn later_compatible_format_retires_only_the_pending_attempt() {
let (mut handler, old_dropped) = recovery_handler();
let (first_dropped, _, first_errors) = begin_pending(&mut handler);
let (second_dropped, second_status, _) = begin_pending(&mut handler);
assert!(first_dropped.load(Ordering::SeqCst));
assert!(!old_dropped.load(Ordering::SeqCst));
first_errors.push(StreamError::DeviceNotAvailable);
second_status.ready.store(true, Ordering::Release);
handler.recover_playback_with(Instant::now(), |_, _| {
panic!("Retired attempt affected current output")
});
assert!(old_dropped.load(Ordering::SeqCst));
assert!(!second_dropped.load(Ordering::SeqCst));
assert!(Arc::ptr_eq(&second_status, &handler.playback_status));
}
#[test]
fn both_outputs_failing_retains_format_and_paces_recovery() {
let (mut handler, old_dropped) = recovery_handler();
let old_errors = handler.playback_recovery.errors.clone();
let (candidate_dropped, _, errors) = begin_pending(&mut handler);
old_errors.push(StreamError::DeviceNotAvailable);
errors.push(StreamError::DeviceNotAvailable);
handler.recover_playback_with(Instant::now(), |_, _| panic!("Retry must be paced"));
assert!(old_dropped.load(Ordering::SeqCst));
assert!(candidate_dropped.load(Ordering::SeqCst));
assert!(handler.audio_stream.is_none());
let due = handler.playback_recovery.retry_at.unwrap();
let mut attempts = 0;
handler.recover_playback_with(due, |_, requested| {
attempts += 1;
assert_eq!(requested, format(INPUT_RATE, CHANNELS));
Ok(())
});
assert_eq!(attempts, 1);
}
#[test]
fn superseding_format_keeps_ready_candidate_when_active_output_failed() {
let (mut handler, old_dropped) = recovery_handler();
let old_errors = handler.playback_recovery.errors.clone();
let (candidate_dropped, candidate_status, _) = begin_pending(&mut handler);
candidate_status.ready.store(true, Ordering::Release);
old_errors.push(StreamError::DeviceNotAvailable);
handler.handle_format_with_start(format(INPUT_RATE, CHANNELS), failed_output);
assert!(!candidate_dropped.load(Ordering::SeqCst));
assert!(old_dropped.load(Ordering::SeqCst));
assert!(Arc::ptr_eq(&candidate_status, &handler.playback_status));
assert!(handler.audio_decoder.is_some());
assert!(handler.playback_recovery.retry_at.is_none());
}
#[test]
fn superseding_format_preserves_failure_when_both_outputs_failed() {
let (mut handler, old_dropped) = recovery_handler();
let old_errors = handler.playback_recovery.errors.clone();
let (candidate_dropped, status, errors) = begin_pending(&mut handler);
status.ready.store(true, Ordering::Release);
old_errors.push(StreamError::DeviceNotAvailable);
errors.push(StreamError::DeviceNotAvailable);
handler.handle_format_with_start(format(INPUT_RATE, CHANNELS), failed_output);
assert!(candidate_dropped.load(Ordering::SeqCst));
assert!(old_dropped.load(Ordering::SeqCst));
assert!(handler.audio_stream.is_none());
assert!(handler.playback_recovery.retry_at.is_some());
}