Conversation
#resetPlaybackIfNeeded dispatches seekableStart() (absolute) to media-seek-request, but since 1.14.0 the seek handler treats the detail as clip-relative and boundTime() adds clipStartTime back. This double- adds clipStartTime, jumping clipped players to ~ clip end on play() after a clip ends or when realCurrentTime < seekableStart. Pass 0 instead — boundTime's isStart clamp resolves 0 + clipStartTime (<= seekableStart) to seekableStart(), the intended target. Safe for all video types: clipped on-demand (-> clipStartTime), non-clipped (-> 0), live DVR (-> DVR window start). Fixes the auto-advance / replay-after-clip-end regression that appeared in 1.14.0 (maverick 0.44.1) when currentTime became clip-relative. This contribution was created with the support of GLM-5.2.
|
Correction to my explanation above: The relevant change was #1824. The previous boundary check happened to accept the absolute I rechecked the published packages. For a clip spanning 300–600 seconds, The proposed fix still looks correct. My explanation of why it became necessary was wrong. The Maverick attribution in #1843 was also stronger than the evidence supported. This re-check verifies the boundary calculation, not a fresh full-browser regression run. |
What
Fixes #1858
Fix
#resetPlaybackIfNeededpassing the absoluteseekableStart()value to the clip-relativemedia-seek-requesthandler.if (shouldReset) { this.dispatch('media-seek-request', { - detail: seekableStart(), + detail: 0, trigger, }); }Corrected explanation
currentTimeand seek requests were already clip-relative in 1.12.13. They also already passed throughboundTime().The relevant change was #1824. The previous boundary check happened to accept the absolute
seekableStart()value passed by#resetPlaybackIfNeeded. The revised check exposed that incorrect argument.I rechecked the published packages. For a clip spanning 300–600 seconds,
boundTime(300)returns 300 in 1.12.13 and 1.13.0, but 600 in 1.14.0 and 1.15.6. Passing0returns the intended 300 in the affected versions.The proposed fix still looks correct. My explanation of why it became necessary was wrong. The Maverick attribution in #1843 was also stronger than the evidence supported.
Verification
I added eight regression tests covering clipped replay, playback before the clip start or near its end, playback within a clip, unclipped replay, and live-DVR window resets. They exercise the state manager and the real seek handler with a mocked provider. Five fail with the original
seekableStart()argument. All eight pass with0.I also tested a standalone MP4 player in Chrome 153 and Safari 27, without Rails or Stimulus. For a clip spanning 30–34 seconds, replay jumps to 34 without the fix. With the fix, it seeks to 30 and plays the four-second clip again.
Local checks passed on Node 22.22.3 and pnpm 8.7.0:
pnpm format:checkpnpm typecheckpnpm buildpnpm test, 71 tests passedThe build emits the same
fscreenresolution and Remotion dynamic-import warnings with and without the fix.These browser checks cover replay after the clip has stopped. An immediate restart from a
pausecallback hit a separate race in Safari, where an end notification stopped the restarted playback. This change does not address that case.Reproduction
Save this as
packages/vidstack/sandbox/clip-replay.htmlin a checkout of this PR:Run
pnpm install --frozen-lockfile, thenpnpm --filter vidstack exec vite --port 3101. Openhttp://localhost:3101/sandbox/clip-replay.html, play the clip, wait until it stops, and click the button again.To reproduce the failure, change
detail: 0back todetail: seekableStart()in#resetPlaybackIfNeededand reload.This contribution was created with the support of GLM-5.2.