Match capture strips: freeze video on every scrubber interaction

The point-capture and coach-review strips have a forcePause helper that
pauses the video (and re-pauses 30ms later to beat HLS.js's async
resume) whenever the slider is grabbed or scrubbed. It wasn't being
called from the frame-step buttons or the typed-timestamp commit, so
using the ± nudge to land on an exact moment while the video was
playing would happily keep the video playing and drift past the target.

Expose forcePause on both _pcbCtx and _rcbCtx and call it from:
- pcbNudge / rcbNudge (frame-step buttons)
- commitTimeInput / _rcbCommitTimeInput (typed timestamp on Enter)
- _rcbSetActiveSlot (switching start/end slot re-scrubs the video)

Playback now resumes only on an explicit play press.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
ghassan 2026-08-11 13:48:34 +03:00
parent 3e18bd2150
commit 9f82d3cf31

View File

@ -4756,6 +4756,9 @@
timeInp.value = toMinuteSecondClock(video.currentTime || 0); timeInp.value = toMinuteSecondClock(video.currentTime || 0);
return; return;
} }
// Typing a timestamp is a scrubber interaction — freeze the
// frame. Only the play button resumes playback.
forcePause();
const dur = Number(video.duration) || 0; const dur = Number(video.duration) || 0;
const clamped = Math.max(0, dur > 0 ? Math.min(dur, secs) : secs); const clamped = Math.max(0, dur > 0 ? Math.min(dur, secs) : secs);
try { video.currentTime = clamped; } catch (_) {} try { video.currentTime = clamped; } catch (_) {}
@ -4808,6 +4811,10 @@
_pcbCtx = { roundNumber, roundId, video, wrap, bar, slider, _pcbCtx = { roundNumber, roundId, video, wrap, bar, slider,
onSlide, onSlideCommit, onVideoTime, onKey, onSliderGrab, onSlide, onSlideCommit, onVideoTime, onKey, onSliderGrab,
timeInp, commitTimeInput, onTimeInputKey, timeInp, commitTimeInput, onTimeInputKey,
// Exposed so pcbNudge / commitTimeInput (defined
// outside this closure) can guarantee the video
// stays paused during any scrubber interaction.
forcePause,
existingId: existingSingle ? existingSingle.id : null, existingId: existingSingle ? existingSingle.id : null,
existingBlueId: existingBlue ? existingBlue.id : null, existingBlueId: existingBlue ? existingBlue.id : null,
existingRedId: existingRed ? existingRed.id : null }; existingRedId: existingRed ? existingRed.id : null };
@ -4991,7 +4998,11 @@
} }
function pcbNudge(deltaSeconds) { function pcbNudge(deltaSeconds) {
if (!_pcbCtx) return; if (!_pcbCtx) return;
const { video, slider } = _pcbCtx; const { video, slider, forcePause } = _pcbCtx;
// Any scrubber interaction must freeze the frame — the user is
// trying to land on a precise moment. Playback resumes only when
// they explicitly press the play button.
if (forcePause) forcePause();
// Direction only — nudge is always ±1 frame, at every zoom level // Direction only — nudge is always ±1 frame, at every zoom level
const dir = deltaSeconds >= 0 ? 1 : -1; const dir = deltaSeconds >= 0 ? 1 : -1;
const step = FRAME_STEP() * dir; const step = FRAME_STEP() * dir;
@ -5570,6 +5581,10 @@
sliderS, sliderE, fillEl, sliderS, sliderE, fillEl,
onSlideStart, onSlideEnd, onSlideCommit, onVideoTime, onKey, onSlideStart, onSlideEnd, onSlideCommit, onVideoTime, onKey,
onSliderGrabR, onSliderGrabR,
// Exposed for rcbNudge / commitStart / commitEnd so
// every scrubber interaction freezes the frame — only
// an explicit play press should resume playback.
forcePause: forcePauseR,
startInp, endInp, commitStart, commitEnd, startInp, endInp, commitStart, commitEnd,
noteInp, previewFn, overlay, noteInp, previewFn, overlay,
// Seed from existing position when editing, else default // Seed from existing position when editing, else default
@ -5686,6 +5701,9 @@
const val = document.getElementById(which === 'end' ? 'rcbEnd' : 'rcbStart').value; const val = document.getElementById(which === 'end' ? 'rcbEnd' : 'rcbStart').value;
const secs = parsePcbTimeInput(val); const secs = parsePcbTimeInput(val);
if (secs !== null && _rcbCtx && _rcbCtx.video) { if (secs !== null && _rcbCtx && _rcbCtx.video) {
// Switching the active slot re-scrubs the video — pause it
// so the user doesn't unexpectedly resume playback.
if (_rcbCtx.forcePause) _rcbCtx.forcePause();
try { _rcbCtx.video.currentTime = secs; } catch (_) {} try { _rcbCtx.video.currentTime = secs; } catch (_) {}
// Keep dual sliders in sync with the input value // Keep dual sliders in sync with the input value
if (_rcbCtx.sliderS && which === 'start') _rcbCtx.sliderS.value = secs; if (_rcbCtx.sliderS && which === 'start') _rcbCtx.sliderS.value = secs;
@ -5711,6 +5729,9 @@
inp.value = toMinuteSecondClock(_rcbCtx.video.currentTime || 0); inp.value = toMinuteSecondClock(_rcbCtx.video.currentTime || 0);
return; return;
} }
// Typing a timestamp is a scrubber interaction — freeze the
// frame. Playback resumes only on an explicit play press.
if (_rcbCtx.forcePause) _rcbCtx.forcePause();
const dur = Number(_rcbCtx.video.duration) || 0; const dur = Number(_rcbCtx.video.duration) || 0;
const clamped = Math.max(0, dur > 0 ? Math.min(dur, secs) : secs); const clamped = Math.max(0, dur > 0 ? Math.min(dur, secs) : secs);
inp.value = toMinuteSecondClock(clamped); inp.value = toMinuteSecondClock(clamped);
@ -5787,7 +5808,10 @@
// Nudge the LAST-TOUCHED scrubber (start or end) by ±1 frame. // Nudge the LAST-TOUCHED scrubber (start or end) by ±1 frame.
function rcbNudge(deltaSeconds) { function rcbNudge(deltaSeconds) {
if (!_rcbCtx) return; if (!_rcbCtx) return;
const { video, sliderS, sliderE, startInp, endInp } = _rcbCtx; const { video, sliderS, sliderE, startInp, endInp, forcePause } = _rcbCtx;
// Freeze the frame — scrubber steps are for precise landing,
// not incidental resume-playback triggers.
if (forcePause) forcePause();
const which = (typeof _rcbCtxLastTouched === 'function' ? _rcbCtxLastTouched() : 'start'); const which = (typeof _rcbCtxLastTouched === 'function' ? _rcbCtxLastTouched() : 'start');
const target = which === 'end' ? sliderE : sliderS; const target = which === 'end' ? sliderE : sliderS;
const otherVal = Number(which === 'end' ? sliderS.value : sliderE.value) || 0; const otherVal = Number(which === 'end' ? sliderS.value : sliderE.value) || 0;