fix(flutter): cancel stale background fills when queue changes
The lazy source-build commit (1ddde12) introduced a race: when the
user taps play on a new track while a previous queue's
_fillRemainingSources is still working, the stale fill keeps
calling _player.addAudioSource() on the new player state — appending
old-playlist tracks into the new queue and confusing the player into
the "locked to one song" symptom.
Fix: queue-generation counter. setQueueFromTracks bumps
_queueGeneration first thing; the background fill captures its gen
at start and aborts before any further player mutation if a newer
queue has taken over. The previous play() never gets to mutate the
new player state.
Also resets _suppressIndexUpdates at the start of every
setQueueFromTracks (defensive — covers the case where a prior
backward-fill bailed on its gen check before reaching `finally`)
and only releases the flag in finally if we're still the active
gen.
Symptoms this should resolve:
- "locked to one song" after rapid play taps
- Late `play() returned 73189ms` lines indicating a previous
hung play() call finally resolving and stepping on current state
- Player stuck in odd processingState after queue swaps
This commit is contained in:
@@ -70,6 +70,13 @@ class MinstrelAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandl
|
|||||||
/// is already correct, and we re-enable normal listener behavior.
|
/// is already correct, and we re-enable normal listener behavior.
|
||||||
bool _suppressIndexUpdates = false;
|
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 stream for UI subscribers. Mirrors the just_audio player's
|
||||||
/// volume directly; set via setVolume(double).
|
/// volume directly; set via setVolume(double).
|
||||||
Stream<double> get volumeStream => _player.volumeStream;
|
Stream<double> get volumeStream => _player.volumeStream;
|
||||||
@@ -103,6 +110,15 @@ class MinstrelAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandl
|
|||||||
if (tracks.isEmpty) return;
|
if (tracks.isEmpty) return;
|
||||||
final clampedInitial = initialIndex.clamp(0, tracks.length - 1);
|
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
|
// Populate the visible queue + current mediaItem immediately so
|
||||||
// the player UI reflects the user's tap before any source has
|
// the player UI reflects the user's tap before any source has
|
||||||
// been built. Source list at the just_audio layer fills in
|
// 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]);
|
mediaItem.add(items[clampedInitial]);
|
||||||
|
|
||||||
debugPrint('audio_handler.setQueueFromTracks: '
|
debugPrint('audio_handler.setQueueFromTracks: '
|
||||||
'_baseUrl="$_baseUrl" trackCount=${tracks.length}');
|
'_baseUrl="$_baseUrl" trackCount=${tracks.length} gen=$myGen');
|
||||||
final sw = Stopwatch()..start();
|
final sw = Stopwatch()..start();
|
||||||
|
|
||||||
// Fast path: build only the initial source so the player can
|
// 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
|
// in the background — addAudioSource for next/auto-advance
|
||||||
// tracks, insertAudioSource for skipPrev tracks.
|
// tracks, insertAudioSource for skipPrev tracks.
|
||||||
final initial = await _buildAudioSource(tracks[clampedInitial]);
|
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 '
|
debugPrint('audio_handler: built initial source in '
|
||||||
'${sw.elapsedMilliseconds}ms');
|
'${sw.elapsedMilliseconds}ms');
|
||||||
sw.reset();
|
sw.reset();
|
||||||
@@ -129,7 +149,7 @@ class MinstrelAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandl
|
|||||||
debugPrint('audio_handler: setAudioSources(1) ${sw.elapsedMilliseconds}ms');
|
debugPrint('audio_handler: setAudioSources(1) ${sw.elapsedMilliseconds}ms');
|
||||||
|
|
||||||
unawaited(_loadArtForCurrentItem());
|
unawaited(_loadArtForCurrentItem());
|
||||||
unawaited(_fillRemainingSources(tracks, clampedInitial));
|
unawaited(_fillRemainingSources(tracks, clampedInitial, myGen));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Background fill of the rest of the just_audio source list after
|
/// 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
|
/// currentIndex; we suppress _onCurrentIndexChanged side effects
|
||||||
/// for those so the mediaItem stream doesn't bounce to the wrong
|
/// for those so the mediaItem stream doesn't bounce to the wrong
|
||||||
/// queue entry.
|
/// 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<void> _fillRemainingSources(
|
Future<void> _fillRemainingSources(
|
||||||
List<TrackRef> tracks, int initialIndex) async {
|
List<TrackRef> tracks, int initialIndex, int gen) async {
|
||||||
final sw = Stopwatch()..start();
|
final sw = Stopwatch()..start();
|
||||||
for (var i = initialIndex + 1; i < tracks.length; i++) {
|
for (var i = initialIndex + 1; i < tracks.length; i++) {
|
||||||
|
if (gen != _queueGeneration) {
|
||||||
|
debugPrint('audio_handler: forward fill aborted (gen=$gen, current=$_queueGeneration)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
final src = await _buildAudioSource(tracks[i]);
|
final src = await _buildAudioSource(tracks[i]);
|
||||||
|
if (gen != _queueGeneration) return;
|
||||||
await _player.addAudioSource(src);
|
await _player.addAudioSource(src);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('audio_handler: forward fill failed for ${tracks[i].id}: $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;
|
_suppressIndexUpdates = true;
|
||||||
try {
|
try {
|
||||||
for (var i = 0; i < initialIndex; i++) {
|
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]);
|
final src = await _buildAudioSource(tracks[i]);
|
||||||
|
if (gen != _queueGeneration) return;
|
||||||
await _player.insertAudioSource(i, src);
|
await _player.insertAudioSource(i, src);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('audio_handler: backward fill failed: $e');
|
debugPrint('audio_handler: backward fill failed: $e');
|
||||||
} finally {
|
} 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) '
|
debugPrint('audio_handler: backward fill ($initialIndex) '
|
||||||
'${sw.elapsedMilliseconds}ms');
|
'${sw.elapsedMilliseconds}ms');
|
||||||
|
|||||||
Reference in New Issue
Block a user