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:
@@ -649,8 +649,7 @@ public class StreamingAudioPlayerService : AudioPlayerService, IStreamingPlayerS
|
||||
{
|
||||
var technicalError = mediaResult.GetMessage() ?? "Failed to load audio from position";
|
||||
_logger.LogError("Failed to get track media from offset {Offset}: {Error}", byteOffset, technicalError);
|
||||
ErrorMessage = StreamingErrorHandler.GetUserFriendlyMessage(technicalError);
|
||||
IsSeekingBeyondBuffer = false;
|
||||
await RecoverFromFailedRefill(seekPosition, StreamingErrorHandler.GetUserFriendlyMessage(technicalError));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -661,8 +660,7 @@ public class StreamingAudioPlayerService : AudioPlayerService, IStreamingPlayerS
|
||||
if (!reinitResult.Success)
|
||||
{
|
||||
_logger.LogError("Failed to reinitialize for offset streaming: {Error}", reinitResult.Error);
|
||||
ErrorMessage = "Failed to seek to position";
|
||||
IsSeekingBeyondBuffer = false;
|
||||
await RecoverFromFailedRefill(seekPosition, "Failed to seek to position");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -691,13 +689,49 @@ public class StreamingAudioPlayerService : AudioPlayerService, IStreamingPlayerS
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// A refill fetch can fail deep into a long mix (the listener didn't initiate it). Recover
|
||||
// into a clean paused-but-loaded state (AC6) rather than leaving the starved scheduler to
|
||||
// fire a silent false end. Only when we are still the active seek — a superseding seek owns
|
||||
// the state and the OCE catch above handles its own teardown.
|
||||
_logger.LogError(ex, "Error during seek beyond buffer to position {Position}", seekPosition);
|
||||
ErrorMessage = StreamingErrorHandler.GetUserFriendlyMessage(ex.Message);
|
||||
IsSeekingBeyondBuffer = false;
|
||||
await NotifyStateChanged();
|
||||
if (ReferenceEquals(_streamingCancellation, seekCts))
|
||||
{
|
||||
await RecoverFromFailedRefill(seekPosition, StreamingErrorHandler.GetUserFriendlyMessage(ex.Message));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clean-failure recovery for a window-miss refill (Phase 21.3 / AC6). A backward seek past the
|
||||
/// retained tail re-fetches via the existing Range path; that mid-stream fetch the listener did not
|
||||
/// initiate can fail deep into a long mix. When it does, the pre-seek loop has already been
|
||||
/// cancelled and drained, but the JS scheduler is still holding stale pre-seek buffers and still
|
||||
/// "playing" — left alone it drains them and fires a silent false end (the wedged/starved state AC6
|
||||
/// forbids). This halts the scheduler into a paused-but-loaded state at <paramref name="seekPosition"/>,
|
||||
/// surfaces a clear error, and leaves the track loaded so the listener can retry the seek or pick
|
||||
/// another track. Mirrors <c>PlaybackScheduler.playFromPosition</c>'s end-of-buffer recovery: stop
|
||||
/// pretending to play.
|
||||
/// </summary>
|
||||
private async Task RecoverFromFailedRefill(double seekPosition, string userFacingError)
|
||||
{
|
||||
// Halt the starved scheduler JS-side (stop sources, drop stale buffers, anchor at the target).
|
||||
// Best-effort: if even this interop fails the player is no worse off, and we still surface the
|
||||
// error and settle C# state below.
|
||||
var recovered = await _audioInterop.RecoverFromFailedRefill(PlayerId, seekPosition);
|
||||
if (!recovered.Success)
|
||||
{
|
||||
_logger.LogWarning("Refill-failure recovery interop did not succeed: {Error}", recovered.Error);
|
||||
}
|
||||
|
||||
// Settle C# into the matching recoverable state: not playing, paused at the target, still loaded.
|
||||
ErrorMessage = userFacingError;
|
||||
IsPlaying = false;
|
||||
IsPaused = true;
|
||||
CurrentTime = seekPosition;
|
||||
IsSeekingBeyondBuffer = false;
|
||||
await NotifyStateChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Single method to reset all state - called by both Stop and Unload.
|
||||
/// </summary>
|
||||
|
||||
Reference in New Issue
Block a user