fix(core): a seek during playback lands a playing video on the new time (#4467)

* fix(core): a seek during playback lands a playing video on the new time

* fix(core): the seek fix passes the comment ratchet without a new comment

* fix(core): keep why a pausing seek still lands, folded into the paragraph above

---------

Co-authored-by: Miguel Angel Simon Sierra <miguelangelsisi098@gmail.com>
This commit is contained in:
Miguel Ángel
2026-09-25 09:48:57 +00:00
committed by GitHub
co-authored by Miguel Angel Simon Sierra
parent 93df8a814d
commit 2455a3d0ee
2 changed files with 25 additions and 7 deletions
+19
View File
@@ -1628,6 +1628,25 @@ describe("syncRuntimeMedia", () => {
expect(clip.el.currentTime).toBe(5);
});
// A seek while playing pauses and syncs in one pass, before the video element has paused.
it("a seek that pauses mid-playback lands a lagging playing video on the new time", () => {
const clip = createMockClip({ start: 3.85, end: 5.6, duration: 1.75 });
Object.defineProperty(clip.el, "paused", { value: false, writable: true });
Object.defineProperty(clip.el, "currentTime", { value: 0.031, writable: true });
syncRuntimeMedia({ clips: [clip], timeSeconds: 4.109, playing: true, playbackRate: 1 });
expect(clip.el.currentTime).toBe(0.031);
syncRuntimeMedia({
clips: [clip],
timeSeconds: 4.4,
playing: false,
playbackRate: 1,
forceSync: true,
});
expect(clip.el.currentTime).toBeCloseTo(0.55, 5);
expect(clip.el.pause).toHaveBeenCalled();
});
it("mutes when either outputMuted OR userMuted is true (OR invariant)", () => {
// Explicit validation of the combined-flag contract: setting one to
// false while the other is true must keep the element muted.
+6 -7
View File
@@ -461,13 +461,12 @@ export function syncRuntimeMedia(params: {
(el.ended && canSeekEndedMediaBackward && drift > 0.001) ||
staleAudioOnFirstTick ||
(drift > 0.5 && (firstTickOfClip || offsetJumped || catastrophicDrift));
// Playing video elements use the browser's native decoder pipeline for
// timing. Seeking a playing video resets the decoder, causing a ~150ms
// freeze while it re-buffers — during which the monotonic clock advances,
// creating a perpetual seek→freeze→drift→seek stutter loop. Skip strict
// and force sync for playing videos; only hard sync (>0.5s) warrants
// the decoder-reset cost.
const isPlayingVideo = el.tagName === "VIDEO" && !el.paused;
// Playing videos use the browser's decoder for timing. Seeking one resets the decoder: a
// ~150ms freeze while it re-buffers, as the monotonic clock advances, which loops into a
// seek→freeze→drift→seek stutter. So a playing video skips strict and force sync; only hard
// sync (>0.5s) warrants the decoder-reset cost. A paused transport pauses this video below,
// so a seek that pauses mid-playback still lands it.
const isPlayingVideo = el.tagName === "VIDEO" && !el.paused && params.playing;
// Only apply strict sync when offset has stabilized (not growing).
// During initial buffering, offset grows ~16ms/tick as the timeline
// advances while media stays at 0. Accumulated drift from pause/play