mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
refactor(studio): reuse player probe errors
This commit is contained in:
@@ -32,7 +32,7 @@ export interface NLEContextValue {
|
||||
togglePlay: () => void;
|
||||
seek: (time: number, options?: { keepPlaying?: boolean }) => boolean;
|
||||
refreshPlayer: () => void;
|
||||
onIframeLoad: (reportError?: (message: string) => void) => void;
|
||||
onIframeLoad: () => void;
|
||||
// composition stack (from useCompositionStack)
|
||||
compositionStack: CompositionLevel[];
|
||||
updateCompositionStack: React.Dispatch<React.SetStateAction<CompositionLevel[]>>;
|
||||
@@ -122,17 +122,14 @@ export function NLEProvider({
|
||||
refreshPlayer();
|
||||
}, [refreshKey, refreshPlayer]);
|
||||
|
||||
const onIframeLoad = useCallback(
|
||||
(reportError?: (message: string) => void) => {
|
||||
baseOnIframeLoad(reportError);
|
||||
// Pre-load + register MotionPathPlugin once so adding a motion path in the
|
||||
// studio doesn't take the async plugin-load flash path on the first soft
|
||||
// reload (the comp may not ship the plugin until it actually uses one).
|
||||
ensureMotionPathPluginLoaded(iframeRef.current);
|
||||
onIframeRef?.(iframeRef.current);
|
||||
},
|
||||
[baseOnIframeLoad, iframeRef, onIframeRef],
|
||||
);
|
||||
const onIframeLoad = useCallback(() => {
|
||||
baseOnIframeLoad();
|
||||
// Pre-load + register MotionPathPlugin once so adding a motion path in the
|
||||
// studio doesn't take the async plugin-load flash path on the first soft
|
||||
// reload (the comp may not ship the plugin until it actually uses one).
|
||||
ensureMotionPathPluginLoaded(iframeRef.current);
|
||||
onIframeRef?.(iframeRef.current);
|
||||
}, [baseOnIframeLoad, iframeRef, onIframeRef]);
|
||||
|
||||
const {
|
||||
compositionStack,
|
||||
|
||||
@@ -15,7 +15,7 @@ import { readStudioUiPreferences, writeStudioUiPreferences } from "../../utils/s
|
||||
interface NLEPreviewProps {
|
||||
projectId: string;
|
||||
iframeRef: RefObject<HTMLIFrameElement | null>;
|
||||
onIframeLoad: (reportError?: (message: string) => void) => void;
|
||||
onIframeLoad: () => void;
|
||||
onCompositionLoadingChange?: (loading: boolean) => void;
|
||||
portrait?: boolean;
|
||||
directUrl?: string;
|
||||
@@ -491,9 +491,9 @@ export const NLEPreview = memo(function NLEPreview({
|
||||
ref={setPreviewIframeRef}
|
||||
projectId={directUrl ? undefined : projectId}
|
||||
directUrl={directUrl}
|
||||
onLoad={(reportError) => {
|
||||
onLoad={() => {
|
||||
updateCompositionSizeFromPreview();
|
||||
onIframeLoad(reportError);
|
||||
onIframeLoad();
|
||||
applyInitialZoom();
|
||||
}}
|
||||
onCompositionLoadingChange={onCompositionLoadingChange}
|
||||
|
||||
@@ -7,6 +7,15 @@ import {
|
||||
shouldShowCompositionLoadingOverlay,
|
||||
} from "./Player";
|
||||
|
||||
function createAudioIframe() {
|
||||
const iframe = document.createElement("iframe");
|
||||
document.body.appendChild(iframe);
|
||||
const audio = iframe.contentDocument?.createElement("audio");
|
||||
expect(audio).toBeDefined();
|
||||
iframe.contentDocument?.body.appendChild(audio!);
|
||||
return { audio: audio!, iframe };
|
||||
}
|
||||
|
||||
describe("preview errors", () => {
|
||||
it("reads the player probe error for the visible retry state", () => {
|
||||
expect(
|
||||
@@ -35,10 +44,7 @@ describe("composition loading overlay", () => {
|
||||
});
|
||||
|
||||
it("keeps the asset overlay up while media is still buffering", () => {
|
||||
const iframe = document.createElement("iframe");
|
||||
document.body.appendChild(iframe);
|
||||
const audio = iframe.contentDocument?.createElement("audio");
|
||||
expect(audio).toBeDefined();
|
||||
const { audio, iframe } = createAudioIframe();
|
||||
Object.defineProperty(audio, "readyState", {
|
||||
value: 0,
|
||||
configurable: true,
|
||||
@@ -47,7 +53,6 @@ describe("composition loading overlay", () => {
|
||||
value: 2,
|
||||
configurable: true,
|
||||
});
|
||||
iframe.contentDocument?.body.appendChild(audio!);
|
||||
|
||||
expect(hasUnloadedAssets(iframe, false)).toBe(true);
|
||||
|
||||
@@ -55,10 +60,7 @@ describe("composition loading overlay", () => {
|
||||
});
|
||||
|
||||
it("does not keep the asset overlay stuck on failed media sources", () => {
|
||||
const iframe = document.createElement("iframe");
|
||||
document.body.appendChild(iframe);
|
||||
const audio = iframe.contentDocument?.createElement("audio");
|
||||
expect(audio).toBeDefined();
|
||||
const { audio, iframe } = createAudioIframe();
|
||||
Object.defineProperty(audio, "error", {
|
||||
value: { code: 4, message: "format error" },
|
||||
configurable: true,
|
||||
@@ -71,7 +73,6 @@ describe("composition loading overlay", () => {
|
||||
value: 3,
|
||||
configurable: true,
|
||||
});
|
||||
iframe.contentDocument?.body.appendChild(audio!);
|
||||
|
||||
expect(hasUnloadedAssets(iframe, false)).toBe(false);
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import { HyperframesLoader } from "../../components/ui";
|
||||
interface PlayerProps {
|
||||
projectId?: string;
|
||||
directUrl?: string;
|
||||
onLoad: (reportError: (message: string) => void) => void;
|
||||
onLoad: () => void;
|
||||
onCompositionLoadingChange?: (loading: boolean) => void;
|
||||
portrait?: boolean;
|
||||
style?: React.CSSProperties;
|
||||
@@ -208,10 +208,7 @@ export const Player = forwardRef<HTMLIFrameElement, PlayerProps>(
|
||||
const onEnd = () => container.classList.remove("preview-revealing");
|
||||
container.addEventListener("animationend", onEnd, { once: true });
|
||||
}
|
||||
onLoad((message) => {
|
||||
setPreviewError(message);
|
||||
setCompositionLoading(false);
|
||||
});
|
||||
onLoad();
|
||||
|
||||
// Show a loading overlay until every `<video>`/`<audio>` and Lottie
|
||||
// asset is ready. Without this users can click play before audio has
|
||||
|
||||
@@ -129,39 +129,6 @@ function expectStorePlaybackState(
|
||||
}
|
||||
|
||||
describe("useTimelinePlayer seek hydration", () => {
|
||||
it("reports when Studio cannot initialize a timeline after iframe load", () => {
|
||||
vi.useFakeTimers();
|
||||
const { api, root } = renderTimelinePlayerHarness();
|
||||
const iframe = document.createElement("iframe");
|
||||
const iframeWindow = {
|
||||
postMessage: vi.fn(),
|
||||
scrollTo: vi.fn(),
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
};
|
||||
Object.defineProperty(iframe, "contentWindow", {
|
||||
value: iframeWindow,
|
||||
configurable: true,
|
||||
});
|
||||
Object.defineProperty(iframe, "contentDocument", {
|
||||
value: document.implementation.createHTMLDocument("preview"),
|
||||
configurable: true,
|
||||
});
|
||||
const reportError = vi.fn();
|
||||
|
||||
act(() => {
|
||||
api.iframeRef.current = iframe;
|
||||
api.onIframeLoad(reportError);
|
||||
vi.advanceTimersByTime(5000);
|
||||
});
|
||||
|
||||
expect(reportError).toHaveBeenCalledWith(
|
||||
"Studio could not initialize the composition timeline.",
|
||||
);
|
||||
unmountWithAct(root);
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("keeps an external seek request until the iframe adapter is ready", () => {
|
||||
const observedTimes: number[] = [];
|
||||
const unsubscribe = liveTime.subscribe((time) => {
|
||||
|
||||
@@ -399,60 +399,51 @@ export function useTimelineSyncCallbacks({
|
||||
pendingSeekRef,
|
||||
]);
|
||||
|
||||
const onIframeLoad = useCallback(
|
||||
(reportError?: (message: string) => void) => {
|
||||
applyPreviewAudioState();
|
||||
if (probeIntervalRef.current) clearInterval(probeIntervalRef.current);
|
||||
const onIframeLoad = useCallback(() => {
|
||||
applyPreviewAudioState();
|
||||
if (probeIntervalRef.current) clearInterval(probeIntervalRef.current);
|
||||
|
||||
// Fast path: adapter already available (in-place reloads, cached compositions)
|
||||
if (initializeAdapter()) return;
|
||||
// Fast path: adapter already available (in-place reloads, cached compositions)
|
||||
if (initializeAdapter()) return;
|
||||
|
||||
// The runtime posts "state" or "timeline" messages once ready.
|
||||
// Listen for those instead of polling.
|
||||
const iframe = iframeRef.current;
|
||||
let settled = false;
|
||||
// The runtime posts "state" or "timeline" messages once ready.
|
||||
// Listen for those instead of polling.
|
||||
const iframe = iframeRef.current;
|
||||
let settled = false;
|
||||
|
||||
const trySettle = () => {
|
||||
if (settled) return;
|
||||
if (initializeAdapter()) {
|
||||
settled = true;
|
||||
window.removeEventListener("message", onMessage);
|
||||
if (probeIntervalRef.current) clearInterval(probeIntervalRef.current);
|
||||
}
|
||||
};
|
||||
|
||||
const onMessage = (e: MessageEvent) => {
|
||||
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();
|
||||
}
|
||||
};
|
||||
window.addEventListener("message", onMessage);
|
||||
|
||||
// Safety net: if no message arrives within 5s, try one last time then give up.
|
||||
probeIntervalRef.current = setTimeout(() => {
|
||||
if (!settled) {
|
||||
trySettle();
|
||||
if (!settled) {
|
||||
reportError?.("Studio could not initialize the composition timeline.");
|
||||
}
|
||||
}
|
||||
const trySettle = () => {
|
||||
if (settled) return;
|
||||
if (initializeAdapter()) {
|
||||
settled = true;
|
||||
window.removeEventListener("message", onMessage);
|
||||
// Never leave the preview stuck invisible if the runtime never settled
|
||||
// (initializeAdapter reveals on success; this covers the give-up case).
|
||||
revealIframe(iframeRef.current);
|
||||
}, 5000) as unknown as ReturnType<typeof setInterval>;
|
||||
},
|
||||
[initializeAdapter, iframeRef, probeIntervalRef, applyPreviewAudioState],
|
||||
);
|
||||
if (probeIntervalRef.current) clearInterval(probeIntervalRef.current);
|
||||
}
|
||||
};
|
||||
|
||||
const onMessage = (e: MessageEvent) => {
|
||||
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();
|
||||
}
|
||||
};
|
||||
window.addEventListener("message", onMessage);
|
||||
|
||||
// Safety net: if no message arrives within 5s, try one last time then give up.
|
||||
probeIntervalRef.current = setTimeout(() => {
|
||||
if (!settled) {
|
||||
trySettle();
|
||||
}
|
||||
window.removeEventListener("message", onMessage);
|
||||
// Never leave the preview stuck invisible if the runtime never settled
|
||||
// (initializeAdapter reveals on success; this covers the give-up case).
|
||||
revealIframe(iframeRef.current);
|
||||
}, 5000) as unknown as ReturnType<typeof setInterval>;
|
||||
}, [initializeAdapter, iframeRef, probeIntervalRef, applyPreviewAudioState]);
|
||||
|
||||
// Stable refs so mount-effect closures always call the latest version
|
||||
const processTimelineMessageRef = { current: processTimelineMessage };
|
||||
|
||||
Reference in New Issue
Block a user