fix(player): version runtime protocol

This commit is contained in:
James
2026-07-13 13:28:11 -04:00
parent 95fa14b2c2
commit dcefdd98ca
32 changed files with 683 additions and 78 deletions
@@ -153,6 +153,61 @@ describe("useTimelinePlayer seek hydration", () => {
unmountWithAct(root);
unsubscribe();
});
it("does not settle from an unsupported runtime protocol message", () => {
const { api, root } = renderTimelinePlayerHarness();
const iframe = document.createElement("iframe");
const iframeWindow = {
postMessage: vi.fn(),
scrollTo: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
} as Record<string, unknown>;
Object.defineProperty(iframe, "contentWindow", {
value: iframeWindow,
configurable: true,
});
Object.defineProperty(iframe, "contentDocument", {
value: document.implementation.createHTMLDocument("preview"),
configurable: true,
});
act(() => {
api.iframeRef.current = iframe;
api.onIframeLoad();
});
expect(usePlayerStore.getState().timelineReady).toBe(false);
iframeWindow.__player = {
play: vi.fn(),
pause: vi.fn(),
seek: vi.fn(),
getTime: () => 0,
getDuration: () => 30,
isPlaying: () => false,
};
act(() => {
window.dispatchEvent(
new MessageEvent("message", {
source: iframeWindow as unknown as Window,
data: { source: "hf-preview", type: "state", protocolVersion: 999 },
}),
);
});
expect(usePlayerStore.getState().timelineReady).toBe(false);
act(() => {
window.dispatchEvent(
new MessageEvent("message", {
source: iframeWindow as unknown as Window,
data: { source: "hf-preview", type: "state" },
}),
);
});
expect(usePlayerStore.getState().timelineReady).toBe(true);
unmountWithAct(root);
});
});
describe("useTimelinePlayer audio controls (#835)", () => {
@@ -46,6 +46,7 @@ import { scrubMusicAtSeek, stopScrubPreviewAudio } from "../lib/playbackScrub";
import { applyCachedSourceDurations, probeMissingSourceDurations } from "../lib/mediaProbe";
import { shouldResumeForwardPlaybackAfterSeek, shouldStopAfterSeek } from "../lib/playbackSeek";
import { applyPreviewVariablesToUrl } from "../../hooks/previewVariablesStore";
import { acceptStudioRuntimeMessage } from "../lib/runtimeProtocol";
/**
* Whether the derived elements differ from the current ones in any field that
@@ -492,6 +493,9 @@ export function useTimelinePlayer() {
if (e.source && ourIframe && e.source !== ourIframe.contentWindow) {
return;
}
if (data?.source === "hf-preview") {
if (!acceptStudioRuntimeMessage(data)) return;
}
if (data?.source === "hf-preview" && data?.type === "state") {
try {
if (usePlayerStore.getState().elements.length === 0) {
@@ -26,6 +26,7 @@ import {
autoHealMissingCompositionIds,
buildMissingCompositionElements,
} from "../lib/timelineIframeHelpers";
import { acceptedRuntimeMessageFps, inspectStudioRuntimeMessage } from "../lib/runtimeProtocol";
interface UseTimelineSyncCallbacksParams {
iframeRef: React.RefObject<HTMLIFrameElement | null>;
@@ -132,6 +133,9 @@ export function useTimelineSyncCallbacks({
clips: ClipManifestClip[];
durationInFrames: number;
scenes?: Array<{ id: string; label: string; start: number; duration: number }>;
protocolVersion?: unknown;
capabilities?: unknown;
fps?: unknown;
}) => {
if (!data.clips || data.clips.length === 0) {
return;
@@ -222,7 +226,7 @@ export function useTimelineSyncCallbacks({
hostEl,
});
});
const rawDuration = data.durationInFrames / 30;
const rawDuration = data.durationInFrames / acceptedRuntimeMessageFps(data);
// Clamp non-finite or absurdly large durations — the runtime can emit
// Infinity when it detects a loop-inflated GSAP timeline without an
// explicit data-duration on the root composition. Floor the manifest total
@@ -418,6 +422,10 @@ export function useTimelineSyncCallbacks({
if (e.source && iframe && e.source !== iframe.contentWindow) return;
const data = e.data;
if (data?.source === "hf-preview" && (data?.type === "state" || data?.type === "timeline")) {
// The main message handler owns protocol-error diagnostics. This readiness-only
// listener mirrors its acceptance gate without dispatching a duplicate event:
// an unsupported runtime must not make the iframe appear successfully settled.
if (inspectStudioRuntimeMessage(data).status === "unsupported") return;
trySettle();
}
};