forked from Keelrleek/SoulStream
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
|
/// Transfers whose completed cache file should also be organized into the
|
||||||
/// library. A save can subscribe to a stream/preload already in progress.
|
/// library. A save can subscribe to a stream/preload already in progress.
|
||||||
pub deferred_saves: Mutex<HashMap<u64, PathBuf>>,
|
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)]
|
#[derive(Serialize, Clone)]
|
||||||
@@ -168,6 +190,10 @@ pub fn init(app: &tauri::AppHandle) {
|
|||||||
waveform_jobs: Mutex::new(HashMap::new()),
|
waveform_jobs: Mutex::new(HashMap::new()),
|
||||||
preloads: Mutex::new(HashMap::new()),
|
preloads: Mutex::new(HashMap::new()),
|
||||||
deferred_saves: Mutex::new(HashMap::new()),
|
deferred_saves: Mutex::new(HashMap::new()),
|
||||||
|
last_transport: Mutex::new(PlaybackSnapshot {
|
||||||
|
status: "idle".into(),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
let handle = app.clone();
|
let handle = app.clone();
|
||||||
@@ -201,6 +227,36 @@ pub fn init(app: &tauri::AppHandle) {
|
|||||||
}
|
}
|
||||||
PlayerEvent::Error(message) => PlayerUiEvent::Error { message },
|
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);
|
let _ = handle.emit("soulseek:player", ui);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in new issue
Block a user