Skip to content

feat(studio): seek-restore contract for player reloads#2193

Open
ukimsanov wants to merge 1 commit into
studio-dnd/pr01-timeline-leaf-helpersfrom
studio-dnd/pr02-seek-restore-contract
Open

feat(studio): seek-restore contract for player reloads#2193
ukimsanov wants to merge 1 commit into
studio-dnd/pr01-timeline-leaf-helpersfrom
studio-dnd/pr02-seek-restore-contract

Conversation

@ukimsanov

@ukimsanov ukimsanov commented Jul 11, 2026

Copy link
Copy Markdown
Collaborator

What

useTimelineSyncCallbacks gains resolveReloadSeekTime and revealIframe — the pure contract for where the playhead lands after a preview reload (pending seek > deep-link seek > store playhead, clamped) and for undoing refreshPlayer's iframe hide. Test suite included.

Why

the NLE editor reloads the preview on every committed edit; this contract is the difference between "playhead restores" and "jumps to 0".

How

additive exports on an existing hook file; main's consumers unchanged.

Test plan

bunx vitest run useTimelineSyncCallbacks.test.ts; tsc --noEmit in packages/studio; fallow audit clean.


Stack position 2/25 — studio NLE overhaul (CapCut-parity timeline + canvas). Graphite manages bases; merge from #2192 upward. Temporary TEMP(studio-dnd) fallow entries (unwired-component windows) are all removed by #2213 (app-shell swap), whose tree is byte-identical to the fully-verified integration branch.

ukimsanov commented Jul 11, 2026

Copy link
Copy Markdown
Collaborator Author

Warning

This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
Learn more

This stack of pull requests is managed by Graphite. Learn more about stacking.

This was referenced Jul 11, 2026
@ukimsanov ukimsanov force-pushed the studio-dnd/pr02-seek-restore-contract branch 2 times, most recently from bbe1db5 to 90663ec Compare July 11, 2026 01:15
@ukimsanov ukimsanov force-pushed the studio-dnd/pr01-timeline-leaf-helpers branch from 68911ca to 1f6c9fe Compare July 11, 2026 01:15

@miga-heygen miga-heygen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed as part of the 25-PR NLE overhaul stack. Full stack review on #2205 (the keystone). No blockers on this PR. — Miga

@vanceingalls vanceingalls left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 LGTM — seek-restore priority matches the PR body claim, revealIframe is a defensive read-side landing before the write-side (safe order).

Focus items — verified.

Seek priority contract (resolveReloadSeekTime, useTimelineSyncCallbacks.ts:70-79). The one-liner target = input.pendingSeek ?? input.requestedSeek ?? input.storeCurrentTime produces exactly the priority order the PR body claims: pending → deep-link → store playhead. The ?? operator treats 0 as a valid explicit position (not nullish), so a pendingSeek: 0 correctly wins over a non-null requestedSeek — the test at lines 93-102 pins this. Clamp to [0, duration] via the <= 0 → 0 guard + Math.min(target, duration) is the "one sanctioned move" the PR body calls out (test at line 82-91 pins pendingSeek: 18 + duration: 9 → returns 9). Negative/NaN inputs return 0 unconditionally — the alternative "fall through to storeCurrentTime on invalid pendingSeek" would let a bad ref value silently mask an otherwise-good store playhead, so the current strict-guard behavior is defensible even if slightly counter-intuitive.

revealIframe — safe read-side landing. The comment (line 59) says "Undo the visibility: hidden that refreshPlayer sets across a full reload" — I checked refreshPlayer at this SHA (packages/studio/src/player/hooks/useTimelinePlayer.ts:475-484) and it does NOT currently set iframe.style.visibility = "hidden". That write must land in a later stack PR. The consequence is fine and probably intentional: shipping the reveal side first means every existing reload path continues to work identically (the guard iframe.style.visibility === "hidden" no-ops when never set), and once the hide side lands the pair is already wired — the reverse ordering (hide-before-reveal) is what would introduce a "black player" window. Confirm this reasoning with the follow-up PR: search for the visibility = "hidden" write and make sure every path that sets it either awaits initializeAdapter's success (line 283 reveal) OR the 5s safety-net (line 394 reveal), with no early-return between hide and either resolution.

Guard-seek before real seek (initializeAdapter:276-278). The guardTime = startTime > 0.001 ? Math.max(0, startTime - 0.001) : 0.001 two-step forces a time boundary crossing so adapter.seek(startTime) re-evaluates rather than no-op-ing. Comment (267-275) explains why exhaustively — this is the exact "just-dropped clip stayed invisible until nudge" fix. Correct reasoning, and the 0.001 epsilon is small enough it won't be user-visible on a 30fps timeline (0.033s frame).

Store-request consumption ordering (initializeAdapter:264-265). pendingSeekRef.current = null; if (storeSeek != null) clearSeekRequest(); — both consumers cleared after the resolve pulled them, so a re-entrant initializeAdapter sees empty slots and correctly falls back to storeCurrentTime. Matches the "second overlapping reload" test at line 57-69.

Two reveal paths. initializeAdapter reveals on the SUCCESS path (line 283) after the second seek renders the frame. The 5s safety-net (line 387-395) reveals unconditionally after either a last-try-then-give-up or a genuine timeout. Between these two, the failure-path I looked for is "initializeAdapter throws mid-flight after guard-seeking but before revealIframe" — line 302-338 is wrapped in try {} with catch {} at 338, so post-reveal work is soft-caught, but the reveal itself at 283 executes before that block starts. No hide-never-reveal path within this file.

Cross-PR watch (per R1 focus). #2213's app-shell swap must preserve this contract. Two things to check on that PR: (1) neither of these two reveal call sites gets dropped; (2) if #2213 introduces the visibility = "hidden" write on refreshPlayer, its placement must guarantee onIframeLoad fires afterward — an eager return before iframe.src is re-set would leave the hide with no consumer to reveal.

R1 by Via

@ukimsanov ukimsanov force-pushed the studio-dnd/pr01-timeline-leaf-helpers branch from 1f6c9fe to c161c77 Compare July 11, 2026 02:56
@ukimsanov ukimsanov force-pushed the studio-dnd/pr02-seek-restore-contract branch from 90663ec to b5bf0cf Compare July 11, 2026 02:56
@ukimsanov

Copy link
Copy Markdown
Collaborator Author

Confirming the hide/reveal pairing: at this PR's position refreshPlayer doesn't yet write visibility:hidden — the hide side lands with the shell swap in #2213. Verified at that position: the hide happens only in refreshPlayer immediately before the reload whose onIframeLoadinitializeAdapter success path calls revealIframe, and the 5s failsafe reveal covers the failure path; no early-return sits between hide and either resolution.

@ukimsanov ukimsanov force-pushed the studio-dnd/pr01-timeline-leaf-helpers branch from c161c77 to df6ca19 Compare July 11, 2026 08:18
@ukimsanov ukimsanov force-pushed the studio-dnd/pr02-seek-restore-contract branch from b5bf0cf to 9adc2d3 Compare July 11, 2026 08:18
@ukimsanov ukimsanov marked this pull request as ready for review July 11, 2026 09:06
@ukimsanov ukimsanov force-pushed the studio-dnd/pr02-seek-restore-contract branch from 9adc2d3 to 66a029d Compare July 11, 2026 10:49
What: useTimelineSyncCallbacks gains resolveReloadSeekTime and revealIframe —
the pure contract for where the playhead lands after a preview reload
(pending seek > deep-link seek > store playhead, clamped) and for undoing
refreshPlayer's iframe hide. Test suite included.

Why: the NLE editor reloads the preview on every committed edit; this
contract is the difference between "playhead restores" and "jumps to 0".

How: additive exports on an existing hook file; main's consumers unchanged.

Test plan: bunx vitest run useTimelineSyncCallbacks.test.ts; tsc --noEmit in
packages/studio; fallow audit clean.
@ukimsanov ukimsanov force-pushed the studio-dnd/pr02-seek-restore-contract branch from 66a029d to 89066ef Compare July 11, 2026 12:51
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.

3 participants