fix(audio): guard underrun/stream-complete against false end-of-playback

pause() clears underrun_ so setStreamComplete can't fire TrackEnded while paused; resetToStart() resets streamComplete. Prior fix: underrun_ park + streamComplete discriminator prevent the Opus-startup false-end. Tests: 18 PlaybackScheduler cases including pause-during-underrun and underrun->resume->genuine-end-once.
This commit is contained in:
daniel-c-harvey
2026-06-25 15:16:22 -04:00
parent 3aed5c129f
commit 67422e922d
5 changed files with 340 additions and 14 deletions
+11 -1
View File
@@ -237,6 +237,12 @@ export class AudioPlayer {
}
}
this.streamingCompleted = true;
// Hand the genuine-end signal to the scheduler AFTER the tail buffers are added and
// scheduled: now an empty scheduled queue is a real end-of-track, not a startup gap, so
// the scheduler may fire onPlaybackEnded when its queue drains. If the queue was already
// empty at this point (the tail produced no buffers, or they were already played),
// setStreamComplete finalises immediately.
this.scheduler.setStreamComplete(true);
return { success: true, bufferCount: this.scheduler.getBufferCount() };
} catch (error) {
return { success: false, error: (error as Error).message };
@@ -321,9 +327,13 @@ export class AudioPlayer {
}
}
// Check if streaming is complete
// Check if streaming is complete. The StreamDecoder self-detects completion by byte
// count (WAV/MP3/FLAC); propagate that to the scheduler so a drained queue past this
// point is treated as a genuine end. Buffers from this chunk were already added above,
// so any final end fires through handleSourceEnded when they drain.
if (this.streamDecoder.isComplete) {
this.streamingCompleted = true;
this.scheduler.setStreamComplete(true);
}
const canStart = this.streamDecoder.headerParsed &&