From 8288350559ba7a199347d9ac7bc6fa68ee5fca5d Mon Sep 17 00:00:00 2001 From: vicrodh <103399283+vicrodh@users.noreply.github.com> Date: Wed, 6 May 2026 11:42:21 -0600 Subject: [PATCH] feat(playback): wake state polling loop on V2 playback mutations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The background task in lib.rs that emits playback:state to the frontend sleeps adaptively: 250ms while playing, 1s when paused-with-track, and 5s when idle. The 5s idle branch leaves the seekbar blank for several seconds on cold start because the loop only reads new state — and only then emits — after the sleep ends, even though audio is already playing. Add a tokio::sync::Notify (PLAYBACK_STATE_WAKEUP) shared via the existing commands_v2::helpers module. Race the sleep against PLAYBACK_STATE_WAKEUP.notified() with tokio::select. Each V2 command that mutates playback state (play_track, pause, resume, stop, seek) calls notify_one after the underlying bridge call succeeds, so the loop wakes within milliseconds and emits the new state immediately. notify_one stores up to one permit when no waiter is parked, so a notification fired between iterations is delivered on the next select. Make commands_v2::helpers a pub(crate) module so the static is reachable from lib.rs and the V2 command modules. cc --- src-tauri/src/commands_v2/helpers.rs | 11 +++++++++++ src-tauri/src/commands_v2/mod.rs | 2 +- src-tauri/src/commands_v2/playback.rs | 14 +++++++++++--- src-tauri/src/lib.rs | 12 +++++++++++- 4 files changed, 34 insertions(+), 5 deletions(-) diff --git a/src-tauri/src/commands_v2/helpers.rs b/src-tauri/src/commands_v2/helpers.rs index 7b4aef687..c084f3570 100644 --- a/src-tauri/src/commands_v2/helpers.rs +++ b/src-tauri/src/commands_v2/helpers.rs @@ -26,6 +26,17 @@ lazy_static::lazy_static! { /// Notify waiters when streaming download finishes. /// Prefetch waits on this if a streaming download is active. pub(crate) static ref CDN_STREAM_DONE: tokio::sync::Notify = tokio::sync::Notify::new(); + + /// Wakes the playback-state polling loop in `lib.rs` immediately after a + /// state-mutating V2 command (play, pause, resume, stop, seek). Without + /// this signal, the loop's idle/paused sleep (5s/1s) gates the first + /// `playback:state` emit on cold start, leaving the seekbar blank for + /// several seconds while audio is already playing. + /// + /// `notify_one` is used at the call site: it stores up to one permit if + /// no waiter is currently parked, so notifications are not lost between + /// loop iterations. + pub(crate) static ref PLAYBACK_STATE_WAKEUP: tokio::sync::Notify = tokio::sync::Notify::new(); } /// Number of active streaming downloads (for the currently-playing track). diff --git a/src-tauri/src/commands_v2/mod.rs b/src-tauri/src/commands_v2/mod.rs index 6eb3ebc64..8159ee94f 100644 --- a/src-tauri/src/commands_v2/mod.rs +++ b/src-tauri/src/commands_v2/mod.rs @@ -20,7 +20,7 @@ pub struct V2PlaylistSuggestionsInput { pub config: Option, } -mod helpers; +pub(crate) mod helpers; pub use helpers::*; mod runtime; diff --git a/src-tauri/src/commands_v2/playback.rs b/src-tauri/src/commands_v2/playback.rs index 6fffb00b5..91c44d758 100644 --- a/src-tauri/src/commands_v2/playback.rs +++ b/src-tauri/src/commands_v2/playback.rs @@ -290,7 +290,9 @@ pub async fn v2_pause_playback( log::info!("[V2] Command: pause_playback"); app_state.media_controls.set_playback(false); let bridge = bridge.get().await; - bridge.pause().map_err(RuntimeError::Internal) + bridge.pause().map_err(RuntimeError::Internal)?; + crate::commands_v2::helpers::PLAYBACK_STATE_WAKEUP.notify_one(); + Ok(()) } /// Resume playback (V2) @@ -307,7 +309,9 @@ pub async fn v2_resume_playback( log::info!("[V2] Command: resume_playback"); app_state.media_controls.set_playback(true); let bridge = bridge.get().await; - bridge.resume().map_err(RuntimeError::Internal) + bridge.resume().map_err(RuntimeError::Internal)?; + crate::commands_v2::helpers::PLAYBACK_STATE_WAKEUP.notify_one(); + Ok(()) } /// Stop playback (V2) @@ -324,7 +328,9 @@ pub async fn v2_stop_playback( log::info!("[V2] Command: stop_playback"); app_state.media_controls.set_stopped(); let bridge = bridge.get().await; - bridge.stop().map_err(RuntimeError::Internal) + bridge.stop().map_err(RuntimeError::Internal)?; + crate::commands_v2::helpers::PLAYBACK_STATE_WAKEUP.notify_one(); + Ok(()) } /// Seek to position in seconds (V2) @@ -351,6 +357,7 @@ pub async fn v2_seek( .media_controls .set_playback_with_progress(playback_state.is_playing, playback_state.position); + crate::commands_v2::helpers::PLAYBACK_STATE_WAKEUP.notify_one(); Ok(()) } @@ -1574,6 +1581,7 @@ pub async fn v2_play_track( .play_data(audio_data, track_id) .map_err(RuntimeError::Internal)?; log::info!("[V2] Playing track {} ({} bytes)", track_id, data_size); + crate::commands_v2::helpers::PLAYBACK_STATE_WAKEUP.notify_one(); // Prefetch next tracks in background (using CoreBridge queue) let upcoming_tracks = bridge_guard.peek_upcoming(V2_PREFETCH_LOOKAHEAD).await; diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index d9bc04633..ad02eca27 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1258,6 +1258,13 @@ pub fn run(qconnect_cli_override: Option) { // - fast (250ms) when playing - improves seekbar/lyrics sync // - slow (1000ms) when paused/stopped with a track loaded // - very slow (5000ms) when no track is loaded (idle) + // + // The idle/paused branches would gate the first emit after a + // play/pause/seek command by their full sleep duration. We + // race the sleep against `PLAYBACK_STATE_WAKEUP`, which the + // V2 playback commands signal after they mutate state, so + // the loop reads the new state within milliseconds instead + // of waiting up to 5 seconds for the next idle tick. let sleep_duration = if is_playing { std::time::Duration::from_millis(250) } else if track_id == 0 { @@ -1265,7 +1272,10 @@ pub fn run(qconnect_cli_override: Option) { } else { std::time::Duration::from_millis(1000) }; - tokio::time::sleep(sleep_duration).await; + tokio::select! { + _ = tokio::time::sleep(sleep_duration) => {}, + _ = commands_v2::helpers::PLAYBACK_STATE_WAKEUP.notified() => {}, + } } });