Restore IsStreamingMode on recovery; guard superseded-load else-branch

RecoverFromFailedRefill now sets IsStreamingMode=true so the in-place
seek-retry route isn't wedged. The generic-catch unload path is gated on
the loadCts identity, so a superseded load no longer clobbers a newer
operation's state.
This commit is contained in:
daniel-c-harvey
2026-06-24 15:37:38 -04:00
parent e7762e35e8
commit cc9d20184d
2 changed files with 20 additions and 6 deletions
@@ -295,15 +295,22 @@ public class StreamingAudioPlayerService : AudioPlayerService, IStreamingPlayerS
{
await RecoverFromFailedRefill(CurrentTime, userError);
}
else
else if (ReferenceEquals(_streamingCancellation, loadCts))
{
// First-segment failure (nothing buffered / playing yet) or superseded load: the
// normal unload-to-error path is correct — no buffered tail to halt.
// First-segment failure (nothing buffered / playing yet), still the active operation:
// the normal unload-to-error path is correct — nothing is in the scheduler to halt.
ErrorMessage = userError;
LoadProgress = 0;
IsLoaded = false;
IsStreamingMode = false;
}
else
{
// Superseded load: a newer seek (or track switch) has already claimed _streamingCancellation
// and owns all shared state. Writing IsLoaded/IsStreamingMode here would corrupt the live
// operation — mirror the OCE catch's identity guard and do nothing to shared state.
_logger.LogDebug("Generic throw on superseded load for track {TrackId} — newer operation owns state, skipping unload", track.EntryKey);
}
}
finally
{
@@ -879,13 +886,16 @@ public class StreamingAudioPlayerService : AudioPlayerService, IStreamingPlayerS
_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.
// IsLoaded = true is load-bearing — the "paused-but-loaded" contract lets the listener retry
// the seek or pick another track; resetting to unloaded would evict the track identity.
// Settle C# into the matching recoverable state: not playing, paused at the target, still loaded
// and still in streaming mode. IsLoaded = true and IsStreamingMode = true are both load-bearing —
// the "paused-but-loaded" contract lets the listener retry the seek (Seek early-returns when
// !IsLoaded || !IsStreamingMode), resume via TogglePlayPause, or pick another track. Resetting
// either to false would wedge at least one of the three retry routes (AC6 / Phase 21.3).
ErrorMessage = userFacingError;
IsPlaying = false;
IsPaused = true;
IsLoaded = true;
IsStreamingMode = true;
CurrentTime = seekPosition;
IsSeekingBeyondBuffer = false;
await NotifyStateChanged();