From daa334a94727539a2c51e4c90125199ea2935c58 Mon Sep 17 00:00:00 2001 From: daniel-c-harvey Date: Sun, 7 Jun 2026 15:02:34 -0400 Subject: [PATCH] fix: seek lower-bound guard and pointer-down callback ordering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AudioPlayer.ts: route seeks below bufferStart to seekBeyondBuffer; previous missing lower-bound caused clamped playback after first seek. WaveformSeeker: fire OnSeekStart/OnSeekChange before capturePointer await to prevent fast-click race that locked _isSeeking true. Latent: WavOffsetService encodes remaining-only DataSize, overwriting JS this.duration after seek — not fixed here, scope separately. --- .../AudioPlayerBar/WaveformSeeker.razor.cs | 20 +++++++++++-------- DeepDrftPublic/Interop/audio/AudioPlayer.ts | 14 ++++++++----- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/DeepDrftPublic.Client/Controls/AudioPlayerBar/WaveformSeeker.razor.cs b/DeepDrftPublic.Client/Controls/AudioPlayerBar/WaveformSeeker.razor.cs index 2341c53..753b161 100644 --- a/DeepDrftPublic.Client/Controls/AudioPlayerBar/WaveformSeeker.razor.cs +++ b/DeepDrftPublic.Client/Controls/AudioPlayerBar/WaveformSeeker.razor.cs @@ -183,12 +183,20 @@ public partial class WaveformSeeker : ComponentBase, IAsyncDisposable { if (!CanSeek) return; - // Set seeking state BEFORE the async capturePointer so a fast mobile tap that fires - // pointerup before capturePointer returns doesn't miss the commit. _isSeeking = true; _seekFraction = FractionFromOffset(e.OffsetX); - // Capture the pointer so a drag that leaves the element keeps tracking until release. + // Fire seek-start notifications BEFORE awaiting capturePointer. In Blazor WASM a JS + // interop await yields to the browser event loop. A fast click can fire pointerup + // during that window: HandlePointerUp runs (OnSeekEnd, _isSeeking = false), then + // HandlePointerDown resumes and calls OnSeekStart (_isSeeking stuck true, display + // frozen). Notifying first ensures the ordering is always Start → End, never End → Start. + if (Duration is not > 0) { _isSeeking = false; return; } + await OnSeekStart.InvokeAsync(); + await OnSeekChange.InvokeAsync(_seekFraction * Duration.Value); + + // Capture AFTER seek-start is notified so a fast pointerup cannot reorder + // OnSeekEnd before OnSeekStart in AudioPlayerBar. if (_jsModule is not null) { try @@ -197,13 +205,9 @@ public partial class WaveformSeeker : ComponentBase, IAsyncDisposable } catch { - // Capture is a UX nicety; if it fails the gesture still works within the element bounds. + // Capture is a UX nicety; gesture still works within element bounds. } } - - if (Duration is not > 0) { _isSeeking = false; return; } - await OnSeekStart.InvokeAsync(); - await OnSeekChange.InvokeAsync(_seekFraction * Duration.Value); } private async Task HandlePointerMove(PointerEventArgs e) diff --git a/DeepDrftPublic/Interop/audio/AudioPlayer.ts b/DeepDrftPublic/Interop/audio/AudioPlayer.ts index 8e2dbf8..22d8c8a 100644 --- a/DeepDrftPublic/Interop/audio/AudioPlayer.ts +++ b/DeepDrftPublic/Interop/audio/AudioPlayer.ts @@ -274,14 +274,18 @@ export class AudioPlayer { return { success: false, error: 'Invalid seek position' }; } - // Get buffered duration (accounting for playback offset) - const bufferedDuration = this.scheduler.getTotalDuration() + this.scheduler.getPlaybackOffset(); + const bufferStart = this.scheduler.getPlaybackOffset(); + const bufferEnd = this.scheduler.getTotalDuration() + bufferStart; - // Check if seeking within buffered content - if (position <= bufferedDuration) { + // Position must be within [bufferStart, bufferEnd] to use buffered content. + // A lower-bound check is required: after a seek-beyond-buffer, bufferStart is + // set to the prior seek position. Seeking to a position below bufferStart would + // produce a negative bufferRelativePosition in seekWithinBuffer, silently + // clamping to position 0 of the offset buffer instead of the requested time. + if (position >= bufferStart && position <= bufferEnd) { return this.seekWithinBuffer(position); } else { - // Seeking beyond buffer - signal C# to fetch new stream + // Seeking outside buffered window - signal C# to fetch new stream return this.seekBeyondBuffer(position); } }