playback: remember the last transport so a reload lands where you left off
PlayerState carries last_transport, mirrored as events go past, and player_state reports it. The frontend restores everything else from localStorage, but the transport cannot come from there — the audio lives in Rust, and a reload that trusted the browser's copy would show a position the engine had never heard of. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
dc918c98ef
commit
7547fed56d
1 file changed
+56
@@ -141,6 +141,28 @@ pub struct PlayerState {
|
||||
/// Transfers whose completed cache file should also be organized into the
|
||||
/// library. A save can subscribe to a stream/preload already in progress.
|
||||
pub deferred_saves: Mutex<HashMap<u64, PathBuf>>,
|
||||
/// The last transport state forwarded to the webview.
|
||||
///
|
||||
/// The audio thread is send-only — it reports by event and answers no
|
||||
/// questions — so a webview that RELOADS has no way to learn that audio is
|
||||
/// still running. It kept receiving position ticks and drew a moving
|
||||
/// scrubber under the words "Nothing playing", because every other part of
|
||||
/// the player store had been wiped with the page. This is the answer to
|
||||
/// "what is the audio thread doing right now", mirrored here as the events
|
||||
/// go past and read back by `player_state` at boot.
|
||||
pub last_transport: Mutex<PlaybackSnapshot>,
|
||||
}
|
||||
|
||||
/// What the audio thread is doing, as of its last event. Deliberately only
|
||||
/// TRANSPORT: which song this is, and the queue around it, are the frontend's
|
||||
/// own knowledge and are restored from its session snapshot.
|
||||
#[derive(Serialize, Clone, Default)]
|
||||
pub struct PlaybackSnapshot {
|
||||
/// "idle" | "buffering" | "playing" | "paused" — the same vocabulary the
|
||||
/// frontend's `player.status` uses, so a restore is a direct assignment.
|
||||
pub status: String,
|
||||
pub position: f64,
|
||||
pub duration: Option<f64>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
@@ -168,6 +190,10 @@ pub fn init(app: &tauri::AppHandle) {
|
||||
waveform_jobs: Mutex::new(HashMap::new()),
|
||||
preloads: Mutex::new(HashMap::new()),
|
||||
deferred_saves: Mutex::new(HashMap::new()),
|
||||
last_transport: Mutex::new(PlaybackSnapshot {
|
||||
status: "idle".into(),
|
||||
..Default::default()
|
||||
}),
|
||||
});
|
||||
|
||||
let handle = app.clone();
|
||||
@@ -201,6 +227,36 @@ pub fn init(app: &tauri::AppHandle) {
|
||||
}
|
||||
PlayerEvent::Error(message) => PlayerUiEvent::Error { message },
|
||||
};
|
||||
// Mirror the transport before emitting, so a webview that
|
||||
// reloads mid-track can ask what it missed. TrackEnded is
|
||||
// deliberately not recorded: the frontend owns the queue and
|
||||
// decides what follows, and the next Buffering/Stopped says
|
||||
// what actually happened.
|
||||
if let Some(state) = handle.try_state::<PlayerState>() {
|
||||
if let Ok(mut last) = state.last_transport.lock() {
|
||||
match &ui {
|
||||
PlayerUiEvent::Buffering => {
|
||||
last.status = "buffering".into();
|
||||
last.position = 0.0;
|
||||
}
|
||||
PlayerUiEvent::Playing => last.status = "playing".into(),
|
||||
PlayerUiEvent::Paused => last.status = "paused".into(),
|
||||
PlayerUiEvent::Stopped | PlayerUiEvent::Error { .. } => {
|
||||
*last = PlaybackSnapshot {
|
||||
status: "idle".into(),
|
||||
..Default::default()
|
||||
};
|
||||
}
|
||||
PlayerUiEvent::Position { secs, duration } => {
|
||||
last.position = *secs;
|
||||
if duration.is_some() {
|
||||
last.duration = *duration;
|
||||
}
|
||||
}
|
||||
PlayerUiEvent::TrackEnded { .. } => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
let _ = handle.emit("soulseek:player", ui);
|
||||
}
|
||||
})
|
||||
|
||||
Reference in new issue
Block a user