fix(playback): poll player state at uniform 250ms cadence
The previous adaptive scheme dropped to 5s when idle and 1s when paused-with-track. PLAYBACK_STATE_WAKEUP catches synchronous V2 mutations (pause/resume/stop/seek) instantly, but play_data() is asynchronous: it queues an AudioCommand::Play over a channel and returns. The audio thread sets is_playing/start_playback_timer only after decoder + device init (500-2000ms), so the wakeup fires too early — the loop reads stale state, decides 'idle' or 'paused', and sleeps for the full long branch before catching the actual transition. current_position() is wall-clock based (now - playback_start_millis), so by the time the next idle tick arrives, position is already past second 1 and the seekbar appears to jump straight to 2+. Polling uniformly at 250ms catches the async transition within one cadence and keeps the seekbar showing 0:00 -> 0:01 -> 0:02 from the start. Cost is negligible (~10 atomic reads x 4 Hz), and should_emit filters every no-change iteration so the frontend wire is unaffected. cc
This commit is contained in:
1 file changed
+21
-17
+21
-17
@@ -1254,24 +1254,28 @@ pub fn run(qconnect_cli_override: Option<bool>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adaptive polling:
|
// Uniform 250ms polling cadence regardless of state.
|
||||||
// - 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
|
// The earlier adaptive scheme used 5s when idle and 1s when
|
||||||
// play/pause/seek command by their full sleep duration. We
|
// paused-with-track to save power. In practice the savings
|
||||||
// race the sleep against `PLAYBACK_STATE_WAKEUP`, which the
|
// were imperceptible (the loop only does atomic reads when
|
||||||
// V2 playback commands signal after they mutate state, so
|
// there's no state change, since `should_emit` filters all
|
||||||
// the loop reads the new state within milliseconds instead
|
// no-op iterations) and the cost was visible: when play
|
||||||
// of waiting up to 5 seconds for the next idle tick.
|
// starts, `play_data` is asynchronous — the audio thread
|
||||||
let sleep_duration = if is_playing {
|
// sets `is_playing` / `current_position` only after decoder
|
||||||
std::time::Duration::from_millis(250)
|
// and device init (~500-2000ms). A wakeup fired at the end
|
||||||
} else if track_id == 0 {
|
// of `v2_play_track` reads stale state, so the loop falls
|
||||||
std::time::Duration::from_millis(5000)
|
// back to its long sleep and the first emit reflecting the
|
||||||
} else {
|
// real position arrives much later. By the time it lands,
|
||||||
std::time::Duration::from_millis(1000)
|
// `current_position()` (wall-clock based) is already past
|
||||||
};
|
// second 0-1, so the seekbar appears to jump straight to 2+.
|
||||||
|
//
|
||||||
|
// Polling uniformly at 250ms catches the async transition
|
||||||
|
// within one cadence and keeps the seekbar showing 0:00 →
|
||||||
|
// 0:01 → 0:02 from the start. PLAYBACK_STATE_WAKEUP is kept
|
||||||
|
// as the fast path for synchronous V2 commands (pause/
|
||||||
|
// resume/stop/seek) — it still gives them sub-50ms response.
|
||||||
|
let sleep_duration = std::time::Duration::from_millis(250);
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
_ = tokio::time::sleep(sleep_duration) => {},
|
_ = tokio::time::sleep(sleep_duration) => {},
|
||||||
_ = commands_v2::helpers::PLAYBACK_STATE_WAKEUP.notified() => {},
|
_ = commands_v2::helpers::PLAYBACK_STATE_WAKEUP.notified() => {},
|
||||||
|
|||||||
Reference in new issue
Block a user