mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-22 14:35:41 +00:00
* fix(audio): resume playback seamlessly on output device switch (#743) When the OS default output device changed (Bluetooth, USB DAC, HDMI), rodio/cpal had to reopen the stream, which silently stopped the active sink and left the engine with no playback — causing the track to restart from the beginning (or not restart at all after the null-payload bug). Root cause (null-payload): audio_set_device emitted () (unit), which Tauri serialises to JSON null. The null-guard added for the "Rust handled replay internally" signal was therefore also triggered by manual device switches, so playTrack was never called and the engine stayed silent. Fix: audio_set_device now emits the current playback position as f64 (null remains the exclusive "Rust handled" sentinel). Rust-side seamless replay (device watcher path): reopen_output_stream captures a ResumeSnapshot before the blocking stream reopen, then calls try_resume_after_device_change, which: - local files (psysonic-local://): reopens the file, builds a new seekable source, seeks to the saved position — zero frontend round-trip, no audible restart. - fully-cached HTTP tracks (stream_completed_cache / spill file): replays from the in-memory or on-disk bytes — no re-download. - partial downloads / radio / paused: returns false → falls back to the existing frontend path (seekFallbackVisualTarget + playTrack). Frontend (useAudioDeviceBridge): null payload → Rust already resumed; skip playTrack. number payload → call playTrack + seekFallbackVisualTarget(position). Visibility: pub(super) → pub(crate) on the play_input / progress_task helpers that device_watcher.rs needs to call directly. * refactor(audio): split device_resume module; add bridge tests Split the 551-line device_watcher.rs (above the ~500-line soft ceiling) by extracting ResumeSnapshot and try_resume_after_device_change into a dedicated device_resume.rs module. device_watcher.rs is now 320 lines, device_resume.rs 258 lines. Add useAudioDeviceBridge.test.ts: 9 characterisation tests covering the null-payload guard ("Rust replayed, skip playTrack"), the seek-fallback path (position > 0.5 s sets seekFallbackVisualTarget), paused-device branch (resetAudioPause), and the device-reset event path. * chore(changelog): add entry for #765 (device switch seamless resume)
This commit is contained in:
@@ -89,10 +89,19 @@ pub async fn audio_set_device(
|
||||
|
||||
*state.stream_handle.lock().unwrap() = new_handle;
|
||||
|
||||
// Drop active sinks — they were bound to the old stream.
|
||||
if let Some(s) = state.current.lock().unwrap().sink.take() { s.stop(); }
|
||||
// Capture position and drop the active sink atomically so the position
|
||||
// reading is still valid (play_started / paused_at intact before take).
|
||||
let current_time = {
|
||||
let mut cur = state.current.lock().unwrap();
|
||||
let pos = cur.position();
|
||||
if let Some(s) = cur.sink.take() { s.stop(); }
|
||||
pos
|
||||
};
|
||||
if let Some(s) = state.fading_out_sink.lock().unwrap().take() { s.stop(); }
|
||||
|
||||
app.emit("audio:device-changed", ()).map_err(|e| e.to_string())?;
|
||||
// Emit the saved position so the frontend can use seekFallbackVisualTarget
|
||||
// and resume from where the track was, rather than restarting from the beginning.
|
||||
// null is reserved for "Rust already resumed internally" (see reopen_output_stream).
|
||||
app.emit("audio:device-changed", current_time).map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user