mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 12:54:29 +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:
@@ -0,0 +1,11 @@
|
||||
export async function loadRuntimeSourceFallback(): Promise<string | null> {
|
||||
try {
|
||||
const mod = await import("@hyperframes/core");
|
||||
if (typeof mod.loadHyperframeRuntimeSource === "function") {
|
||||
return mod.loadHyperframeRuntimeSource();
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn("[studio] Failed to load runtime source fallback:", err);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { loadHyperframeRuntimeSource } from "@hyperframes/core";
|
||||
import { loadRuntimeSourceFallback } from "./runtimeSource.js";
|
||||
|
||||
describe("loadRuntimeSourceFallback", () => {
|
||||
it("loads runtime source from the published core entrypoint", async () => {
|
||||
await expect(loadRuntimeSourceFallback()).resolves.toBe(loadHyperframeRuntimeSource());
|
||||
});
|
||||
});
|
||||
@@ -10,6 +10,7 @@ import { streamSSE } from "hono/streaming";
|
||||
import { existsSync, readFileSync, writeFileSync, statSync } from "node:fs";
|
||||
import { resolve, join, basename } from "node:path";
|
||||
import { createProjectWatcher, type ProjectWatcher } from "./fileWatcher.js";
|
||||
import { loadRuntimeSourceFallback } from "./runtimeSource.js";
|
||||
import { VERSION as version } from "../version.js";
|
||||
import {
|
||||
createStudioApi,
|
||||
@@ -228,7 +229,31 @@ export function createStudioServer(options: StudioServerOptions): StudioServer {
|
||||
}, opts.seekTime);
|
||||
// Let the seek render settle.
|
||||
await new Promise((r) => setTimeout(r, 200));
|
||||
const screenshot = (await page.screenshot({ type: "jpeg", quality: 80 })) as Buffer;
|
||||
let clip: { x: number; y: number; width: number; height: number } | undefined;
|
||||
if (opts.selector) {
|
||||
clip = await page.evaluate((selector: string) => {
|
||||
const el = document.querySelector(selector);
|
||||
if (!(el instanceof HTMLElement)) return undefined;
|
||||
const rect = el.getBoundingClientRect();
|
||||
if (rect.width < 4 || rect.height < 4) return undefined;
|
||||
const pad = 8;
|
||||
const x = Math.max(0, rect.left - pad);
|
||||
const y = Math.max(0, rect.top - pad);
|
||||
const maxWidth = window.innerWidth - x;
|
||||
const maxHeight = window.innerHeight - y;
|
||||
return {
|
||||
x,
|
||||
y,
|
||||
width: Math.max(1, Math.min(rect.width + pad * 2, maxWidth)),
|
||||
height: Math.max(1, Math.min(rect.height + pad * 2, maxHeight)),
|
||||
};
|
||||
}, opts.selector);
|
||||
}
|
||||
const screenshot = (await page.screenshot({
|
||||
type: "jpeg",
|
||||
quality: 80,
|
||||
...(clip ? { clip } : {}),
|
||||
})) as Buffer;
|
||||
return screenshot;
|
||||
} catch {
|
||||
return null;
|
||||
@@ -256,11 +281,17 @@ export function createStudioServer(options: StudioServerOptions): StudioServer {
|
||||
|
||||
// CLI-specific routes (before shared API)
|
||||
app.get("/api/runtime.js", (c) => {
|
||||
if (!existsSync(runtimePath)) return c.text("runtime not built", 404);
|
||||
return c.body(readFileSync(runtimePath, "utf-8"), 200, {
|
||||
"Content-Type": "text/javascript",
|
||||
"Cache-Control": "no-store",
|
||||
});
|
||||
const serve = async () => {
|
||||
const runtimeSource = existsSync(runtimePath)
|
||||
? readFileSync(runtimePath, "utf-8")
|
||||
: await loadRuntimeSourceFallback();
|
||||
if (!runtimeSource) return c.text("runtime not available", 404);
|
||||
return c.body(runtimeSource, 200, {
|
||||
"Content-Type": "text/javascript",
|
||||
"Cache-Control": "no-store",
|
||||
});
|
||||
};
|
||||
return serve();
|
||||
});
|
||||
|
||||
app.get("/api/events", (c) => {
|
||||
|
||||
Reference in New Issue
Block a user