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
@@ -0,0 +1,48 @@
import { describe, expect, it, vi } from "vitest";
import {
acceptStudioRuntimeMessage,
acceptedRuntimeMessageFps,
createRuntimeControlMessage,
inspectStudioRuntimeMessage,
postRuntimeControlMessage,
} from "./runtimeProtocol";
describe("Studio runtime protocol", () => {
it("versions every control message and declares rational fps", () => {
expect(createRuntimeControlMessage("seek", { timeSeconds: 1.25 }, 60)).toEqual({
source: "hf-parent",
type: "control",
action: "seek",
protocolVersion: 1,
capabilities: ["seconds-time", "rational-fps", "seek-keep-playing"],
fps: { numerator: 60, denominator: 1 },
timeSeconds: 1.25,
});
});
it("posts the typed message to the target window", () => {
const target = { postMessage: vi.fn() };
postRuntimeControlMessage(target as unknown as Window, "pause");
expect(target.postMessage).toHaveBeenCalledWith(
expect.objectContaining({ action: "pause", protocolVersion: 1 }),
"*",
);
});
it("preserves legacy 30fps messages and rejects unknown majors", () => {
expect(inspectStudioRuntimeMessage({ source: "hf-preview" })).toEqual({
status: "legacy",
fps: 30,
});
expect(inspectStudioRuntimeMessage({ protocolVersion: 2 })).toMatchObject({
status: "unsupported",
code: "unsupported_protocol_version",
});
});
it("reads explicit fps for accepted timeline messages", () => {
const message = createRuntimeControlMessage("pause", {}, 60);
expect(acceptedRuntimeMessageFps(message)).toBe(60);
expect(acceptStudioRuntimeMessage(message)).toMatchObject({ status: "supported", fps: 60 });
});
});
@@ -0,0 +1,65 @@
import {
inspectRuntimeProtocol,
runtimeProtocolMetadata,
type RuntimeProtocolInspection,
} from "@hyperframes/core/runtime/protocol";
export type RuntimeControlMessage = {
source: "hf-parent";
type: "control";
action: string;
} & ReturnType<typeof runtimeProtocolMetadata> &
Record<string, unknown>;
export function createRuntimeControlMessage(
action: string,
payload: Record<string, unknown> = {},
fps = 30,
): RuntimeControlMessage {
return {
...payload,
source: "hf-parent",
type: "control",
action,
...runtimeProtocolMetadata(fps),
};
}
export function postRuntimeControlMessage(
target: Pick<Window, "postMessage"> | null | undefined,
action: string,
payload: Record<string, unknown> = {},
fps = 30,
): void {
target?.postMessage(createRuntimeControlMessage(action, payload, fps), "*");
}
export function inspectStudioRuntimeMessage(value: unknown): RuntimeProtocolInspection {
return inspectRuntimeProtocol(value, 30);
}
function dispatchRuntimeProtocolError(inspection: RuntimeProtocolInspection): void {
if (inspection.status !== "unsupported") return;
window.dispatchEvent(
new CustomEvent("runtimeprotocolerror", {
detail: {
code: inspection.code,
receivedVersion: inspection.receivedVersion,
},
}),
);
}
export function acceptStudioRuntimeMessage(
value: unknown,
): Exclude<RuntimeProtocolInspection, { status: "unsupported" }> | null {
const inspection = inspectStudioRuntimeMessage(value);
if (inspection.status !== "unsupported") return inspection;
dispatchRuntimeProtocolError(inspection);
return null;
}
export function acceptedRuntimeMessageFps(value: unknown): number {
const inspection = inspectStudioRuntimeMessage(value);
return inspection.status === "supported" ? inspection.fps : 30;
}
@@ -20,6 +20,7 @@ import {
buildTimelineElementIdentity,
readTimelineElementZIndex,
} from "./timelineElementHelpers";
import { postRuntimeControlMessage } from "./runtimeProtocol";
// ---------------------------------------------------------------------------
// Viewport / DOM normalisation
@@ -103,10 +104,7 @@ function postPreviewControl(
action: string,
payload: Record<string, unknown>,
): void {
iframe.contentWindow?.postMessage(
{ source: "hf-parent", type: "control", action, ...payload },
"*",
);
postRuntimeControlMessage(iframe.contentWindow, action, payload);
}
export function shouldMutePreviewAudio(audioMuted: boolean, playbackRate: number): boolean {