Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions packages/player/src/hyperframes-player.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1807,6 +1807,9 @@ describe("HyperframesPlayer runtime ready handshake", () => {
volume: number;
audioLocked: boolean;
playbackRate: number;
ready: boolean;
duration: number;
paused: boolean;
iframe: HTMLIFrameElement;
_onMessage: (event: MessageEvent) => void;
}
Expand All @@ -1822,6 +1825,18 @@ describe("HyperframesPlayer runtime ready handshake", () => {
});
}

function timelineMessage(durationInFrames = 120) {
return new MessageEvent("message", {
source: frameWindow,
data: {
source: "hf-preview",
type: "timeline",
durationInFrames,
scenes: [],
},
});
}

function findControlCalls(action: string) {
return postSpy.mock.calls.filter((call) => {
const data = call[0] as { type?: string; action?: string };
Expand Down Expand Up @@ -1951,6 +1966,29 @@ describe("HyperframesPlayer runtime ready handshake", () => {

expect(findControlCalls("set-muted")).toHaveLength(0);
});

it("treats a cross-origin runtime timeline message as player ready", () => {
const readyEvents: Array<{ duration: number }> = [];
player.addEventListener("ready", (event) => {
readyEvents.push((event as CustomEvent<{ duration: number }>).detail);
});

player._onMessage(timelineMessage(120));

expect(player.ready).toBe(true);
expect(player.duration).toBe(4);
expect(readyEvents).toEqual([{ duration: 4 }]);
});

it("honors autoplay after cross-origin runtime timeline readiness", () => {
player.setAttribute("autoplay", "");
postSpy.mockClear();

player._onMessage(timelineMessage(120));

expect(player.paused).toBe(false);
expect(findControlCalls("play")).toHaveLength(1);
});
});

describe("HyperframesPlayer audio lock — Claude desktop UA fallback", () => {
Expand Down
18 changes: 18 additions & 0 deletions packages/player/src/hyperframes-player.ts
Original file line number Diff line number Diff line change
Expand Up @@ -643,6 +643,7 @@ class HyperframesPlayer extends HTMLElement {
sendControl: (action, extra) => this._sendControl(action, extra),
getIframeDoc: () => this.iframe.contentDocument,
onRuntimeReady: () => this._replayBridgeState(),
onRuntimeTimelineReady: (duration) => this._onRuntimeTimelineReady(duration),
shouldPromoteMediaAutoplayFallback: () => !this._isSlideshowPlayer(),
setScenes: (scenes) => {
this._scenes = scenes;
Expand All @@ -658,6 +659,23 @@ class HyperframesPlayer extends HTMLElement {
});
}

private _onRuntimeTimelineReady(duration: number) {
if (this._ready) return;
this.probe.stop();
this._duration = duration;
this._directTimelineAdapter = null;
this._ready = true;
this.controlsApi?.updateTime(this._currentTime, duration);
this.dispatchEvent(new CustomEvent("ready", { detail: { duration } }));

const doc = this._getSameOriginIframeDocument();
if (doc) this._media.setupFromIframe(doc);

this._replayBridgeState();
this._setIframeMediaMuted(this.muted);
if (this.hasAttribute("autoplay")) this.play();
}

private _onProbeReady({ duration, adapter, compositionSize }: ProbeResult) {
this._duration = duration;
this._directTimelineAdapter = adapter.kind === "direct-timeline" ? adapter.timeline : null;
Expand Down
27 changes: 27 additions & 0 deletions packages/player/src/runtime-message-handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const makeCallbacks = (): MessageHandlerCallbacks => ({
updateControlsPlaying: vi.fn(),
dispatchEvent: vi.fn(),
onRuntimeReady: vi.fn(),
onRuntimeTimelineReady: vi.fn(),
seek: vi.fn(),
play: vi.fn(),
getLoop: vi.fn(() => false),
Expand Down Expand Up @@ -101,3 +102,29 @@ describe("handleRuntimeMessage media autoplay fallback", () => {
expect(callbacks.sendControl).not.toHaveBeenCalled();
});
});

describe("handleRuntimeMessage timeline ready", () => {
const timelineEvent = (durationInFrames: unknown, source: object): MessageEvent =>
({
source,
data: { source: "hf-preview", type: "timeline", durationInFrames, scenes: [] },
}) as unknown as MessageEvent;

it("reports a finite positive timeline duration in seconds", () => {
const frameWindow = {} as Window;
const callbacks = makeCallbacks();

handleRuntimeMessage(timelineEvent(120, frameWindow), frameWindow, callbacks);

expect(callbacks.onRuntimeTimelineReady).toHaveBeenCalledWith(4);
});

it("does not report invalid timeline durations as ready", () => {
const frameWindow = {} as Window;
const callbacks = makeCallbacks();

handleRuntimeMessage(timelineEvent(Infinity, frameWindow), frameWindow, callbacks);

expect(callbacks.onRuntimeTimelineReady).not.toHaveBeenCalled();
});
});
5 changes: 5 additions & 0 deletions packages/player/src/runtime-message-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ export interface MessageHandlerCallbacks extends PlaybackStateCallbacks {
* uses it to replay current bridge state (mute, volume, playback rate) so
* control messages sent before the iframe's listener registered aren't lost. */
onRuntimeReady: () => void;
/** Invoked when the runtime posts a finite positive timeline duration. The
* player uses this as the cross-origin readiness signal because the
* same-origin composition probe cannot inspect CDN iframes. */
onRuntimeTimelineReady: (duration: number) => void;
/** Called with the scene list whenever a "timeline" message is received. */
setScenes: (scenes: SceneRecord[]) => void;
/** Return false to ignore the iframe runtime's audible-media autoplay fallback.
Expand Down Expand Up @@ -111,6 +115,7 @@ export function handleRuntimeMessage(
const duration = (data["durationInFrames"] as number) / FPS;
callbacks.setPlaybackState({ ...pb, duration });
callbacks.updateControlsTime(pb.currentTime, duration);
callbacks.onRuntimeTimelineReady(duration);
}
callbacks.setScenes(extractScenes(data["scenes"]));
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,7 @@ describe("handleRuntimeMessage scenes seam", () => {
sendControl: () => {},
getIframeDoc: () => null,
onRuntimeReady: () => {},
onRuntimeTimelineReady: () => {},
setScenes,
updateControlsTime: () => {},
updateControlsPlaying: () => {},
Expand Down
Loading