Skip to content

fix(slideshow): make slide holds deterministic (no auto-progress)#1596

Open
vanceingalls wants to merge 2 commits into
mainfrom
slideshow-deterministic-hold
Open

fix(slideshow): make slide holds deterministic (no auto-progress)#1596
vanceingalls wants to merge 2 commits into
mainfrom
slideshow-deterministic-hold

Conversation

@vanceingalls

Copy link
Copy Markdown
Collaborator

Follow-up to #1585 / #1594 (both merged). Fixes the intermittent auto-progress flakiness in slideshow presenter mode.

Root cause

playTo() played the player and relied on being paused at holdAt = target + 0.2s (~6 frames at 30fps). If a timeupdate was coarse or missed, playback overran that window — sometimes into the next fragment or scene. That's the flaky auto-progress.

Fix

Bound the repaint to a single frame: seek to the exact target, play, and pause on the first timeupdate at/after the target.

  • timeupdate fires only from the playback clock (never on seek), so the first tick is guaranteed to be a genuine painted frame — one tick forces the repaint a bare paused seek can't produce…
  • …but it physically can't overrun: there's no fixed time window for a coarse/missed update to run past.
  • Pause runs before the change handlers, so no second tick advances the playhead.

Verification (headless)

  • Slide holds with currentTime stable over 6s (paused, no drift).
  • Every next lands and holds: 1/11 → 2/11 (frag0 @9.3) → 2/11 (frag1 @9.6) → 3/11 @22.5 — each still stable after a 2.5s wait.
  • 106 player tests pass; tsc / oxlint / oxfmt clean.

One file (SlideshowController.ts); removes the now-unused RENDER_NUDGE constant.

🤖 Generated with Claude Code

playTo() played the player and relied on being paused at holdAt = target + 0.2s
(~6 frames). If a timeupdate was coarse or missed, playback overran the hold —
sometimes into the next fragment/scene, the intermittent auto-progress.

Bound the repaint to a single frame instead: seek to the exact target, play, and
pause on the FIRST timeupdate at/after the target. timeupdate only fires from the
playback clock (never on seek), so the first tick is a genuine painted frame —
one tick forces a repaint but can't overrun. Pause before the change handlers so
no second tick advances the playhead.

Verified headless: slide holds with currentTime stable over 6s; every next
lands and holds (1→2 frag0→2 frag1→3), no drift. 106 player tests pass.
Supersedes the single-frame play-nudge: that left a backgrounded audience
window playing (waiting for a throttled timeupdate to pause it) → drift and the
'one side doesn't move' desync.

- SlideshowController: navigation is now a pure synchronous seek (no sustained
  playback, no auto-progress). fragmentIndex is advanced by the caller, not on a
  played tick. Removed holdAt/holdTarget/onTime and the timeupdate subscription.
- Player: _tryDirectTimelineSeek now seeks with suppressEvents=false so the
  composition's root-timeline onUpdate fires — imperative scene-visibility decks
  (slideshows) repaint on a paused seek, not only while playing. (Direct-timeline
  seek is the bare-player/present path; the runtime path already repaints.)

Verified two-window headful: presenter + audience both land on the same scene,
paused, no drift; audience visibly shows the correct slide. 268 player tests pass.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant