mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-10 22:20:14 +00:00
fix(studio): improve preview loading reliability
This commit is contained in:
@@ -40,6 +40,33 @@ function mount(onSelect = vi.fn(), onAddToTimeline = vi.fn()) {
|
||||
}
|
||||
|
||||
describe("composition card drag", () => {
|
||||
it("uses a cached image instead of eagerly mounting a live preview iframe", () => {
|
||||
const { host } = mount();
|
||||
expect(host.querySelector('img[src*="/thumbnail/"]')).not.toBeNull();
|
||||
expect(host.querySelector("iframe")).toBeNull();
|
||||
});
|
||||
|
||||
it("mounts one live preview only after sustained hover and removes it on leave", () => {
|
||||
vi.useFakeTimers();
|
||||
const consoleError = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
try {
|
||||
const { host, card } = mount();
|
||||
act(() => {
|
||||
card.dispatchEvent(new Event("pointerover", { bubbles: true }));
|
||||
vi.advanceTimersByTime(300);
|
||||
});
|
||||
expect(host.querySelectorAll("iframe")).toHaveLength(1);
|
||||
|
||||
act(() => {
|
||||
card.dispatchEvent(new Event("pointerout", { bubbles: true }));
|
||||
});
|
||||
expect(host.querySelector("iframe")).toBeNull();
|
||||
} finally {
|
||||
consoleError.mockRestore();
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps ordinary click navigation", () => {
|
||||
const { card, onSelect } = mount();
|
||||
act(() => card.click());
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { memo, useCallback, useEffect, useRef, useState } from "react";
|
||||
import { setPreviewMediaMuted } from "../../player/lib/timelineIframeHelpers";
|
||||
import { buildCompositionThumbnailUrl } from "../../player/components/CompositionThumbnail";
|
||||
import { TIMELINE_COMPOSITION_MIME } from "../../utils/timelineCompositionDrop";
|
||||
|
||||
interface CompositionsTabProps {
|
||||
@@ -130,6 +131,7 @@ function CompCard({
|
||||
}) {
|
||||
const [hovered, setHovered] = useState(false);
|
||||
const [stageSize, setStageSize] = useState(DEFAULT_PREVIEW_STAGE);
|
||||
const [livePreviewLoaded, setLivePreviewLoaded] = useState(false);
|
||||
const iframeRef = useRef<HTMLIFrameElement | null>(null);
|
||||
const hoverTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const syncTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
@@ -159,9 +161,16 @@ function CompCard({
|
||||
hoverTimer.current = null;
|
||||
}
|
||||
setHovered(false);
|
||||
setLivePreviewLoaded(false);
|
||||
};
|
||||
const name = comp.replace(/^compositions\//, "").replace(/\.html$/, "");
|
||||
const previewUrl = `/api/projects/${projectId}/preview/comp/${comp}`;
|
||||
const thumbnailUrl = buildCompositionThumbnailUrl({
|
||||
previewUrl,
|
||||
seekTime: 0,
|
||||
duration: THUMBNAIL_SEEK_TIME_SECONDS * 2,
|
||||
origin: window.location.origin,
|
||||
});
|
||||
const previewScale = resolveCompositionPreviewScale({
|
||||
cardWidth: CARD_W,
|
||||
cardHeight: CARD_H,
|
||||
@@ -172,7 +181,7 @@ function CompCard({
|
||||
const thumbnailOffsetY = (CARD_H - stageSize.height * previewScale) / 2;
|
||||
|
||||
useEffect(() => {
|
||||
requestIframePlaybackSync(hovered);
|
||||
if (hovered) requestIframePlaybackSync(true);
|
||||
}, [hovered, requestIframePlaybackSync]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -216,36 +225,49 @@ function CompCard({
|
||||
}`}
|
||||
>
|
||||
<div className="w-20 h-[45px] rounded overflow-hidden bg-neutral-900 flex-shrink-0 relative">
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
src={previewUrl}
|
||||
sandbox="allow-scripts allow-same-origin"
|
||||
<img
|
||||
src={thumbnailUrl}
|
||||
alt=""
|
||||
draggable={false}
|
||||
loading="lazy"
|
||||
className="absolute border-none pointer-events-none"
|
||||
style={{
|
||||
transformOrigin: "0 0",
|
||||
width: stageSize.width,
|
||||
height: stageSize.height,
|
||||
left: thumbnailOffsetX,
|
||||
top: thumbnailOffsetY,
|
||||
transform: `scale(${previewScale})`,
|
||||
}}
|
||||
onLoad={(e) => {
|
||||
try {
|
||||
const iframe = e.currentTarget;
|
||||
const root = iframe.contentDocument?.querySelector("[data-composition-id]");
|
||||
const width = Number(root?.getAttribute("data-width")) || DEFAULT_PREVIEW_STAGE.width;
|
||||
const height =
|
||||
Number(root?.getAttribute("data-height")) || DEFAULT_PREVIEW_STAGE.height;
|
||||
setStageSize({ width, height });
|
||||
requestIframePlaybackSync(hovered);
|
||||
} catch {
|
||||
setStageSize(DEFAULT_PREVIEW_STAGE);
|
||||
}
|
||||
}}
|
||||
title={`${name} preview`}
|
||||
tabIndex={-1}
|
||||
decoding="async"
|
||||
className={`absolute inset-0 h-full w-full object-contain transition-opacity ${
|
||||
livePreviewLoaded ? "opacity-0" : "opacity-100"
|
||||
}`}
|
||||
/>
|
||||
{hovered && (
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
src={previewUrl}
|
||||
sandbox="allow-scripts allow-same-origin"
|
||||
className="absolute border-none pointer-events-none"
|
||||
style={{
|
||||
transformOrigin: "0 0",
|
||||
width: stageSize.width,
|
||||
height: stageSize.height,
|
||||
left: thumbnailOffsetX,
|
||||
top: thumbnailOffsetY,
|
||||
transform: `scale(${previewScale})`,
|
||||
}}
|
||||
onLoad={(e) => {
|
||||
try {
|
||||
const iframe = e.currentTarget;
|
||||
const root = iframe.contentDocument?.querySelector("[data-composition-id]");
|
||||
const width =
|
||||
Number(root?.getAttribute("data-width")) || DEFAULT_PREVIEW_STAGE.width;
|
||||
const height =
|
||||
Number(root?.getAttribute("data-height")) || DEFAULT_PREVIEW_STAGE.height;
|
||||
setStageSize({ width, height });
|
||||
setLivePreviewLoaded(true);
|
||||
requestIframePlaybackSync(true);
|
||||
} catch {
|
||||
setStageSize(DEFAULT_PREVIEW_STAGE);
|
||||
}
|
||||
}}
|
||||
title={`${name} preview`}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className="min-w-0 flex-1"
|
||||
|
||||
Reference in New Issue
Block a user