21.3 review fixes: guard superseded-seek failures; restore post-recovery retry

C6/AC8: IsStillActiveSeek() predicate guards all three SeekBeyondBuffer
failure exits, so a superseded seek never recovers over a newer seek's
state. AC6: empty scheduler routes to seekBeyondBuffer so a same-target
retry (seek or play) refetches instead of no-oping.
This commit is contained in:
daniel-c-harvey
2026-06-23 23:55:28 -04:00
parent af4cb186f3
commit b93881cd66
4 changed files with 102 additions and 15 deletions
+15 -8
View File
@@ -440,21 +440,28 @@ export class AudioPlayer {
const bufferEnd = this.scheduler.getTotalDuration() + bufferStart;
// The window-miss test for BOTH directions, and the 21.3 refill trigger for backward seeks.
// Position must be within [bufferStart, bufferEnd] to resolve from the retained buffers:
// - position >= bufferStart : UC3 — seek back within the retained back-window. Served from
// buffer with NO network refetch. (The lower bound is load-bearing: after eviction or a
// prior seek-beyond-buffer, bufferStart > 0, and a target below it would otherwise produce
// a negative bufferRelativePosition in seekWithinBuffer, silently clamping to position 0.)
// Position must be within [bufferStart, bufferEnd] AND the scheduler must hold buffers to
// resolve from the retained window:
// - position >= bufferStart AND hasBuffers : UC3 — seek back within the retained back-window.
// Served from buffer with NO network refetch. (The lower bound is load-bearing: after
// eviction or a prior seek-beyond-buffer, bufferStart > 0, and a target below it would
// otherwise produce a negative bufferRelativePosition in seekWithinBuffer, silently clamping
// to position 0.)
// - position < bufferStart : UC4 — seek back PAST the retained tail (the window was evicted).
// Falls through to seekBeyondBuffer, which is the existing Range path run toward an EARLIER
// offset. This is the 21.3 window-miss refill: "a seek the listener didn't initiate" reuses
// the same per-path resolver + reinit a forward seek-beyond-buffer uses, no new mechanism.
// - position > bufferEnd : UC2/UC5 — forward seek beyond buffer, unchanged.
if (position >= bufferStart && position <= bufferEnd) {
// - !hasBuffers (degenerate [P,P] window post-recovery): the window check above would
// spuriously route ANY target to seekWithinBuffer (bufferStart==bufferEnd==seekPosition
// after recoverFromFailedRefill). Force seekBeyondBuffer so a same-target retry actually
// refetches (AC6 retry contract). The !hasBuffers guard only fires in the degenerate case —
// a populated retained window has buffers and is unaffected (AC4 not regressed).
if (position >= bufferStart && position <= bufferEnd && this.scheduler.hasBuffers()) {
return this.seekWithinBuffer(position);
} else {
// Seeking outside the retained window - signal C# to fetch a new stream from the resolved
// offset (earlier for a back-past-window refill, later for a forward seek).
// Seeking outside the retained window, or to any position in an empty scheduler —
// signal C# to fetch a new stream from the resolved offset.
return this.seekBeyondBuffer(position);
}
}