mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-11-04 21:17:30 +00:00
Carry the runtime's blur through the render pipeline and the Studio render panel. - engine: installPageMotionBlur sets the page-side off switch and the render's device scale before any author script runs; CaptureOptions.pageMotionBlur lets a caller stand the page blur down. - producer: a composition that asks for data-motion-blur on the root the runtime would pick forces screenshot capture, because html-in-canvas needs a real paint; the probe session carries the request's pageMotionBlur like every other session. - studio-server and Studio: the render panel sends previewMotionBlur, the render route turns "off" into pageMotionBlur once, and the Studio dev server and the CLI server both receive the final value. - mover scan: a large element that is see-through while it moves (a sweep, a light leak) is no mover, including a large image or canvas with alpha; the scan skips elements moved by a 3D individual transform property. - cli snapshot injects video frames before the seek settles, matching the engine's order. Folded review fixes: R1, R2, R3 (scan half), R7, R16, R18, T3 (render adapters). Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
140 lines
4.5 KiB
TypeScript
140 lines
4.5 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { createStudioServer, type StudioServer } from "./studioServer.js";
|
|
|
|
const mocks = vi.hoisted(() => ({
|
|
resolveRenderBrowser: vi.fn(),
|
|
createRenderJob: vi.fn(() => ({ id: "job", perfSummary: undefined })),
|
|
executeRenderJob: vi.fn(async () => {}),
|
|
}));
|
|
|
|
vi.mock("../browser/preflight.js", async (importOriginal) => ({
|
|
...(await importOriginal<typeof import("../browser/preflight.js")>()),
|
|
resolveRenderBrowser: mocks.resolveRenderBrowser,
|
|
}));
|
|
|
|
// Under vitest the module is a .ts file, which would take the dev-mode producer load.
|
|
vi.mock("../utils/env.js", async (importOriginal) => ({
|
|
...(await importOriginal<typeof import("../utils/env.js")>()),
|
|
isDevMode: () => false,
|
|
}));
|
|
|
|
// Not importOriginal: loading the real producer takes longer than a wait timeout on a cold Windows runner.
|
|
vi.mock("@hyperframes/producer", () => ({
|
|
createRenderJob: mocks.createRenderJob,
|
|
executeRenderJob: mocks.executeRenderJob,
|
|
}));
|
|
|
|
vi.mock("./studioRenderTelemetry.js", () => ({
|
|
emitStudioRenderComplete: vi.fn(),
|
|
emitStudioRenderError: vi.fn(),
|
|
}));
|
|
|
|
const dirs: string[] = [];
|
|
let server: StudioServer | undefined;
|
|
|
|
afterEach(() => {
|
|
server?.watcher.close();
|
|
server = undefined;
|
|
delete process.env.PRODUCER_HEADLESS_SHELL_PATH;
|
|
vi.clearAllMocks();
|
|
for (const dir of dirs.splice(0)) rmSync(dir, { recursive: true, force: true });
|
|
});
|
|
|
|
function startStudioRender(extra: Record<string, unknown> = {}) {
|
|
const dir = mkdtempSync(join(tmpdir(), "hf-studio-render-browser-"));
|
|
dirs.push(dir);
|
|
server = createStudioServer({ projectDir: dir });
|
|
return server.adapter.startRender({
|
|
project: { dir, id: "p", title: "p" },
|
|
outputPath: join(dir, "out.mp4"),
|
|
format: "mp4",
|
|
fps: { num: 30, den: 1 },
|
|
quality: "draft",
|
|
jobId: "job",
|
|
...extra,
|
|
} as never);
|
|
}
|
|
|
|
describe("Studio render browser resolution", () => {
|
|
it("refuses the render with the resolver's message instead of continuing without a browser", async () => {
|
|
mocks.resolveRenderBrowser.mockRejectedValue(
|
|
new Error("Chrome not found Chrome Headless Shell is required for local rendering."),
|
|
);
|
|
|
|
const state = startStudioRender();
|
|
await vi.waitFor(() => expect(state.status).toBe("failed"), { timeout: 10_000 });
|
|
|
|
expect(state.error).toBe(
|
|
"Chrome not found Chrome Headless Shell is required for local rendering.",
|
|
);
|
|
expect(mocks.createRenderJob).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("hands the resolved browser to the producer before the job is created", async () => {
|
|
mocks.resolveRenderBrowser.mockResolvedValue({
|
|
executablePath: "/opt/chrome",
|
|
source: "cache",
|
|
});
|
|
|
|
startStudioRender();
|
|
await vi.waitFor(() => expect(mocks.createRenderJob).toHaveBeenCalledTimes(1), {
|
|
timeout: 10_000,
|
|
});
|
|
|
|
expect(process.env.PRODUCER_HEADLESS_SHELL_PATH).toBe("/opt/chrome");
|
|
});
|
|
});
|
|
|
|
describe("Studio render GPU encoding", () => {
|
|
it("creates the job with useGpu when the request asks for GPU encoding", async () => {
|
|
mocks.resolveRenderBrowser.mockResolvedValue({
|
|
executablePath: "/opt/chrome",
|
|
source: "cache",
|
|
});
|
|
|
|
startStudioRender({ useGpu: true });
|
|
await vi.waitFor(() => expect(mocks.createRenderJob).toHaveBeenCalledTimes(1), {
|
|
timeout: 10_000,
|
|
});
|
|
|
|
expect(mocks.createRenderJob).toHaveBeenCalledWith(expect.objectContaining({ useGpu: true }));
|
|
});
|
|
});
|
|
|
|
describe("Studio render motion blur", () => {
|
|
it("hands the route's pageMotionBlur to the job", async () => {
|
|
mocks.resolveRenderBrowser.mockResolvedValue({
|
|
executablePath: "/opt/chrome",
|
|
source: "cache",
|
|
});
|
|
|
|
startStudioRender({ pageMotionBlur: "off" });
|
|
await vi.waitFor(() => expect(mocks.createRenderJob).toHaveBeenCalledTimes(1), {
|
|
timeout: 10_000,
|
|
});
|
|
|
|
expect(mocks.createRenderJob).toHaveBeenCalledWith(
|
|
expect.objectContaining({ pageMotionBlur: "off" }),
|
|
);
|
|
});
|
|
|
|
it("sends none when the route sent none", async () => {
|
|
mocks.resolveRenderBrowser.mockResolvedValue({
|
|
executablePath: "/opt/chrome",
|
|
source: "cache",
|
|
});
|
|
|
|
startStudioRender();
|
|
await vi.waitFor(() => expect(mocks.createRenderJob).toHaveBeenCalledTimes(1), {
|
|
timeout: 10_000,
|
|
});
|
|
|
|
expect(mocks.createRenderJob).not.toHaveBeenCalledWith(
|
|
expect.objectContaining({ pageMotionBlur: expect.anything() }),
|
|
);
|
|
});
|
|
});
|