feat(playback): wake state polling loop on V2 playback mutations
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
This commit is contained in:
4 files changed
+34
-5
No files matched your search
@@ -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).
|
||||
|
||||
@@ -20,7 +20,7 @@ pub struct V2PlaylistSuggestionsInput {
|
||||
pub config: Option<crate::artist_vectors::SuggestionConfig>,
|
||||
}
|
||||
|
||||
mod helpers;
|
||||
pub(crate) mod helpers;
|
||||
pub use helpers::*;
|
||||
|
||||
mod runtime;
|
||||
|
||||
@@ -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;
|
||||
|
||||
+11
-1
@@ -1258,6 +1258,13 @@ pub fn run(qconnect_cli_override: Option<bool>) {
|
||||
// - 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<bool>) {
|
||||
} 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() => {},
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user