diff --git a/flutter_client/lib/player/audio_handler.dart b/flutter_client/lib/player/audio_handler.dart index 663f9c28..bec44789 100644 --- a/flutter_client/lib/player/audio_handler.dart +++ b/flutter_client/lib/player/audio_handler.dart @@ -70,6 +70,13 @@ class MinstrelAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandl /// is already correct, and we re-enable normal listener behavior. bool _suppressIndexUpdates = false; + /// Increments on every setQueueFromTracks call. The background + /// fill task captures the value at start and bails if the live + /// counter has moved on — without this, a stale fill will keep + /// addAudioSource'ing tracks from the previous playlist into the + /// new queue, leaving the player "locked" to a corrupted state. + int _queueGeneration = 0; + /// Volume stream for UI subscribers. Mirrors the just_audio player's /// volume directly; set via setVolume(double). Stream get volumeStream => _player.volumeStream; @@ -103,6 +110,15 @@ class MinstrelAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandl if (tracks.isEmpty) return; final clampedInitial = initialIndex.clamp(0, tracks.length - 1); + // Bump the generation FIRST. Any in-flight _fillRemainingSources + // from a previous play will see the mismatch on its next gen + // check and stop calling player mutations — important so a stale + // fill doesn't append old-playlist tracks into the new queue. + final myGen = ++_queueGeneration; + // Reset suppress flag in case a prior backward-fill bailed on + // gen check before reaching its `finally`. + _suppressIndexUpdates = false; + // Populate the visible queue + current mediaItem immediately so // the player UI reflects the user's tap before any source has // been built. Source list at the just_audio layer fills in @@ -112,7 +128,7 @@ class MinstrelAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandl mediaItem.add(items[clampedInitial]); debugPrint('audio_handler.setQueueFromTracks: ' - '_baseUrl="$_baseUrl" trackCount=${tracks.length}'); + '_baseUrl="$_baseUrl" trackCount=${tracks.length} gen=$myGen'); final sw = Stopwatch()..start(); // Fast path: build only the initial source so the player can @@ -120,6 +136,10 @@ class MinstrelAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandl // in the background — addAudioSource for next/auto-advance // tracks, insertAudioSource for skipPrev tracks. final initial = await _buildAudioSource(tracks[clampedInitial]); + if (myGen != _queueGeneration) { + debugPrint('audio_handler: superseded before setAudioSources (gen=$myGen)'); + return; + } debugPrint('audio_handler: built initial source in ' '${sw.elapsedMilliseconds}ms'); sw.reset(); @@ -129,7 +149,7 @@ class MinstrelAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandl debugPrint('audio_handler: setAudioSources(1) ${sw.elapsedMilliseconds}ms'); unawaited(_loadArtForCurrentItem()); - unawaited(_fillRemainingSources(tracks, clampedInitial)); + unawaited(_fillRemainingSources(tracks, clampedInitial, myGen)); } /// Background fill of the rest of the just_audio source list after @@ -138,12 +158,23 @@ class MinstrelAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandl /// currentIndex; we suppress _onCurrentIndexChanged side effects /// for those so the mediaItem stream doesn't bounce to the wrong /// queue entry. + /// + /// `gen` is the queue generation captured when this fill started. + /// Every loop iteration checks against _queueGeneration; if a + /// newer setQueueFromTracks has run (user tapped play on something + /// else), bail immediately so we don't pollute the new queue with + /// addAudioSource calls from this stale fill. Future _fillRemainingSources( - List tracks, int initialIndex) async { + List tracks, int initialIndex, int gen) async { final sw = Stopwatch()..start(); for (var i = initialIndex + 1; i < tracks.length; i++) { + if (gen != _queueGeneration) { + debugPrint('audio_handler: forward fill aborted (gen=$gen, current=$_queueGeneration)'); + return; + } try { final src = await _buildAudioSource(tracks[i]); + if (gen != _queueGeneration) return; await _player.addAudioSource(src); } catch (e) { debugPrint('audio_handler: forward fill failed for ${tracks[i].id}: $e'); @@ -157,13 +188,20 @@ class MinstrelAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandl _suppressIndexUpdates = true; try { for (var i = 0; i < initialIndex; i++) { + if (gen != _queueGeneration) { + debugPrint('audio_handler: backward fill aborted (gen=$gen, current=$_queueGeneration)'); + return; + } final src = await _buildAudioSource(tracks[i]); + if (gen != _queueGeneration) return; await _player.insertAudioSource(i, src); } } catch (e) { debugPrint('audio_handler: backward fill failed: $e'); } finally { - _suppressIndexUpdates = false; + // Only release the flag if we're still the active gen — a + // newer setQueueFromTracks already reset it for itself. + if (gen == _queueGeneration) _suppressIndexUpdates = false; } debugPrint('audio_handler: backward fill ($initialIndex) ' '${sw.elapsedMilliseconds}ms');