fix: guard LoadTrackStreaming OCE catch with loadCts identity so an in-flight seek isn't clobbered mid-load
This commit is contained in:
@@ -92,13 +92,17 @@ public class StreamingAudioPlayerService : AudioPlayerService, IStreamingPlayerS
|
||||
// track while it's still loading, not only after playback starts.
|
||||
CurrentTrack = track;
|
||||
|
||||
// Create new cancellation token for this streaming operation
|
||||
_streamingCancellation = new CancellationTokenSource();
|
||||
// Create new cancellation token for this streaming operation. Capture it in a local
|
||||
// so the catch/finally can compare identity against _streamingCancellation: a seek
|
||||
// replaces _streamingCancellation with its own seekCts before this load's continuation
|
||||
// resumes on the single-threaded WASM dispatcher, and we must not clobber the seek's state.
|
||||
var loadCts = new CancellationTokenSource();
|
||||
_streamingCancellation = loadCts;
|
||||
|
||||
// Fetch the waveform profile alongside the audio. Fire-and-forget against the same
|
||||
// streaming token so a track switch abandons it; it only updates display state and must
|
||||
// never gate or fail the audio load (a missing profile yields the flat-seekbar fallback).
|
||||
_ = LoadWaveformProfileAsync(track.EntryKey, _streamingCancellation.Token);
|
||||
_ = LoadWaveformProfileAsync(track.EntryKey, loadCts.Token);
|
||||
|
||||
try
|
||||
{
|
||||
@@ -119,7 +123,7 @@ public class StreamingAudioPlayerService : AudioPlayerService, IStreamingPlayerS
|
||||
var mediaResult = await _trackMediaClient.GetTrackMedia(
|
||||
track.EntryKey,
|
||||
byteOffset: 0,
|
||||
cancellationToken: _streamingCancellation.Token);
|
||||
cancellationToken: loadCts.Token);
|
||||
if (!mediaResult.Success)
|
||||
{
|
||||
var technicalError = mediaResult.GetMessage();
|
||||
@@ -150,15 +154,24 @@ public class StreamingAudioPlayerService : AudioPlayerService, IStreamingPlayerS
|
||||
return;
|
||||
}
|
||||
|
||||
_activeStreamingTask = StreamAudioWithEarlyPlayback(audio, _streamingCancellation.Token);
|
||||
_activeStreamingTask = StreamAudioWithEarlyPlayback(audio, loadCts.Token);
|
||||
await _activeStreamingTask;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
catch (OperationCanceledException) when (loadCts.IsCancellationRequested)
|
||||
{
|
||||
// Cancellation is expected, reset state
|
||||
// Cancellation is expected when this load was superseded (track switch or seek).
|
||||
// The when filter ensures HttpClient timeout OCEs — where loadCts was NOT
|
||||
// cancelled — fall through to the error handler below instead of being swallowed.
|
||||
_logger.LogDebug("Audio streaming cancelled for track {TrackId}", track.EntryKey);
|
||||
IsLoaded = false;
|
||||
IsStreamingMode = false;
|
||||
|
||||
// Only reset streaming state if this load is still the active operation. A seek
|
||||
// in flight has already replaced _streamingCancellation with its own seekCts and
|
||||
// owns IsLoaded/IsStreamingMode; clobbering them here corrupts the seek mid-flight.
|
||||
if (ReferenceEquals(_streamingCancellation, loadCts))
|
||||
{
|
||||
IsLoaded = false;
|
||||
IsStreamingMode = false;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user