fix(cli): snapshot no longer transcodes every iPhone clip it never plays (#5755)

* fix(cli): snapshot no longer transcodes every iPhone clip it never plays

snapshot served Studio's media proxies by default, so its page server transcoded every browser-hostile clip (every HEVC clip) in full at load. Snapshots show frames extracted with FFmpeg and never use those copies. Proxies are now served only with --proxy; hyperframes.json media.autoProxy no longer turns them on for snapshots.

* docs: snapshot makes no media proxy unless asked
This commit is contained in:
Miguel Ángel
2026-10-19 14:36:01 +00:00
committed by GitHub
parent 6c3cc7cbe9
commit 2927bf0303
4 changed files with 68 additions and 5 deletions
+2 -1
View File
@@ -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
@@ -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<typeof import("@hyperframes/studio-server/proxy-transcoder")>()),
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 = '<html><head></head><body><video src="clip.mov"></video></body></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);
});
});
+2 -2
View File
@@ -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", () => {
+6 -2
View File
@@ -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": {