Phase 21.3: seek-back-past-window refill + clean refill-failure recovery
Seek-back past the retained tail reuses the existing seek-beyond-buffer Range path (per-path resolver). A failed refill now halts the scheduler into a paused-but-loaded state (AC6) instead of a silent false end.
This commit is contained in:
@@ -432,18 +432,29 @@ export class AudioPlayer {
|
||||
return { success: false, error: 'Invalid seek position' };
|
||||
}
|
||||
|
||||
// bufferStart is the absolute track time at which buffers[0] begins. Under Phase 21.1
|
||||
// partial eviction this is the start of the RETAINED BACK-WINDOW TAIL — eviction advances
|
||||
// playbackOffset as it drops played buffers off the front — so [bufferStart, bufferEnd] is
|
||||
// exactly the window currently held in memory.
|
||||
const bufferStart = this.scheduler.getPlaybackOffset();
|
||||
const bufferEnd = this.scheduler.getTotalDuration() + bufferStart;
|
||||
|
||||
// Position must be within [bufferStart, bufferEnd] to use buffered content.
|
||||
// A lower-bound check is required: after a seek-beyond-buffer, bufferStart is
|
||||
// set to the prior seek position. Seeking to a position below bufferStart would
|
||||
// produce a negative bufferRelativePosition in seekWithinBuffer, silently
|
||||
// clamping to position 0 of the offset buffer instead of the requested time.
|
||||
// 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 < 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) {
|
||||
return this.seekWithinBuffer(position);
|
||||
} else {
|
||||
// Seeking outside buffered window - signal C# to fetch new stream
|
||||
// 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).
|
||||
return this.seekBeyondBuffer(position);
|
||||
}
|
||||
}
|
||||
@@ -580,6 +591,44 @@ export class AudioPlayer {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recover the player into a clean, paused-but-loaded state after a window-miss REFILL failed
|
||||
* (Phase 21.3 / AC6). A refill is "a seek the listener didn't initiate"; when its Range fetch or
|
||||
* reinit fails mid-stream, the pre-seek loop has already been cancelled and drained, but the
|
||||
* scheduler is still holding stale pre-seek buffers and is still `isActive_`. Left alone it would
|
||||
* play the retained tail to exhaustion and fire `onPlaybackEnded` — a SILENT FALSE END (the
|
||||
* "wedged playing with a starved scheduler" AC6 forbids).
|
||||
*
|
||||
* The recovery mirrors `PlaybackScheduler.playFromPosition`'s end-of-buffer recovery in spirit:
|
||||
* stop pretending to play. We stop all sources and clear the buffers for a seek (clearForSeek
|
||||
* keeps no stale audio but is ready to accept a fresh continuation), set the offset to the
|
||||
* requested seek position, and leave the player paused there. The track stays loaded so the
|
||||
* listener can retry the seek or pick another track — no new transport control, only a recoverable
|
||||
* stop (C4). A subsequent seek to the same target re-enters seekBeyondBuffer cleanly because the
|
||||
* offset names the seek position and the scheduler is empty (so it routes to a fresh fetch).
|
||||
*
|
||||
* @param seekPosition The seek target the failed refill was aiming for; becomes the resume anchor.
|
||||
*/
|
||||
recoverFromFailedRefill(seekPosition: number): AudioResult {
|
||||
try {
|
||||
this.stopProgressTracking();
|
||||
// Halt the starved scheduler and drop the stale pre-seek buffers so no false end can fire.
|
||||
this.scheduler.clearForSeek();
|
||||
this.scheduler.setPlaybackOffset(seekPosition);
|
||||
|
||||
// Paused-but-loaded: not playing, not mid-seek-stream. pausePosition anchors a retry.
|
||||
this.isPlaying = false;
|
||||
this.isPaused = true;
|
||||
this.pausePosition = seekPosition;
|
||||
this.streamingStarted = false;
|
||||
this.streamingCompleted = false;
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return { success: false, error: (error as Error).message };
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Volume ====================
|
||||
|
||||
setVolume(volume: number): AudioResult {
|
||||
|
||||
Reference in New Issue
Block a user