From c8db41a5f5768a06e414acdd99fb5be7979b93c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Thu, 16 Apr 2026 21:11:16 +0200 Subject: [PATCH] fix(runtime,studio): silent-first-play, pause/play stutter, cold-play word-skipping MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three related audio-sync defects in the studio preview, plus a small UX improvement. 1. Silent/very-late first play on slow-loading audio `syncRuntimeMedia`'s old flow, when it met `readyState < HAVE_FUTURE_DATA`, called `el.load()` and attached a `canplay` listener to retry `play()`. Two real problems, neither of which were "lost user activation" (the media sync runs from a 50 ms `setInterval`, well outside any gesture window): - `bindMediaMetadataListeners` already sets `preload="auto"` and calls `el.load()` at runtime init, kicking off a fetch the moment a media element is discovered. The sync's duplicate `el.load()` aborts that in-flight fetch and restarts from zero — on slow networks that delays playback by seconds, which users perceive as silent until a second click. - The `canplay` listener was racy: the event can fire between `load()` and `addEventListener`, leaving the element wedged waiting for a callback that never arrives. `HTMLMediaElement.play()` is already spec'd to queue playback and resolve once data arrives, so we can unconditionally call it and let the browser handle buffering. Drop the `readyState` gate, the extra `load()`, and the `canplay` listener. 2. Audible stutter on rapid pause/play The 0.3 s drift-seek threshold fired on nearly every toggle because pause/play ordering between the timeline and the media element produces 0.1–0.4 s of transient drift. Each forced `el.currentTime = relTime` drops `readyState` and surfaces as a `waiting` event the user hears as a stutter. Threshold raised to 0.5 s. 3. Skipped words on cold first play Even with a 0.5 s threshold, drift still grew past 0.5 s during initial buffering because the timeline advances while the audio element is stuck at `currentTime = 0`. The old logic would then force-seek audio forward and the user would miss the opening of the narration. The fix distinguishes drift that grows *gradually* (buffer catch-up — offset moves ~16 ms per tick) from drift that *jumps* in one tick (a scrub). Only jumps, or the first tick a clip becomes active, or catastrophic drift (>3 s) trigger a resync. Inline tradeoff note: for strictly lip-synced dialogue a future tightening would be to track time-since-last-transition and drop the threshold to ~0.15 s outside a 500 ms toggle window. 4. "Loading assets…" overlay in the studio preview Spinner while every timed `