fix: assign seek CTS synchronously and guard load finally to stop seek/load race

This commit is contained in:
daniel-c-harvey
2026-06-10 14:30:12 -04:00
parent fb27918ed6
commit 8b94a5fdf7
@@ -184,9 +184,14 @@ public class StreamingAudioPlayerService : AudioPlayerService, IStreamingPlayerS
finally finally
{ {
IsLoading = false; IsLoading = false;
// Only notify if this load is still the active operation. A superseding seek
// owns state notifications; firing here mid-seek would push a stale snapshot.
if (ReferenceEquals(_streamingCancellation, loadCts))
{
await NotifyStateChanged(); await NotifyStateChanged();
} }
} }
}
/// <summary> /// <summary>
/// Fetches and decodes the track's waveform loudness profile, then notifies state so the /// Fetches and decodes the track's waveform loudness profile, then notifies state so the
@@ -442,11 +447,21 @@ public class StreamingAudioPlayerService : AudioPlayerService, IStreamingPlayerS
// OperationCanceledException asynchronously; if we kick off a new loop // OperationCanceledException asynchronously; if we kick off a new loop
// immediately, both can race against the single-instance JS StreamDecoder // immediately, both can race against the single-instance JS StreamDecoder
// and corrupt decode state. Draining here is the load-bearing guarantee. // and corrupt decode state. Draining here is the load-bearing guarantee.
_streamingCancellation?.Cancel(); //
await DrainActiveStreamingTaskAsync(); // Invariant: any caller that supersedes a load WITHOUT wanting the load's
_streamingCancellation?.Dispose(); // state reset must assign its own CTS to _streamingCancellation *before*
// its first await. LoadTrackStreaming's OCE continuation fires during the
// drain await on the shared _activeStreamingTask; it resets IsLoaded/
// IsStreamingMode only when _streamingCancellation still equals its loadCts.
// Assigning seekCts synchronously here makes that identity check fail, so
// the seek's state survives. (ResetToIdle deliberately does NOT do this —
// it wants the reset, and nulls _streamingCancellation only after the drain.)
var oldCts = _streamingCancellation;
var seekCts = new CancellationTokenSource(); var seekCts = new CancellationTokenSource();
_streamingCancellation = seekCts; _streamingCancellation = seekCts;
oldCts?.Cancel();
await DrainActiveStreamingTaskAsync();
oldCts?.Dispose();
try try
{ {