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:
@@ -629,6 +629,15 @@ public class StreamingAudioPlayerService : AudioPlayerService, IStreamingPlayerS
|
||||
await DrainActiveStreamingTaskAsync();
|
||||
oldCts?.Dispose();
|
||||
|
||||
// Single-writer discipline (C6/AC8): all three failure exits must share the same guard.
|
||||
// TrackMediaClient.GetTrackMedia swallows OperationCanceledException and returns
|
||||
// Success==false, so a superseded seek lands in the media-fetch-fail branch below
|
||||
// rather than in the OCE catch. Without the guard those branches would call
|
||||
// RecoverFromFailedRefill — running clearForSeek + setPlaybackOffset against the player
|
||||
// state the NEWER seek now owns. A local predicate keeps all three exits symmetric so a
|
||||
// future exit cannot forget the check.
|
||||
bool IsStillActiveSeek() => ReferenceEquals(_streamingCancellation, seekCts);
|
||||
|
||||
try
|
||||
{
|
||||
// Update UI immediately
|
||||
@@ -649,7 +658,15 @@ 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);
|
||||
await RecoverFromFailedRefill(seekPosition, StreamingErrorHandler.GetUserFriendlyMessage(technicalError));
|
||||
// Guard: a superseded seek must NOT touch shared state. The newer seek owns teardown.
|
||||
if (IsStillActiveSeek())
|
||||
{
|
||||
await RecoverFromFailedRefill(seekPosition, StreamingErrorHandler.GetUserFriendlyMessage(technicalError));
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogDebug("Media-fetch failed on superseded seek to {Position} — newer seek owns state, skipping recovery", seekPosition);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -660,7 +677,15 @@ public class StreamingAudioPlayerService : AudioPlayerService, IStreamingPlayerS
|
||||
if (!reinitResult.Success)
|
||||
{
|
||||
_logger.LogError("Failed to reinitialize for offset streaming: {Error}", reinitResult.Error);
|
||||
await RecoverFromFailedRefill(seekPosition, "Failed to seek to position");
|
||||
// Guard: same single-writer discipline — only recover when we are still the active seek.
|
||||
if (IsStillActiveSeek())
|
||||
{
|
||||
await RecoverFromFailedRefill(seekPosition, "Failed to seek to position");
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogDebug("Reinit failed on superseded seek to {Position} — newer seek owns state, skipping recovery", seekPosition);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -682,7 +707,7 @@ public class StreamingAudioPlayerService : AudioPlayerService, IStreamingPlayerS
|
||||
// still the active seek — if _streamingCancellation has been replaced, a
|
||||
// newer seek is in progress and owns the flag.
|
||||
_logger.LogDebug("Seek beyond buffer cancelled");
|
||||
if (ReferenceEquals(_streamingCancellation, seekCts))
|
||||
if (IsStillActiveSeek())
|
||||
{
|
||||
IsSeekingBeyondBuffer = false;
|
||||
}
|
||||
@@ -694,7 +719,7 @@ public class StreamingAudioPlayerService : AudioPlayerService, IStreamingPlayerS
|
||||
// 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);
|
||||
if (ReferenceEquals(_streamingCancellation, seekCts))
|
||||
if (IsStillActiveSeek())
|
||||
{
|
||||
await RecoverFromFailedRefill(seekPosition, StreamingErrorHandler.GetUserFriendlyMessage(ex.Message));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user