From 8b94a5fdf718a7cac72390f3569408e6dbfbd0f2 Mon Sep 17 00:00:00 2001 From: daniel-c-harvey Date: Wed, 10 Jun 2026 14:30:12 -0400 Subject: [PATCH] fix: assign seek CTS synchronously and guard load finally to stop seek/load race --- .../Services/StreamingAudioPlayerService.cs | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/DeepDrftPublic.Client/Services/StreamingAudioPlayerService.cs b/DeepDrftPublic.Client/Services/StreamingAudioPlayerService.cs index 8ccd5e3..7568df8 100644 --- a/DeepDrftPublic.Client/Services/StreamingAudioPlayerService.cs +++ b/DeepDrftPublic.Client/Services/StreamingAudioPlayerService.cs @@ -184,7 +184,12 @@ public class StreamingAudioPlayerService : AudioPlayerService, IStreamingPlayerS finally { IsLoading = false; - await NotifyStateChanged(); + // 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(); + } } } @@ -442,11 +447,21 @@ public class StreamingAudioPlayerService : AudioPlayerService, IStreamingPlayerS // OperationCanceledException asynchronously; if we kick off a new loop // immediately, both can race against the single-instance JS StreamDecoder // and corrupt decode state. Draining here is the load-bearing guarantee. - _streamingCancellation?.Cancel(); - await DrainActiveStreamingTaskAsync(); - _streamingCancellation?.Dispose(); + // + // Invariant: any caller that supersedes a load WITHOUT wanting the load's + // 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(); _streamingCancellation = seekCts; + oldCts?.Cancel(); + await DrainActiveStreamingTaskAsync(); + oldCts?.Dispose(); try {