From 3e7b2582a20bf4fc9496875dceb655fdc4fbb860 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Thu, 14 May 2026 07:36:59 -0400 Subject: [PATCH] fix(player): queue skip + cut over silence on track tap Two bugs in the audio handler caused the playback issues seen on the v2026.05.13.0 build: 1. **Queue button on the now-playing screen did nothing.** MinstrelAudio Handler never overrode skipToQueueItem, so taps in QueueScreen fell through to BaseAudioHandler's empty default. The queue UI updated nothing because the handler did nothing. Now overridden: rebuilds the source list via setQueueFromTracks(_lastTracks, initialIndex) so the targeted item plays cleanly even when its source hadn't been built yet by the background fill. 2. **Tapping a song in a playlist let the previous track bleed through until the new source finished building.** setQueueFromTracks awaits _buildAudioSource before swapping the player's source list, and that wait can be a few hundred ms on a cache miss. During the wait the old source kept playing while the mini player UI had already flipped to the new title/artist. Now pause()ing the player at the start of setQueueFromTracks silences the old source the moment the user taps. Also stashes the most recent TrackRef list as _lastTracks so skipToQueueItem can reconstruct sources without having to peek into just_audio's internal source list. --- flutter_client/lib/player/audio_handler.dart | 35 ++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/flutter_client/lib/player/audio_handler.dart b/flutter_client/lib/player/audio_handler.dart index db622187..9d48ae89 100644 --- a/flutter_client/lib/player/audio_handler.dart +++ b/flutter_client/lib/player/audio_handler.dart @@ -68,6 +68,14 @@ class MinstrelAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandl /// new queue, leaving the player "locked" to a corrupted state. int _queueGeneration = 0; + /// Tracks the most recent setQueueFromTracks() input so + /// skipToQueueItem can reconstruct the source list. just_audio + /// requires every source to be built before it can be a skip + /// target, but setQueueFromTracks only builds the initial source + /// and fills the rest in the background — so a skip to an index + /// past the fill front needs to rebuild from the stored tracks. + List _lastTracks = const []; + /// Volume stream for UI subscribers. Mirrors the just_audio player's /// volume directly; set via setVolume(double). Stream get volumeStream => _player.volumeStream; @@ -105,6 +113,16 @@ class MinstrelAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandl // Reset suppress flag in case a prior backward-fill bailed on // gen check before reaching its `finally`. _suppressIndexUpdates = false; + _lastTracks = tracks; + + // Pause the old source immediately so the previous track stops + // audibly the moment the user taps, instead of bleeding through + // until the new source finishes building. setAudioSources below + // will swap the source list cleanly; pause is the simplest way + // to silence the player during the (possibly multi-100ms) build. + if (_player.playing) { + await _player.pause(); + } // Populate the visible queue + current mediaItem immediately so // the player UI reflects the user's tap before any source has @@ -130,6 +148,23 @@ class MinstrelAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandl unawaited(_fillRemainingSources(tracks, clampedInitial, myGen)); } + /// Switches playback to the [index]th queue item. The full + /// just_audio source list isn't necessarily built yet + /// (_fillRemainingSources runs in the background), so we + /// reconstruct from the stored TrackRef list rather than calling + /// _player.seek(index: ...) on a possibly-missing source. Calling + /// setQueueFromTracks again is the safe path: it bumps the + /// generation, cancels any in-flight fill, rebuilds source[0] as + /// the target, and re-fills around. play() restarts playback so + /// queue taps feel like "jump to this song" rather than "set the + /// pointer and wait for me to press play." + @override + Future skipToQueueItem(int index) async { + if (index < 0 || index >= _lastTracks.length) return; + await setQueueFromTracks(_lastTracks, initialIndex: index); + await play(); + } + /// Background fill of the rest of the just_audio source list after /// the initial source is playing. Forward direction first (most /// common skipNext target). Backward inserts shift the player's