fix(player): version runtime protocol#2150
Conversation
47513d8 to
c6e0a39
Compare
3f5f991 to
e96823e
Compare
0d78a2a to
5911119
Compare
235d4fd to
641ca41
Compare
11159b7 to
3a725e6
Compare
301cff8 to
e296678
Compare
3a725e6 to
c33f69e
Compare
fb4b66d to
7869547
Compare
miga-heygen
left a comment
There was a problem hiding this comment.
Review — fix(player): version runtime protocol
588 additions across 30 files — significant surface, but the architectural invariants are clean.
SSOT check
Protocol definition: single canonical source in @hyperframes/core/runtime/protocol. Both Player (runtime-message-handler.ts) and Studio (runtimeProtocol.ts) import from this. No duplicated version constants, capability lists, or inspection logic. Good.
Seek time resolution: the old frame * 30 → frame / 30 dance at the host/runtime boundary is replaced with timeSeconds (seconds-first protocol). The runtime's resolveSeekTimeSeconds prefers timeSeconds, falls back to frame / fps for legacy callers. The Player sends timeSeconds directly — no conversion at the boundary. This eliminates rounding errors at non-30 fps. Clean.
Fps source of truth: each side of the bridge now declares its canonical fps in the protocol metadata. The runtime reads state.canonicalFps via getCanonicalFps(); the Player reads the runtime's declared fps from incoming timeline messages and stores it in _runtimeFps. No hardcoded 30 in the time-calculation paths.
Module-level state
runtimeProtocolFps in bridge.ts — set once in initSandboxRuntimeModular() via setRuntimeProtocolFps(state.canonicalFps), never mutated after init. Module-level but init-once-immutable. Acceptable for the browser runtime context (single composition per page).
CDN URL versioning
Build-time __HYPERFRAMES_RUNTIME_CDN_URL__ substitution in tsup.config.ts pins the injected runtime to @hyperframes/core@<player-version>. verify-runtime-pin.mjs post-build check ensures all three bundles contain the versioned URL and reject the unversioned one. Good — prevents host/runtime drift.
Protocol negotiation
- Legacy messages (no
protocolVersion): accepted with 30fps fallback. Correct for backward compatibility. - Unknown majors (
protocolVersion: 2+): rejected with diagnostic event (runtimeprotocolerror). Both sides (runtime viapostRuntimeMessage, Player/Studio viadispatchEvent). - Additive capabilities within v1: tolerated —
inspectRuntimeProtocoldoesn't require exact capability matches. Good for forward compatibility.
Studio outgoing fps
One cosmetic note: Studio's postRuntimeControlMessage always sends fps=30 in the protocol metadata (default param), even when the composition is non-30fps. This doesn't affect behavior because (a) Studio sends timeSeconds for seeks, so fps isn't used for time resolution, and (b) the runtime checks protocolVersion, not fps, for version negotiation. But it's technically inaccurate metadata. Non-blocking — a future enhancement could thread _runtimeFps from the Player's incoming timeline messages into Studio's outgoing control messages.
Studio accept gates
All message listeners now call acceptStudioRuntimeMessage — consistent gate. useCompositionDimensions was refactored into extracted helpers with proper type narrowing (isStageSizeMessage, readPositiveDimensions). Clean.
Tests
Comprehensive — protocol round-trip, legacy fallback, unknown-major rejection, seek seconds resolution, Player seek format change, Studio message creation, runtime-pin verification. All branches covered.
LGTM — no blocking issues.
— Miga
james-russo-rames-d-jusso
left a comment
There was a problem hiding this comment.
Reviewed at 7869547.
Big substantive PR done right. Introducing a versioned protocol between Player and runtime — with capabilities, rational fps, and a legacy fallback — is a real load-bearing invariant, and the shape here is what I'd want:
protocol.tsas SSOT — single source forRUNTIME_PROTOCOL_VERSION,RUNTIME_PROTOCOL_CAPABILITIES, the rational-fps helpers, andinspectRuntimeProtocol. Both sides (core/bridge,player/runtime-message-handler,studio/lib/runtimeProtocol) import from it. No duplication of the protocol shape.- Legacy fallback —
inspectRuntimeProtocolreturns{status: "legacy", fps: legacyFps}whenprotocolVersionis absent, so unversioned inbound messages still work (with the 30fps assumption that matched the previous hardcodedFPS = 30in the Player handler — no regression). timeSecondspreferred overframein seek dispatch — the runtime bridge'sresolveSeekTimeSecondsreadstimeSecondsfirst and falls back toframe / (messageFps ?? getCanonicalFps()).getCanonicalFps()reading fromstate.canonicalFpscloses the "what if the incoming message doesn't declare fps" gap correctly (uses the runtime's own known fps, not a hardcoded 30).- Runtime pin via
verify-runtime-pin.mjs— build-time check that Player bundles contain@hyperframes/core@${version}/…AND that they do NOT contain the unversioned URL. Belt-and-suspenders — the negative check (.includes("@hyperframes/core/dist/hyperframe.runtime.iife.js")) doesn't false-positive on the versioned form because@hyperframes/core@${version}/dist/…has the version wedged betweencoreand/dist/. Good. runtimeCdnUrlForVersionsemver guard — rejects"latest","next", bare tags. Tested. This is exactly the shape of "prove the guard actually catches what you claim" that I've seen go wrong in other PRs.
Rational-fps handling is careful — runtimeProtocolFpsFromNumber picks denominator 1 for integer fps and 1_000_000 otherwise, then GCD-reduces. Round-trips 24_000/1_001 and 30_000/1_001 accurately.
Concerns
packages/studio/src/player/hooks/useTimelineSyncCallbacks.ts:421-425— the settle listener (onMessagein the timeline-probe path) still uses rawdata?.source === "hf-preview" && (data?.type === "state" || data?.type === "timeline")and does NOT route throughacceptStudioRuntimeMessage. So an unsupported-protocol inbound message still triggerstrySettle(), but downstream consumers (this file's ownuseTimelineSyncCallbacksat :226 viaacceptedRuntimeMessageFps, plususeTimelinePlayeranduseCompositionDimensions) will reject the same message. Result: the settle succeeds ("iframe is alive") while every downstream reader rejects the data — user sees loaded-but-empty state. This may be intentional (settle as proof-of-life signal, separate from protocol validity), but the asymmetry is worth either (a) routing this listener through the accept guard too, so unsupported protocol short-circuits with a clean error path, or (b) adding a comment naming this as intentional proof-of-life gating.
Nits
packages/player/src/hyperframes-player.ts:98—_runtimeFps = 30initial value means every outbound_sendControlbefore the firsttimelinemessage declares fps=30 in its protocol metadata, regardless of the composition's actual fps. Since seeks now use explicittimeSeconds, the fps metadata is not load-bearing on seek — it's only informational. Not a bug, just worth naming as "metadata is best-effort until first timeline lands."runtimeprotocolerrordispatch surface asymmetry — Player dispatches viacallbacks.dispatchEvent(Player element), Studio dispatches viawindow.dispatchEvent(packages/studio/src/player/lib/runtimeProtocol.ts:44). Consumers subscribing to"runtimeprotocolerror"need to know which target to listen on. Not a bug — just worth documenting or unifying.
Questions
- External embeds hardcoding the CDN URL — the runtime pin closes the version-drift gap when Player is the entry point (Player build ↔ runtime URL are locked together). What about users who wrote their own player harness against an older HF version and hardcoded
@hyperframes/core@0.7.53/…in their site? When they upgrade Player to 0.7.55, the new Player expects protocol v1 metadata from the runtime, but their pinned 0.7.53 runtime won't send it. That falls into the "legacy" path (30fps default) — which is safe but silently divergent for non-30fps compositions. Just naming this as a known trade-off, not asking for changes.
What I didn't verify
- Cross-browser CDN URL behavior (jsdelivr's fallback for missing versions).
- The
runtime.protocol.unsupported_protocol_versiondiagnostic path end-to-end (i.e., does anyone actually consume the"diagnostic"message shape in the outer Studio/Player?).
LGTM once the useTimelineSyncCallbacks settle-listener asymmetry is either resolved or documented as intentional.
7869547 to
e7dc050
Compare
03acdb3 to
9762677
Compare
e7dc050 to
b92a574
Compare
|
Addressed the |
9762677 to
979e9d0
Compare
b92a574 to
8157da2
Compare
miguel-heygen
left a comment
There was a problem hiding this comment.
Additive re-review at 8157da27. The restack correctly resolves the prior settle-listener concern: unsupported runtime messages are now rejected before Studio marks the iframe ready, with a focused regression test. The protocol SSOT, rational-fps conversion, and version-pinned injected runtime remain well structured.
blocker — packages/player/src/hyperframes-player.ts:327: the new Player sends a seek with only timeSeconds. A cross-origin composition that already embeds an older/unversioned runtime is intentionally accepted as legacy, but that runtime bridge reads only frame and defaults a missing frame to 0. Because same-origin window.__player.seek is unavailable in this case, every Player seek posts this message and the old runtime jumps to zero. This is a real supported surface: serialized compositions keep embedded runtime scripts by default. The legacy fallback is therefore only inbound, not bidirectional. Please carry a legacy frame alongside timeSeconds and pin a new-Player → legacy-runtime regression test.
Focused verification on this head: core protocol/bridge 39/39, Player protocol/handler/player 145/145, and Studio protocol/settle 14/14 pass; none exercises this direction of version skew.
— Magi / deepwork
Verdict: REQUEST CHANGES
Reasoning: Protocol-v1 peers are correct, but the changed seek payload breaks cross-origin playback against an accepted legacy runtime by deterministically seeking to frame zero.
8157da2 to
dcefdd9
Compare
|
Valid Addressed on the current head ( Validation: |
miguel-heygen
left a comment
There was a problem hiding this comment.
Additive re-review at dcefdd98. The blocker is resolved: seek() now carries frame: Math.round(timeSeconds * _runtimeFps) alongside the protocol-v1 timeSeconds, so accepted cross-origin legacy runtimes no longer default every seek to frame zero, while current peers retain seconds-first precision. The focused regression test explicitly exercises the new-Player → legacy-runtime direction. I also verified the update is limited to this implementation and its test, and reran hyperframes-player.test.ts: 125/125 pass. All required checks on the updated head are green.\n\n— Magi / deepwork
Merge activity
|

What
Version the Player/runtime protocol and pin injected core runtime code to the Player release.
Why
An unversioned CDN runtime and untyped messages allowed silent host/runtime drift and ambiguous frame timing.
How
Define RuntimeProtocolV1 with capabilities and rational fps, generate the versioned runtime URL, validate major versions, and retain a tested legacy fallback.
Test plan