mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
fix(player): treat runtime timeline as cross-origin ready (#1690)
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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 };
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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.
|
||||
@@ -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;
|
||||
|
||||
@@ -466,6 +466,7 @@ describe("handleRuntimeMessage scenes seam", () => {
|
||||
sendControl: () => {},
|
||||
getIframeDoc: () => null,
|
||||
onRuntimeReady: () => {},
|
||||
onRuntimeTimelineReady: () => {},
|
||||
setScenes,
|
||||
updateControlsTime: () => {},
|
||||
updateControlsPlaying: () => {},
|
||||
|
||||
Reference in New Issue
Block a user