Skip to content

feat(player): synchronous seek() API with same-origin detection#397

Open
vanceingalls wants to merge 2 commits intoperf/p1-4-coalesce-mirror-parent-media-timefrom
perf/p3-1-sync-seek-same-origin
Open

feat(player): synchronous seek() API with same-origin detection#397
vanceingalls wants to merge 2 commits intoperf/p1-4-coalesce-mirror-parent-media-timefrom
perf/p3-1-sync-seek-same-origin

Conversation

@vanceingalls
Copy link
Copy Markdown
Collaborator

@vanceingalls vanceingalls commented Apr 21, 2026

Summary

Formalizes the same-origin shortcut Studio has been using privately (iframe.contentWindow.__player.seek in useTimelinePlayer.ts) as a first-class behavior of <hyperframes-player>'s public seek() method. Same-origin seeks now land in the same task as the input event — no postMessage hop, no extra microtask, no perceived scrub lag. Cross-origin embeds fall through to the existing async bridge transparently.

Why

Step P3-1 of the player perf proposal. The current seek() always posts a message to the iframe runtime, which means a single user scrub incurs:

  1. JS task: fire postMessage from parent
  2. Browser task switch into iframe context
  3. Microtask: handler dispatches
  4. Frame: runtime calls markExplicitSeek and updates DOM

Same-origin embeds (Studio, preview pane, embedded compositions) can skip all four by calling the runtime's seek directly. Studio was already doing this manually but had to duplicate the local-state bookkeeping (_currentTime, paused, controls UI) — making it a first-class behavior of the player removes the workaround and gives every same-origin consumer the win for free.

What changed

  • New _trySyncSeek(time) helper attempts a synchronous call into the iframe's window.__player.seek. Returns true on success, false on cross-origin or pre-bootstrap.
  • seek() calls _trySyncSeek first, falls through to the existing _sendControl postMessage path when sync isn't available.
  • Detection is a try/catch on contentWindow access (real cross-origin iframes throw SecurityError) plus a typeof guard on __player.seek.
  • Local _currentTime, the paused flag, and the controls UI update on both paths so scrubs never leave stale state.
  • Runtime-side seek is the same wrapped function the postMessage handler calls — installRuntimeControlBridge routes through player.seek, so markExplicitSeek() and downstream runtime state are identical between the two paths.

Test plan

  • 11 new unit tests in hyperframes-player.test.ts covering:
    • Same-origin sync path executes __player.seek synchronously and skips postMessage.
    • Cross-origin (simulated SecurityError on contentWindow) falls back to postMessage.
    • Pre-bootstrap (no __player installed) falls back to postMessage.
    • __player.seek not a function falls back to postMessage.
    • _currentTime, paused, and controls all stay in sync on both paths.
    • Errors thrown from __player.seek propagate without corrupting state.

Stack

Step P3-1 of the player perf proposal. Independent of the P1-* work — this is a pure latency win on the seek/scrub path. Combined with P3-2 (srcdoc composition switching, next in the stack) it removes most of the iframe-bridge overhead from the studio scrubber.

Copy link
Copy Markdown
Collaborator

@jrusso1020 jrusso1020 left a comment

Choose a reason for hiding this comment

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

Formalizes a pattern Studio was already reaching for under the hood (useTimelinePlayer.ts:233). Good: single-task seek on same-origin embeds, transparent postMessage fallback on SecurityError / missing __player / typeof seek !== "function" / runtime panic. Every branch is tested, including the _currentTime cache must update regardless — that catches the easy-to-miss regression where a cross-origin scrub leaves the controls UI showing stale time.

One subtle semantic change worth calling out in the release notes: the sync path passes timeInSeconds through verbatim, while the postMessage path rounds to Math.round(timeInSeconds * DEFAULT_FPS) at the wire boundary. Same-origin embeds will now land sub-frame scrubs at their exact requested time; cross-origin embeds still quantize to frames. Scrub UIs can now drive fractional-frame times on same-origin, which is the improvement — just means a test that does player.seek(7.3333) and asserts exact-equality will only pass same-origin.

Approved.

Rames Jusso

@vanceingalls vanceingalls force-pushed the perf/p1-4-coalesce-mirror-parent-media-time branch from 7600cd7 to 62e91f3 Compare April 22, 2026 00:43
@vanceingalls vanceingalls force-pushed the perf/p3-1-sync-seek-same-origin branch from 9c5a23d to 048a3c0 Compare April 22, 2026 00:43
Formalizes the same-origin shortcut Studio has been using privately
(`iframe.contentWindow.__player.seek` in `useTimelinePlayer.ts`) as a
first-class behavior of `<hyperframes-player>`'s public `seek()` method.

The player now tries the runtime's `__player.seek` directly via a new
`_trySyncSeek()` helper. When the iframe is reachable (same-origin and
the runtime has installed `__player`), the seek lands in the same task
as the input event — no postMessage hop, no extra microtask, no perceived
scrub lag. Cross-origin embeds and pre-bootstrap calls fall through to
the existing `_sendControl` postMessage bridge transparently, preserving
the original async semantics for external hosts.

Detection is a try/catch on `contentWindow` access (real cross-origin
iframes throw a SecurityError) plus a typeof guard on `__player.seek`.
Local `_currentTime`, the `paused` flag, and the controls UI are updated
on both paths so scrubs never leave stale state.

The runtime-side `seek` is the same wrapped function the postMessage
handler calls (`installRuntimeControlBridge` routes through `player.seek`),
so `markExplicitSeek()` and downstream runtime state stay identical
between the two paths.
Per reviewer feedback on #397: the same-origin path forwards `timeInSeconds`
verbatim to `__player.seek` (sub-frame precision), while the postMessage
fallback rounds to the nearest integer frame for transport. Surface that
asymmetry on the public `seek` JSDoc so external callers (notably studio
scrub UIs) understand which transport is in effect for their embed.

Doc-only — no behavior delta.
@vanceingalls vanceingalls force-pushed the perf/p3-1-sync-seek-same-origin branch from 048a3c0 to 61189b6 Compare April 22, 2026 00:53
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.

2 participants