mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-10 22:20:14 +00:00
fix: stabilize studio preview and runtime sync (#389)
## Summary Stabilize the Studio preview/runtime path so timeline data, preview rendering, and thumbnails stay in sync. This PR includes: - preview hot-refresh without remounting the iframe - runtime duration/timeline fixes so Studio stops drifting from playback state - thumbnail and selector-based preview fixes - local Studio runtime serving and player-resolution fixes so dev/CI do not depend on prebuilt player artifacts - tests around preview identity and thumbnail/runtime behavior ## Why This PR Exists This is the foundation layer for timeline editing. Without it, the editor was prone to: - iframe remount flashes after saves - duration mismatches between preview and timeline - stale or incorrect thumbnails - CI/test failures when `@hyperframes/player` artifacts were not prebuilt ## Verification - `bun run --filter @hyperframes/studio test` - `bun run --filter @hyperframes/studio typecheck` - `bun run --filter @hyperframes/core typecheck` - `bunx oxlint packages/cli/src/server/studioServer.ts packages/core/src/runtime/timeline.ts packages/core/src/runtime/timeline.test.ts packages/core/src/studio-api/routes/thumbnail.ts packages/core/src/studio-api/routes/thumbnail.test.ts packages/core/src/studio-api/types.ts packages/studio/src/components/nle/NLELayout.tsx packages/studio/src/components/nle/NLEPreview.tsx packages/studio/src/components/nle/NLEPreview.test.ts packages/studio/src/player/components/CompositionThumbnail.tsx packages/studio/src/player/components/Player.tsx packages/studio/src/player/hooks/useTimelinePlayer.ts packages/studio/src/player/store/playerStore.ts packages/studio/vite.config.ts` - `bunx oxfmt --check packages/cli/src/server/studioServer.ts packages/core/src/runtime/timeline.ts packages/core/src/runtime/timeline.test.ts packages/core/src/studio-api/routes/thumbnail.ts packages/core/src/studio-api/routes/thumbnail.test.ts packages/core/src/studio-api/types.ts packages/studio/src/components/nle/NLELayout.tsx packages/studio/src/components/nle/NLEPreview.tsx packages/studio/src/components/nle/NLEPreview.test.ts packages/studio/src/player/components/CompositionThumbnail.tsx packages/studio/src/player/components/Player.tsx packages/studio/src/player/hooks/useTimelinePlayer.ts packages/studio/src/player/store/playerStore.ts packages/studio/vite.config.ts` ## Stack - base of stack - followed by `feat: add studio timeline editing` - followed by `fix: smooth scrubber end seeking`
This commit is contained in:
@@ -59,6 +59,7 @@ export const NLELayout = memo(function NLELayout({
|
||||
togglePlay,
|
||||
seek,
|
||||
onIframeLoad: baseOnIframeLoad,
|
||||
refreshPlayer,
|
||||
saveSeekPosition,
|
||||
} = useTimelinePlayer();
|
||||
|
||||
@@ -72,12 +73,13 @@ export const NLELayout = memo(function NLELayout({
|
||||
usePlayerStore.getState().reset();
|
||||
}
|
||||
|
||||
// Preserve seek position when refreshKey changes (iframe will remount via key prop).
|
||||
// Refresh the existing iframe in place when source files change.
|
||||
const prevRefreshKeyRef = useRef(refreshKey);
|
||||
if (refreshKey !== prevRefreshKeyRef.current) {
|
||||
useEffect(() => {
|
||||
if (refreshKey === prevRefreshKeyRef.current) return;
|
||||
prevRefreshKeyRef.current = refreshKey;
|
||||
saveSeekPosition();
|
||||
}
|
||||
refreshPlayer();
|
||||
}, [refreshKey, refreshPlayer]);
|
||||
|
||||
// Wrap onIframeLoad to also notify parent of iframe ref
|
||||
const onIframeLoad = useCallback(() => {
|
||||
@@ -351,12 +353,14 @@ export const NLELayout = memo(function NLELayout({
|
||||
<>
|
||||
{/* Resize divider */}
|
||||
<div
|
||||
className="h-1 flex-shrink-0 bg-neutral-800 hover:bg-studio-accent cursor-row-resize transition-colors active:bg-studio-accent/80 z-10"
|
||||
className="group h-2 flex-shrink-0 cursor-row-resize flex items-center justify-center z-10"
|
||||
style={{ touchAction: "none" }}
|
||||
onPointerDown={handleDividerPointerDown}
|
||||
onPointerMove={handleDividerPointerMove}
|
||||
onPointerUp={handleDividerPointerUp}
|
||||
/>
|
||||
>
|
||||
<div className="h-px w-full bg-white/10 transition-colors group-hover:bg-white/16 group-active:bg-white/22" />
|
||||
</div>
|
||||
|
||||
{/* Timeline section — fixed height, resizable */}
|
||||
<div className="flex flex-col flex-shrink-0" style={{ height: timelineH }}>
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { getPreviewPlayerKey } from "./NLEPreview";
|
||||
|
||||
describe("getPreviewPlayerKey", () => {
|
||||
it("keeps the same player identity when only refreshKey changes", () => {
|
||||
expect(
|
||||
getPreviewPlayerKey({
|
||||
projectId: "timeline-edit-playground",
|
||||
refreshKey: 1,
|
||||
}),
|
||||
).toBe(
|
||||
getPreviewPlayerKey({
|
||||
projectId: "timeline-edit-playground",
|
||||
refreshKey: 2,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("switches identity when drilling into a different directUrl", () => {
|
||||
expect(
|
||||
getPreviewPlayerKey({
|
||||
projectId: "timeline-edit-playground",
|
||||
directUrl: "/api/projects/timeline-edit-playground/preview",
|
||||
}),
|
||||
).not.toBe(
|
||||
getPreviewPlayerKey({
|
||||
projectId: "timeline-edit-playground",
|
||||
directUrl: "/api/projects/timeline-edit-playground/preview/comp/compositions/intro.html",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -10,6 +10,17 @@ interface NLEPreviewProps {
|
||||
refreshKey?: number;
|
||||
}
|
||||
|
||||
export function getPreviewPlayerKey({
|
||||
projectId,
|
||||
directUrl,
|
||||
}: {
|
||||
projectId: string;
|
||||
directUrl?: string;
|
||||
refreshKey?: number;
|
||||
}): string {
|
||||
return directUrl ?? projectId;
|
||||
}
|
||||
|
||||
export const NLEPreview = memo(function NLEPreview({
|
||||
projectId,
|
||||
iframeRef,
|
||||
@@ -18,7 +29,7 @@ export const NLEPreview = memo(function NLEPreview({
|
||||
directUrl,
|
||||
refreshKey,
|
||||
}: NLEPreviewProps) {
|
||||
const playerKey = `${directUrl ?? projectId}_${refreshKey ?? 0}`;
|
||||
const playerKey = getPreviewPlayerKey({ projectId, directUrl, refreshKey });
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full min-h-0">
|
||||
|
||||
Reference in New Issue
Block a user