mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-11-09 20:46:13 +00:00
perf(studio): treat the preview as booted once it can play
Work deferred until the boot (editing session, lint, overlays, GSAP parse, card thumbnails) started at timeline-ready, which fires before the asset gate, so the editing-session parse ran inside the wait for play. The Player now latches the boot when the preview can show and play, or fails to load.
This commit is contained in:
@@ -20,7 +20,6 @@ import { RULER_GUTTER_PX, usePreviewGuidesStore } from "../editor/previewGuidesS
|
||||
import { readStudioUiPreferences, writeStudioUiPreferences } from "../../utils/studioUiPreferences";
|
||||
import { usePreviewFirstFrameTelemetry } from "../../player/hooks/usePreviewFirstFrameTelemetry";
|
||||
import { PreviewPoster, previewPosterUrl } from "./PreviewPoster";
|
||||
import { usePlayerStore } from "../../player/store/playerStore";
|
||||
interface NLEPreviewProps {
|
||||
projectId: string;
|
||||
iframeRef: RefObject<HTMLIFrameElement | null>;
|
||||
@@ -525,10 +524,7 @@ export const NLEPreview = memo(function NLEPreview({
|
||||
}}
|
||||
onCompositionLoadingChange={onCompositionLoadingChange}
|
||||
onReadyToShowChange={onLiveReadyToShowChange}
|
||||
onPreviewError={() => {
|
||||
setPosterDoneFor(activeKey);
|
||||
usePlayerStore.getState().markPreviewBooted();
|
||||
}}
|
||||
onPreviewError={() => setPosterDoneFor(activeKey)}
|
||||
onPainted={(details) => reportPreviewFirstFrame(slot, details)}
|
||||
portrait={portrait}
|
||||
suppressLoadingOverlay={suppressLoadingOverlay}
|
||||
|
||||
@@ -40,12 +40,12 @@ function mount(compositions: string[], masterCompositionPath: string | null) {
|
||||
}
|
||||
|
||||
describe("CompositionsTab card thumbnails", () => {
|
||||
it("waits for the live preview's first ready and keeps them through edit reloads", () => {
|
||||
it("waits for the live preview to boot and keeps them through edit reloads", () => {
|
||||
usePlayerStore.getState().reset();
|
||||
const host = mount(["index.html"], "index.html");
|
||||
expect(host.querySelector("img")).toBeNull();
|
||||
|
||||
act(() => usePlayerStore.getState().setTimelineReady(true));
|
||||
act(() => usePlayerStore.getState().markPreviewBooted());
|
||||
expect(host.querySelector("img")?.getAttribute("src")).toContain("/thumbnail/index.html");
|
||||
|
||||
act(() => usePlayerStore.getState().setTimelineReady(false));
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
readPreviewErrorMessage,
|
||||
shouldShowCompositionLoadingOverlay,
|
||||
} from "./Player";
|
||||
import { usePlayerStore } from "../store/playerStore";
|
||||
|
||||
vi.mock("@hyperframes/player", () => ({}));
|
||||
|
||||
@@ -271,6 +272,21 @@ describe("ready to show", () => {
|
||||
expect(onReadyToShowChange).toHaveBeenLastCalledWith(false);
|
||||
});
|
||||
|
||||
it("marks the preview booted when it can show and play, not at ready", async () => {
|
||||
usePlayerStore.setState({ previewBooted: false });
|
||||
const { player } = await mountPlayer({});
|
||||
const el = player as TestHyperframesPlayer;
|
||||
|
||||
loadAndReady(el);
|
||||
act(() => void el.dispatchEvent(new Event("assetsready")));
|
||||
await twoFrames();
|
||||
expect(usePlayerStore.getState().previewBooted).toBe(false);
|
||||
|
||||
painted(el);
|
||||
await twoFrames();
|
||||
expect(usePlayerStore.getState().previewBooted).toBe(true);
|
||||
});
|
||||
|
||||
it("reports the document start time with the painted iframe", async () => {
|
||||
const onPainted = vi.fn();
|
||||
const now = vi.spyOn(performance, "now").mockReturnValue(100);
|
||||
|
||||
@@ -4,6 +4,7 @@ import { isLottieAnimationLoaded } from "@hyperframes/core/runtime/lottie-readin
|
||||
import { useMountEffect } from "../../hooks/useMountEffect";
|
||||
import { applyPreviewVariablesToUrl } from "../../hooks/previewVariablesStore";
|
||||
import { HyperframesLoader } from "../../components/ui";
|
||||
import { usePlayerStore } from "../store/playerStore";
|
||||
// Importing "@hyperframes/player" registers a class extending HTMLElement at
|
||||
// module load, which throws under SSR, hence the dynamic import behind a
|
||||
// `typeof window` guard. Kicking it here rather than in the mount effect puts
|
||||
@@ -440,6 +441,8 @@ export const Player = forwardRef<HTMLIFrameElement, PlayerProps>(
|
||||
onReadyToShowChangeRef.current?.(false);
|
||||
return;
|
||||
}
|
||||
// The preview shows and can play: work that waited on the boot (editing session, lint) starts.
|
||||
usePlayerStore.getState().markPreviewBooted();
|
||||
let second = 0;
|
||||
const first = requestAnimationFrame(() => {
|
||||
second = requestAnimationFrame(() => onReadyToShowChangeRef.current?.(true));
|
||||
@@ -450,6 +453,10 @@ export const Player = forwardRef<HTMLIFrameElement, PlayerProps>(
|
||||
};
|
||||
}, [readyToShow]);
|
||||
|
||||
useEffect(() => {
|
||||
if (previewError) usePlayerStore.getState().markPreviewBooted();
|
||||
}, [previewError]);
|
||||
|
||||
const showCompositionOverlay =
|
||||
!suppressLoadingOverlay &&
|
||||
!compositionOverlayDeferred &&
|
||||
|
||||
@@ -8,11 +8,10 @@ import { settleCompositionReadiness } from "@hyperframes/core/composition-readin
|
||||
|
||||
export interface PlaybackReadinessSlice {
|
||||
timelineReady: boolean;
|
||||
/** Latched by the project's first ready preview and kept through edit reloads, so work that
|
||||
* must not compete with boot (card thumbnails) waits for it once. */
|
||||
/** Latched when the project's first preview shows and can play (or fails), kept through edit
|
||||
* reloads, so work that must not compete with the boot waits for it once. */
|
||||
previewBooted: boolean;
|
||||
setTimelineReady: (ready: boolean) => void;
|
||||
/** A live preview that failed to load has still finished booting. */
|
||||
markPreviewBooted: () => void;
|
||||
/** Sets timelineReady once doc's readiness inputs settle, or immediately
|
||||
* if doc is null. A wait a later call supersedes never wins the race. */
|
||||
@@ -40,15 +39,13 @@ export function createPlaybackReadinessSlice(
|
||||
markPreviewBooted: () => set({ previewBooted: true }),
|
||||
setTimelineReady: (ready) => {
|
||||
timelineReadyGeneration++;
|
||||
set(ready ? { timelineReady: true, previewBooted: true } : { timelineReady: false });
|
||||
set({ timelineReady: ready });
|
||||
},
|
||||
requestTimelineReady: (doc) => {
|
||||
const generation = ++timelineReadyGeneration;
|
||||
if (!doc) return set({ timelineReady: true, previewBooted: true });
|
||||
if (!doc) return set({ timelineReady: true });
|
||||
settleCompositionReadiness(doc, () => {
|
||||
if (generation === timelineReadyGeneration) {
|
||||
set({ timelineReady: true, previewBooted: true });
|
||||
}
|
||||
if (generation === timelineReadyGeneration) set({ timelineReady: true });
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user