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:
@@ -295,15 +295,22 @@ public class StreamingAudioPlayerService : AudioPlayerService, IStreamingPlayerS
|
|||||||
{
|
{
|
||||||
await RecoverFromFailedRefill(CurrentTime, userError);
|
await RecoverFromFailedRefill(CurrentTime, userError);
|
||||||
}
|
}
|
||||||
else
|
else if (ReferenceEquals(_streamingCancellation, loadCts))
|
||||||
{
|
{
|
||||||
// First-segment failure (nothing buffered / playing yet) or superseded load: the
|
// First-segment failure (nothing buffered / playing yet), still the active operation:
|
||||||
// normal unload-to-error path is correct — no buffered tail to halt.
|
// the normal unload-to-error path is correct — nothing is in the scheduler to halt.
|
||||||
ErrorMessage = userError;
|
ErrorMessage = userError;
|
||||||
LoadProgress = 0;
|
LoadProgress = 0;
|
||||||
IsLoaded = false;
|
IsLoaded = false;
|
||||||
IsStreamingMode = 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
|
finally
|
||||||
{
|
{
|
||||||
@@ -879,13 +886,16 @@ public class StreamingAudioPlayerService : AudioPlayerService, IStreamingPlayerS
|
|||||||
_logger.LogWarning("Refill-failure recovery interop did not succeed: {Error}", recovered.Error);
|
_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.
|
// 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
|
// and still in streaming mode. IsLoaded = true and IsStreamingMode = true are both load-bearing —
|
||||||
// the seek or pick another track; resetting to unloaded would evict the track identity.
|
// 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;
|
ErrorMessage = userFacingError;
|
||||||
IsPlaying = false;
|
IsPlaying = false;
|
||||||
IsPaused = true;
|
IsPaused = true;
|
||||||
IsLoaded = true;
|
IsLoaded = true;
|
||||||
|
IsStreamingMode = true;
|
||||||
CurrentTime = seekPosition;
|
CurrentTime = seekPosition;
|
||||||
IsSeekingBeyondBuffer = false;
|
IsSeekingBeyondBuffer = false;
|
||||||
await NotifyStateChanged();
|
await NotifyStateChanged();
|
||||||
|
|||||||
@@ -353,6 +353,8 @@ public class SegmentedStreamLoopTests
|
|||||||
"a truncated segment while cursor < totalLength is a failure: scheduler must be halted via recovery");
|
"a truncated segment while cursor < totalLength is a failure: scheduler must be halted via recovery");
|
||||||
Assert.That(player.IsLoaded, Is.True,
|
Assert.That(player.IsLoaded, Is.True,
|
||||||
"recovery leaves the track loaded so the listener can retry — not torn down to unloaded");
|
"recovery leaves the track loaded so the listener can retry — not torn down to unloaded");
|
||||||
|
Assert.That(player.IsStreamingMode, Is.True,
|
||||||
|
"recovery must restore IsStreamingMode=true so Seek() is not wedged (AC6 / Phase 21.3 retry contract)");
|
||||||
Assert.That(player.IsPaused, Is.True,
|
Assert.That(player.IsPaused, Is.True,
|
||||||
"recovery settles into a paused state, not playing");
|
"recovery settles into a paused state, not playing");
|
||||||
Assert.That(player.ErrorMessage, Is.Not.Null.And.Not.Empty,
|
Assert.That(player.ErrorMessage, Is.Not.Null.And.Not.Empty,
|
||||||
@@ -381,6 +383,8 @@ public class SegmentedStreamLoopTests
|
|||||||
"a mid-stream fetch failure must halt the scheduler via recovery, not leave it to drain");
|
"a mid-stream fetch failure must halt the scheduler via recovery, not leave it to drain");
|
||||||
Assert.That(player.IsLoaded, Is.True,
|
Assert.That(player.IsLoaded, Is.True,
|
||||||
"recovery leaves the track loaded so the listener can retry — not torn down to unloaded");
|
"recovery leaves the track loaded so the listener can retry — not torn down to unloaded");
|
||||||
|
Assert.That(player.IsStreamingMode, Is.True,
|
||||||
|
"recovery must restore IsStreamingMode=true so Seek() is not wedged (AC6 / Phase 21.3 retry contract)");
|
||||||
Assert.That(player.IsPaused, Is.True,
|
Assert.That(player.IsPaused, Is.True,
|
||||||
"recovery settles into a paused state, not playing");
|
"recovery settles into a paused state, not playing");
|
||||||
Assert.That(player.ErrorMessage, Is.Not.Null.And.Not.Empty,
|
Assert.That(player.ErrorMessage, Is.Not.Null.And.Not.Empty,
|
||||||
|
|||||||
Reference in New Issue
Block a user