diff --git a/docs/prompting/media-and-audio.mdx b/docs/prompting/media-and-audio.mdx index 9796342208..eee56760ee 100644 --- a/docs/prompting/media-and-audio.mdx +++ b/docs/prompting/media-and-audio.mdx @@ -186,7 +186,8 @@ asset and builds a bounded H.264 proxy automatically, cached under `preview`, `play`, Studio, and published player pages use that proxy for playback. A render always decodes the original file, so nothing about final -quality or color is touched. +quality or color is touched. `snapshot` also reads frames from the original with +FFmpeg, so it makes no proxy unless you pass `--proxy`. `hyperframes lint` also flags the asset at info level (`hevc_preview_codec`) so you know a proxy is in play. Proxying is optional. Pass `--no-proxy` per diff --git a/packages/cli/src/commands/snapshot.proxy.test.ts b/packages/cli/src/commands/snapshot.proxy.test.ts new file mode 100644 index 0000000000..098e15412d --- /dev/null +++ b/packages/cli/src/commands/snapshot.proxy.test.ts @@ -0,0 +1,58 @@ +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import type { StaticProjectServer } from "../utils/staticProjectServer.js"; + +const mocks = vi.hoisted(() => ({ + injectMediaCodecMap: vi.fn(async (html: string) => html), + resolveProxy: vi.fn(async () => "/unused-proxy-path"), +})); + +vi.mock("../utils/compositionServer.js", () => ({ + injectMediaCodecMap: mocks.injectMediaCodecMap, +})); +vi.mock("@hyperframes/studio-server/proxy-transcoder", async (importOriginal) => ({ + ...(await importOriginal()), + resolveProxy: mocks.resolveProxy, +})); + +import { serveCapturePage } from "./snapshot.js"; + +let server: StaticProjectServer | undefined; +let dir: string | undefined; + +afterEach(async () => { + await server?.close(); + server = undefined; + if (dir) rmSync(dir, { recursive: true, force: true }); + dir = undefined; + vi.clearAllMocks(); +}); + +/** A project whose hyperframes.json turns media proxies on, with an iPhone-style HEVC clip. */ +function hostileClipProject(): string { + dir = mkdtempSync(join(tmpdir(), "hf-snapshot-proxy-")); + writeFileSync(join(dir, "hyperframes.json"), JSON.stringify({ media: { autoProxy: true } })); + writeFileSync(join(dir, "clip.mov"), "fake hevc bytes"); + return dir; +} + +const html = ''; + +describe("serveCapturePage", () => { + it("starts no transcode of a hostile clip by default, whatever hyperframes.json says", async () => { + server = await serveCapturePage(hostileClipProject(), html, undefined); + const proxy = await fetch(`${server.url}clip.mov?hf-proxy=h264`); + + expect(proxy.status).toBe(404); + expect(mocks.injectMediaCodecMap).not.toHaveBeenCalled(); + expect(mocks.resolveProxy).not.toHaveBeenCalled(); + }); + + it("serves copies when --proxy asks for them", async () => { + server = await serveCapturePage(hostileClipProject(), html, true); + + expect(mocks.injectMediaCodecMap).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/cli/src/commands/snapshot.test.ts b/packages/cli/src/commands/snapshot.test.ts index 593fd106c0..4da4a51b40 100644 --- a/packages/cli/src/commands/snapshot.test.ts +++ b/packages/cli/src/commands/snapshot.test.ts @@ -83,11 +83,11 @@ describe("transparent snapshot capture", () => { expect(source).toContain('page.screenshot({ type: "png", omitBackground: true })'); }); - it("exposes --proxy/--no-proxy and forwards the override to the static server", () => { + it("exposes --proxy and forwards it to the snapshot page server", () => { const source = readFileSync(new URL("./snapshot.ts", import.meta.url), "utf8"); expect(source).toContain("proxy: {"); expect(source).toContain("autoProxy: args.proxy as boolean | undefined"); - expect(source).toContain("opts.autoProxy"); + expect(source).toContain("serveCapturePage(projectDir, html, opts.autoProxy)"); }); it("pairs every frame with the frame-exact reference frame under --against", () => { diff --git a/packages/cli/src/commands/snapshot.ts b/packages/cli/src/commands/snapshot.ts index c2bb3b2ee5..7bd940c2c4 100644 --- a/packages/cli/src/commands/snapshot.ts +++ b/packages/cli/src/commands/snapshot.ts @@ -463,6 +463,10 @@ export function computeSnapshotTimes( return { times: times.map(round), appendedTail: false }; } +export function serveCapturePage(projectDir: string, html: string, proxy: boolean | undefined) { + return serveStaticProjectHtml(projectDir, html, undefined, [], proxy === true); +} + /** * Render key frames from a composition as PNG screenshots. * The agent can Read these to verify its output visually. @@ -506,7 +510,7 @@ async function captureSnapshots( // Localize fonts (embed remote @font-face as data URIs, matching the render // path) so snapshots render the real font instead of a fallback sans. const html = await bundleWithLocalizedFonts(projectDir); - const server = await serveStaticProjectHtml(projectDir, html, undefined, [], opts.autoProxy); + const server = await serveCapturePage(projectDir, html, opts.autoProxy); const savedPaths: string[] = []; @@ -1052,7 +1056,7 @@ export default defineCommand({ proxy: { type: "boolean", description: - "Auto-transcode browser-hostile video codecs for snapshots (default: on; overrides hyperframes.json media.autoProxy)", + "Serve browser-playable copies of hostile video codecs to the snapshot page (default: off; frames come from FFmpeg, and each copy transcodes the whole clip)", default: undefined, }, "browser-gpu": {