From 335bc39b5b1b8f17d37f390189c7170135a4682b Mon Sep 17 00:00:00 2001 From: Daniel Saewitz Date: Sat, 19 Sep 2026 14:59:08 -0400 Subject: [PATCH 1/4] Fix SEEK events dropped in idle/loading track states MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The track machine's idle and loading states had no SEEK handler, so when seekCurrentToStartTime fired during gotoTrack, the SEEK event was silently dropped — pausedAtTrackTime was set correctly but audio.currentTime stayed at 0, causing playback to start from the beginning instead of the target. - Add SEEK → seekHtml5 handler to idle and loading states so the HTML5 element actually seeks during the GOTO phase before PLAY fires. - Defense-in-depth: _playHtml5() now checks if audio.currentTime diverges from pausedAtTrackTime by >0.5s and seeks before calling play(), handling both metadata-ready and not-yet-loaded cases. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/Track.ts | 10 ++++++++++ src/machines/track.machine.ts | 6 ++++++ 2 files changed, 16 insertions(+) diff --git a/src/Track.ts b/src/Track.ts index 2e1c61f..5321e89 100644 --- a/src/Track.ts +++ b/src/Track.ts @@ -577,6 +577,16 @@ export class Track { private _playHtml5(): void { if (this.audio.preload !== 'auto') this.audio.preload = 'auto'; this.audio.playbackRate = this.queueRef.playbackRate; + if (isFinite(this.pausedAtTrackTime) && Math.abs(this.audio.currentTime - this.pausedAtTrackTime) > 0.5) { + if (this.audio.readyState >= HTMLMediaElement.HAVE_METADATA) { + this.audio.currentTime = this.pausedAtTrackTime; + } else { + const target = this.pausedAtTrackTime; + this.audio.addEventListener('loadedmetadata', () => { + this.audio.currentTime = target; + }, { once: true }); + } + } const promise = this.audio.play(); if (promise) { promise.catch((err: unknown) => { diff --git a/src/machines/track.machine.ts b/src/machines/track.machine.ts index b5396aa..b97b041 100644 --- a/src/machines/track.machine.ts +++ b/src/machines/track.machine.ts @@ -177,6 +177,9 @@ export function createTrackMachine(initialContext: TrackContext) { // ----------------------------------------------------------------- idle: { on: { + SEEK: { + actions: ['seekHtml5'], + }, HTML5_ENDED: { actions: ['notifyTrackEnded'], }, @@ -340,6 +343,9 @@ export function createTrackMachine(initialContext: TrackContext) { // ----------------------------------------------------------------- loading: { on: { + SEEK: { + actions: ['seekHtml5'], + }, BUFFER_LOADING: { actions: 'setLoadingState', }, From 26ed48da3f27ae5f9557236830c8a3bb44cadb90 Mon Sep 17 00:00:00 2001 From: Daniel Saewitz Date: Sat, 19 Sep 2026 15:02:04 -0400 Subject: [PATCH 2/4] Add reportProgress to idle/loading SEEK handlers Matches the html5 and webaudio states so UI consumers get notified of the new seek position immediately. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/machines/track.machine.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/machines/track.machine.ts b/src/machines/track.machine.ts index b97b041..5fcfa72 100644 --- a/src/machines/track.machine.ts +++ b/src/machines/track.machine.ts @@ -178,7 +178,7 @@ export function createTrackMachine(initialContext: TrackContext) { idle: { on: { SEEK: { - actions: ['seekHtml5'], + actions: ['seekHtml5', 'reportProgress'], }, HTML5_ENDED: { actions: ['notifyTrackEnded'], @@ -344,7 +344,7 @@ export function createTrackMachine(initialContext: TrackContext) { loading: { on: { SEEK: { - actions: ['seekHtml5'], + actions: ['seekHtml5', 'reportProgress'], }, BUFFER_LOADING: { actions: 'setLoadingState', From 6dc74d33030a415444624a88b1197fa3b79ad1f6 Mon Sep 17 00:00:00 2001 From: Daniel Saewitz Date: Sat, 19 Sep 2026 15:06:35 -0400 Subject: [PATCH 3/4] Fix _playHtml5 defense to only seek forward MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pausedAtTrackTime is not updated on HTML5 pause, so the previous Math.abs check would seek backward on normal resume (e.g. pausedAt=0 after 30s of playback). Changed to only trigger when pausedAtTrackTime is ahead of audio.currentTime — the actual GOTO scenario where the SEEK event was dropped. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/Track.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Track.ts b/src/Track.ts index 5321e89..959248f 100644 --- a/src/Track.ts +++ b/src/Track.ts @@ -577,7 +577,7 @@ export class Track { private _playHtml5(): void { if (this.audio.preload !== 'auto') this.audio.preload = 'auto'; this.audio.playbackRate = this.queueRef.playbackRate; - if (isFinite(this.pausedAtTrackTime) && Math.abs(this.audio.currentTime - this.pausedAtTrackTime) > 0.5) { + if (isFinite(this.pausedAtTrackTime) && this.pausedAtTrackTime > this.audio.currentTime + 0.5) { if (this.audio.readyState >= HTMLMediaElement.HAVE_METADATA) { this.audio.currentTime = this.pausedAtTrackTime; } else { From 49be3df008e385d45bffa9640bf87650c7db8618 Mon Sep 17 00:00:00 2001 From: Daniel Saewitz Date: Sat, 19 Sep 2026 15:17:53 -0400 Subject: [PATCH 4/4] Skip seekHtml5 in idle/loading SEEK for WEBAUDIO_ONLY mode In WEBAUDIO_ONLY mode, calling seekHtml5 triggers audio.load() which initiates an unnecessary HTML5 request and can fire spurious onerror. pausedAtTrackTime is already set by seek(), and startSourceNode reads it on PLAY, so the HTML5 element doesn't need to be touched. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/machines/track.machine.ts | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/src/machines/track.machine.ts b/src/machines/track.machine.ts index 5fcfa72..dff034b 100644 --- a/src/machines/track.machine.ts +++ b/src/machines/track.machine.ts @@ -177,9 +177,15 @@ export function createTrackMachine(initialContext: TrackContext) { // ----------------------------------------------------------------- idle: { on: { - SEEK: { - actions: ['seekHtml5', 'reportProgress'], - }, + SEEK: [ + { + guard: 'isWebAudioOnly', + actions: ['reportProgress'], + }, + { + actions: ['seekHtml5', 'reportProgress'], + }, + ], HTML5_ENDED: { actions: ['notifyTrackEnded'], }, @@ -343,9 +349,15 @@ export function createTrackMachine(initialContext: TrackContext) { // ----------------------------------------------------------------- loading: { on: { - SEEK: { - actions: ['seekHtml5', 'reportProgress'], - }, + SEEK: [ + { + guard: 'isWebAudioOnly', + actions: ['reportProgress'], + }, + { + actions: ['seekHtml5', 'reportProgress'], + }, + ], BUFFER_LOADING: { actions: 'setLoadingState', },