fix(engine): bound injected video frame resource retention (#5494)

* fix(engine): bound injected video frame resource retention

* perf(engine): bound capture inspection response buffers

* chore(engine): condense capture buffer explanation

* test(engine): cover cross-origin frame GPU uploads
This commit is contained in:
Miguel Ángel
2026-10-12 12:52:44 +00:00
committed by GitHub
parent 86521b94f5
commit fbbc4e88c5
10 changed files with 512 additions and 181 deletions
@@ -3,7 +3,7 @@ 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 type { Browser, Page, PuppeteerNode } from "puppeteer-core";
import { CDPSession, type Browser, type Page, type PuppeteerNode } from "puppeteer-core";
import {
_resetBrowserPoolForTests,
_setPuppeteerForTests,
@@ -18,154 +18,78 @@ describe("createCaptureSession construction ownership", () => {
_setPuppeteerForTests(undefined);
});
it("closes the page and releases its exact browser lease when bootstrap fails", async () => {
const outputDir = mkdtempSync(join(tmpdir(), "hf-session-owner-"));
const page = {
evaluateOnNewDocument: vi.fn().mockRejectedValue(new Error("bootstrap failed")),
close: vi.fn().mockResolvedValue(undefined),
} as unknown as Page;
const browser = {
connected: true,
newPage: vi.fn().mockResolvedValue(page),
version: vi.fn().mockResolvedValue("HeadlessChrome/150.0.0.0"),
close: vi.fn().mockResolvedValue(undefined),
disconnect: vi.fn(),
process: () => null,
} as unknown as Browser;
_setPuppeteerForTests({
launch: vi.fn().mockResolvedValue(browser),
} as unknown as PuppeteerNode);
it.each([
{ stage: "network setup", blocked: "none" },
{ stage: "bootstrap", blocked: "none" },
{ stage: "bootstrap", blocked: "page" },
{ stage: "bootstrap", blocked: "browser" },
])(
"releases the exact lease after $stage failure with $blocked close blocked",
async ({ stage, blocked }) => {
if (blocked !== "none") vi.useFakeTimers();
const outputDir = mkdtempSync(join(tmpdir(), "hf-session-owner-"));
const send = vi.fn().mockResolvedValue(undefined);
if (stage === "network setup") send.mockRejectedValue(new Error("network setup failed"));
const closeResult = (resource: string) =>
blocked === resource ? new Promise<void>(() => {}) : Promise.resolve();
const page = {
mainFrame: () => ({ client: Object.setPrototypeOf({ send }, CDPSession.prototype) }),
evaluateOnNewDocument: vi.fn().mockRejectedValue(new Error("bootstrap failed")),
close: vi.fn(() => closeResult("page")),
} as unknown as Page;
const browser = {
connected: true,
newPage: vi.fn().mockResolvedValue(page),
version: vi.fn().mockResolvedValue("HeadlessChrome/150.0.0.0"),
close: vi.fn(() => closeResult("browser")),
disconnect: vi.fn(),
process: () => null,
} as unknown as Browser;
_setPuppeteerForTests({
launch: vi.fn().mockResolvedValue(browser),
} as unknown as PuppeteerNode);
try {
await expect(
createCaptureSession(
"http://127.0.0.1:3000",
outputDir,
{
width: 320,
height: 180,
fps: { num: 30, den: 1 },
format: "jpeg",
requiresWebGpu: false,
},
null,
{
browserGpuMode: "software",
enableBrowserPool: true,
forceScreenshot: true,
},
),
).rejects.toThrow("bootstrap failed");
try {
const creating = expect(
createCaptureSession(
"http://127.0.0.1:3000",
outputDir,
{
width: 320,
height: 180,
fps: { num: 30, den: 1 },
format: "jpeg",
requiresWebGpu: false,
},
null,
{
browserGpuMode: "software",
enableBrowserPool: true,
forceScreenshot: true,
},
),
).rejects.toThrow(`${stage} failed`);
if (blocked !== "none") await vi.runAllTimersAsync();
await creating;
expect(page.close).toHaveBeenCalledTimes(1);
expect(browser.close).toHaveBeenCalledTimes(1);
} finally {
rmSync(outputDir, { recursive: true, force: true });
}
});
it("force-releases its browser lease when rollback page close never settles", async () => {
vi.useFakeTimers();
const outputDir = mkdtempSync(join(tmpdir(), "hf-session-owner-timeout-"));
const page = {
evaluateOnNewDocument: vi.fn().mockRejectedValue(new Error("bootstrap failed")),
close: vi.fn().mockReturnValue(new Promise<void>(() => {})),
} as unknown as Page;
const disconnect = vi.fn();
const browser = {
connected: true,
newPage: vi.fn().mockResolvedValue(page),
version: vi.fn().mockResolvedValue("HeadlessChrome/150.0.0.0"),
close: vi.fn().mockResolvedValue(undefined),
disconnect,
process: () => null,
} as unknown as Browser;
_setPuppeteerForTests({
launch: vi.fn().mockResolvedValue(browser),
} as unknown as PuppeteerNode);
try {
const creating = expect(
createCaptureSession(
"http://127.0.0.1:3000",
outputDir,
{
width: 320,
height: 180,
fps: { num: 30, den: 1 },
format: "jpeg",
requiresWebGpu: false,
},
null,
{
browserGpuMode: "software",
enableBrowserPool: true,
forceScreenshot: true,
},
),
).rejects.toThrow("bootstrap failed");
await vi.runAllTimersAsync();
await creating;
expect(page.close).toHaveBeenCalledTimes(1);
expect(disconnect).toHaveBeenCalledTimes(1);
} finally {
vi.useRealTimers();
rmSync(outputDir, { recursive: true, force: true });
}
});
it("force-releases its browser lease when rollback browser close never settles", async () => {
vi.useFakeTimers();
const outputDir = mkdtempSync(join(tmpdir(), "hf-session-browser-timeout-"));
const page = {
evaluateOnNewDocument: vi.fn().mockRejectedValue(new Error("bootstrap failed")),
close: vi.fn().mockResolvedValue(undefined),
} as unknown as Page;
const disconnect = vi.fn();
const browser = {
connected: true,
newPage: vi.fn().mockResolvedValue(page),
version: vi.fn().mockResolvedValue("HeadlessChrome/150.0.0.0"),
close: vi.fn().mockReturnValue(new Promise<void>(() => {})),
disconnect,
process: () => null,
} as unknown as Browser;
_setPuppeteerForTests({
launch: vi.fn().mockResolvedValue(browser),
} as unknown as PuppeteerNode);
try {
const creating = expect(
createCaptureSession(
"http://127.0.0.1:3000",
outputDir,
{
width: 320,
height: 180,
fps: { num: 30, den: 1 },
format: "jpeg",
requiresWebGpu: false,
},
null,
{
browserGpuMode: "software",
enableBrowserPool: true,
forceScreenshot: true,
},
),
).rejects.toThrow("bootstrap failed");
await vi.runAllTimersAsync();
await creating;
expect(page.close).toHaveBeenCalledTimes(1);
expect(browser.close).toHaveBeenCalledTimes(1);
expect(disconnect).toHaveBeenCalledTimes(1);
} finally {
vi.useRealTimers();
rmSync(outputDir, { recursive: true, force: true });
}
});
expect(send).toHaveBeenCalledWith("Network.enable", {
maxTotalBufferSize: 32 * 1024 * 1024,
maxResourceBufferSize: 16 * 1024 * 1024,
});
if (stage === "network setup") {
expect(page.evaluateOnNewDocument).not.toHaveBeenCalled();
} else {
expect(send.mock.invocationCallOrder[0]).toBeLessThan(
vi.mocked(page.evaluateOnNewDocument).mock.invocationCallOrder[0] ?? 0,
);
}
expect(page.close).toHaveBeenCalledTimes(1);
if (blocked !== "page") expect(browser.close).toHaveBeenCalledTimes(1);
if (blocked !== "none") expect(browser.disconnect).toHaveBeenCalledTimes(1);
} finally {
vi.useRealTimers();
rmSync(outputDir, { recursive: true, force: true });
}
},
);
});
@@ -25,6 +25,7 @@ vi.mock("./browserManager.js", async (importOriginal) => ({
browser: {
version: async () => "Chrome/150.0.0.0",
newPage: async () => ({
mainFrame: () => ({}),
evaluateOnNewDocument: async () => {},
setViewport: async () => {},
evaluate: async () => undefined,
@@ -21,6 +21,7 @@ vi.mock("./browserManager.js", async (importOriginal) => ({
browser: {
version: async () => "Chrome/150.0.0.0",
newPage: async () => ({
mainFrame: () => ({}),
on: () => {},
goto: async () => null,
createCDPSession: async () => ({ on: () => {}, send: async () => ({}) }),
@@ -9,6 +9,7 @@
*/
import {
CDPSession,
type Browser,
type Page,
type Protocol,
@@ -1414,6 +1415,15 @@ async function constructCaptureSession(
const page = await browser.newPage();
onPageCreated(page);
// Cap the existing session's inspection buffer; new sessions have separate buffers.
const frame = page.mainFrame();
// Frame.client is runtime-only, so keep capture working when it is absent.
if ("client" in frame && frame.client instanceof CDPSession) {
await frame.client.send("Network.enable", {
maxTotalBufferSize: 32 * 1024 * 1024,
maxResourceBufferSize: 16 * 1024 * 1024,
});
}
// Polyfill esbuild's keepNames helper inside the page.
//
// The engine is published as raw TypeScript (`packages/engine/package.json`
@@ -17,6 +17,15 @@ import {
DOM_LAYER_MASK_STYLE_ID,
} from "./screenshotService.js";
// These DOM tests isolate layout/decode from the CDP transport, exercised in Chrome separately.
vi.mock("./videoFrameTransport.js", () => ({
withVideoFrameSources: async (
_page: Page,
updates: Array<{ videoId: string; dataUri: string }>,
inject: (resolved: Array<{ videoId: string; dataUri: string }>) => Promise<string[]>,
) => inject(updates),
}));
// Stub a Page + CDPSession just enough that pageScreenshotCapture can call
// `client.send("Page.captureScreenshot", ...)` and we can inspect the args.
function makeFakePageWithCdp(send: (method: string, params: object) => Promise<{ data: string }>) {
@@ -7,6 +7,7 @@
// fallow-ignore-file code-duplication
import { type Page } from "puppeteer-core";
import { withVideoFrameSources, type VideoFrameUpdate } from "./videoFrameTransport.js";
import { type CaptureOptions } from "../types.js";
import { COLOR_GRADING_SOURCE_HIDDEN_ATTR } from "@hyperframes/core/color-grading";
import {
@@ -643,10 +644,19 @@ export async function injectVideoFramesBatch(
updates: Array<{ videoId: string; dataUri: string }>,
): Promise<string[]> {
if (updates.length === 0) return [];
return withVideoFrameSources(page, updates, (resolved) =>
injectResolvedVideoFramesBatch(page, resolved),
);
}
async function injectResolvedVideoFramesBatch(
page: Page,
updates: VideoFrameUpdate[],
): Promise<string[]> {
return await page.evaluate(
// fallow-ignore-next-line complexity
async (
items: Array<{ videoId: string; dataUri: string; frameId: string }>,
items: Array<VideoFrameUpdate & { frameId: string }>,
visualProperties: string[],
colorGradingSourceHiddenAttr: string,
) => {
@@ -795,8 +805,11 @@ export async function injectVideoFramesBatch(
img.decoding = "sync";
if (img.getAttribute("src") !== item.dataUri) {
if (item.crossOrigin) img.crossOrigin = item.crossOrigin;
else img.removeAttribute("crossorigin");
img.src = item.dataUri;
const source = item.dataUri.startsWith("data:") ? "inline frame" : item.dataUri;
const source =
item.sourceLabel ?? (item.dataUri.startsWith("data:") ? "inline frame" : item.dataUri);
pendingDecodes.push(
img.decode().catch((error: unknown) => {
throw new Error(
@@ -0,0 +1,162 @@
import { spawnSync } from "node:child_process";
import { createServer } from "node:http";
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
import puppeteer, { type Browser, type Page } from "puppeteer-core";
import { resolveHeadlessShellPath } from "./browserManager.js";
import { injectVideoFramesBatch } from "./screenshotService.js";
const chromePath = resolveHeadlessShellPath();
const hasBrowser = chromePath !== undefined && spawnSync(chromePath, ["--version"]).status === 0;
describe.skipIf(!hasBrowser)("video frame transport in Chrome", () => {
let browser: Browser;
let page: Page;
let origin: string;
let redPng: string;
let blueJpeg: string;
let unexpectedFrameRequests = 0;
const server = createServer((req, res) => {
if (req.url?.startsWith("/slow")) return;
if (req.url === "/external.png") {
res.writeHead(200, { "Content-Type": "image/png", "Cache-Control": "max-age=600" });
res.end(Buffer.from(redPng.split(",")[1] ?? "", "base64"));
return;
}
if (req.url?.startsWith("/__hyperframes_video_frames__/")) unexpectedFrameRequests++;
res.writeHead(200, { "Content-Type": "text/html" });
res.end(
'<html><body><div style="position:relative;width:4px;height:4px"><video id="a" style="width:4px;height:4px"></video><video id="b" style="width:4px;height:4px"></video></div></body></html>',
);
});
beforeAll(async () => {
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
const address = server.address();
if (!address || typeof address === "string") throw new Error("No test server address");
origin = `http://127.0.0.1:${address.port}`;
browser = await puppeteer.launch({
executablePath: chromePath,
headless: true,
args: ["--no-sandbox", "--disable-dev-shm-usage"],
});
});
beforeEach(async () => {
if (page) await page.close();
page = await browser.newPage();
await page.goto(origin);
[redPng, blueJpeg] = await Promise.all([
frameDataUri("red"),
frameDataUri("blue", "image/jpeg"),
]);
unexpectedFrameRequests = 0;
});
afterAll(async () => {
await browser?.close();
server.closeAllConnections();
await new Promise<void>((resolve) => server.close(() => resolve()));
});
const frameDataUri = (color: string, format = "image/png") =>
page.evaluate(
(fill, mime) => {
const canvas = document.createElement("canvas");
canvas.width = canvas.height = 4;
const ctx = canvas.getContext("2d");
if (!ctx) throw new Error("No 2D context");
ctx.fillStyle = fill;
ctx.fillRect(0, 0, 4, 4);
return canvas.toDataURL(mime, 1);
},
color,
format,
);
const pixels = () =>
page.evaluate(() => {
const canvas = new OffscreenCanvas(4, 4);
const ctx = canvas.getContext("2d");
if (!ctx) throw new Error("No 2D context");
return Array.from(
document.querySelectorAll<HTMLImageElement>("img.__render_frame__"),
(img) => {
ctx.clearRect(0, 0, 4, 4);
ctx.drawImage(img, 0, 0);
return { src: img.src, rgba: Array.from(ctx.getImageData(0, 0, 1, 1).data) };
},
);
});
it("delivers independent decoded frames without retaining inline image URLs", async () => {
const headers: string[] = [];
page.on("response", (response) => {
if (response.request().resourceType() === "image")
headers.push(response.headers()["cache-control"] ?? "");
});
await injectVideoFramesBatch(page, [
{ videoId: "a", dataUri: redPng },
{ videoId: "b", dataUri: blueJpeg },
]);
const first = await pixels();
expect(first.map((img) => img.rgba)).toEqual([
[255, 0, 0, 255],
[0, 0, 254, 255],
]);
expect(first.every((img) => img.src.startsWith(origin))).toBe(true);
expect(headers).toEqual(["no-store", "no-store"]);
await injectVideoFramesBatch(page, [{ videoId: "a", dataUri: blueJpeg }]);
expect((await pixels())[0]?.rgba).toEqual([0, 0, 254, 255]);
expect(await page.evaluate(async (url) => (await fetch(url)).status, first[0]?.src ?? "")).toBe(
404,
);
expect(unexpectedFrameRequests).toBe(0);
});
it("loads frames even while all same-origin download connections are busy", async () => {
await page.evaluate(() => {
for (let i = 0; i < 6; i++) void fetch(`/slow${i}.mp4`);
});
await injectVideoFramesBatch(page, [{ videoId: "a", dataUri: redPng }]);
expect((await pixels())[0]?.rgba).toEqual([255, 0, 0, 255]);
}, 5000);
it.each(["HTTP", "about:blank"])(
"preserves PNG alpha and GPU uploads from %s",
async (source) => {
if (source === "about:blank") {
const content = await page.content();
await page.close();
page = await browser.newPage();
await page.setContent(content);
expect(page.url()).toBe("about:blank");
}
const translucent = await frameDataUri("rgba(255,0,0,0.5)");
await injectVideoFramesBatch(page, [{ videoId: "a", dataUri: translucent }]);
expect((await pixels())[0]?.rgba).toEqual([255, 0, 0, 128]);
expect(
await page.evaluate(() => {
const canvas = document.createElement("canvas");
const gl = canvas.getContext("webgl");
const img = document.querySelector("img");
if (!gl || !img) throw new Error("No GPU texture source");
gl.bindTexture(gl.TEXTURE_2D, gl.createTexture());
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img);
return gl.getError();
}),
).toBe(0);
},
);
it("releases failed frames and preserves the video-specific decode error", async () => {
await expect(
injectVideoFramesBatch(page, [{ videoId: "a", dataUri: "data:image/png;base64,AAAA" }]),
).rejects.toThrow('Video frame for "a" failed to load (inline frame)');
const src = await page.evaluate(() => document.querySelector("img")?.src ?? "");
expect(await page.evaluate(async (url) => (await fetch(url)).status, src)).toBe(404);
});
it("keeps caller-provided frame URLs unchanged", async () => {
const src = `${origin}/external.png`;
await injectVideoFramesBatch(page, [{ videoId: "a", dataUri: src }]);
expect((await pixels())[0]).toEqual({ src, rgba: [255, 0, 0, 255] });
});
});
@@ -0,0 +1,115 @@
import { EventEmitter } from "node:events";
import { describe, expect, it, vi } from "vitest";
import { type Page } from "puppeteer-core";
import { withVideoFrameSources, type VideoFrameUpdate } from "./videoFrameTransport.js";
const update = { videoId: "clip", dataUri: "data:image/png;base64,AAAA" };
function fixture() {
const events = new EventEmitter();
const send = vi.fn(async (_method: string, _params: unknown) => ({}));
const detach = vi.fn(async () => {});
const close = new EventEmitter();
const createCDPSession = vi.fn(async () => ({ on: events.on.bind(events), send, detach }));
// The transport only needs these Page methods; no browser protocol is simulated elsewhere.
const page = {
url: () => "http://localhost:4000/index.html",
createCDPSession,
once: close.once.bind(close),
} as unknown as Page;
const request = async (url: string) => {
events.emit("Fetch.requestPaused", { requestId: "request", request: { url } });
await new Promise<void>((resolve) => setImmediate(resolve));
return send.mock.calls.findLast(([method]) => method === "Fetch.fulfillRequest")?.[1];
};
return { page, send, detach, createCDPSession, close, request };
}
describe("video frame transport ownership", () => {
it.each(["/frames/0001.png", "data:frame-test", "data:image/png,not-base64"])(
"does not intercept an unsupported source %s",
async (dataUri) => {
const f = fixture();
const updates = [{ videoId: "clip", dataUri }];
expect(await withVideoFrameSources(f.page, updates, async (resolved) => resolved)).toBe(
updates,
);
expect(f.createCDPSession).not.toHaveBeenCalled();
},
);
it("releases success, failure and skipped batches without retaining response bytes", async () => {
const f = fixture();
for (const failure of [false, true]) {
let url = "";
const result = withVideoFrameSources(f.page, [update], async (resolved) => {
url = resolved[0]?.dataUri ?? "";
expect(await f.request(url)).toMatchObject({ responseCode: 200, body: "AAAA" });
if (failure) throw new Error("decode failed");
return ["clip"];
});
if (failure) await expect(result).rejects.toThrow("decode failed");
else await expect(result).resolves.toEqual(["clip"]);
expect(await f.request(url)).toMatchObject({ responseCode: 404, body: "" });
}
let skipped = "";
await withVideoFrameSources(f.page, [update], async (resolved) => {
skipped = resolved[0]?.dataUri ?? "";
return [];
});
expect(await f.request(skipped)).toMatchObject({ responseCode: 404, body: "" });
expect(f.createCDPSession).toHaveBeenCalledTimes(1);
});
it("keeps overlapping batches isolated and initializes the page only once", async () => {
const f = fixture();
let finish: () => void = () => {};
const hold = new Promise<void>((resolve) => {
finish = resolve;
});
let first: VideoFrameUpdate[] = [];
const pending = withVideoFrameSources(f.page, [update], async (resolved) => {
first = resolved;
await hold;
});
await withVideoFrameSources(f.page, [update], async (resolved) => {
expect(resolved[0]?.dataUri).not.toBe(first[0]?.dataUri);
expect(await f.request(resolved[0]?.dataUri ?? "")).toMatchObject({ responseCode: 200 });
});
expect(await f.request(first[0]?.dataUri ?? "")).toMatchObject({ responseCode: 200 });
finish();
await pending;
expect(await f.request(first[0]?.dataUri ?? "")).toMatchObject({ responseCode: 404 });
expect(f.createCDPSession).toHaveBeenCalledTimes(1);
});
it("unblocks a failed protocol response and surfaces its cause", async () => {
const f = fixture();
const error = new Error("protocol response failed");
f.send.mockImplementation(async (method) => {
if (method === "Fetch.fulfillRequest") throw error;
return {};
});
await expect(
withVideoFrameSources(f.page, [update], async (resolved) => {
await f.request(resolved[0]?.dataUri ?? "");
throw new Error("image decode failed");
}),
).rejects.toBe(error);
expect(f.send).toHaveBeenCalledWith("Fetch.failRequest", {
requestId: "request",
errorReason: "Failed",
});
});
it("detaches failed initialization and permits a retry", async () => {
const f = fixture();
f.send.mockRejectedValueOnce(new Error("cannot intercept"));
await expect(withVideoFrameSources(f.page, [update], async () => {})).rejects.toThrow(
"cannot intercept",
);
expect(f.detach).toHaveBeenCalledTimes(1);
await withVideoFrameSources(f.page, [update], async () => {});
expect(f.createCDPSession).toHaveBeenCalledTimes(2);
});
});
@@ -0,0 +1,121 @@
import { randomUUID } from "node:crypto";
import { type CDPSession, type Page } from "puppeteer-core";
export interface VideoFrameUpdate {
videoId: string;
dataUri: string;
crossOrigin?: "anonymous";
sourceLabel?: string;
}
interface FrameResponse {
body: string;
contentType: string;
error?: unknown;
}
interface FrameTransport {
prefix: string;
nextId: number;
responses: Map<string, FrameResponse>;
}
const base64ImagePrefix = /^data:(image\/[\w.+-]+);base64,/;
const transports = new WeakMap<Page, Promise<FrameTransport>>();
async function createTransport(page: Page): Promise<FrameTransport> {
const pageUrl = new URL(page.url());
const origin = /^https?:$/.test(pageUrl.protocol)
? pageUrl.origin
: "https://hyperframes-render.invalid";
const prefix = `${origin}/__hyperframes_video_frames__/${randomUUID()}/`;
const responses = new Map<string, FrameResponse>();
const client = await page.createCDPSession();
client.on("Fetch.requestPaused", (event) => {
const frame = responses.get(event.request.url);
void fulfillFrame(client, event.requestId, frame);
});
try {
await client.send("Fetch.enable", {
patterns: [{ urlPattern: `${prefix}*`, requestStage: "Request" }],
});
} catch (error) {
await client.detach().catch(() => {});
throw error;
}
page.once("close", () => responses.clear());
return { prefix, nextId: 0, responses };
}
async function fulfillFrame(
client: CDPSession,
requestId: string,
frame: FrameResponse | undefined,
): Promise<void> {
try {
await client.send("Fetch.fulfillRequest", {
requestId,
responseCode: frame ? 200 : 404,
responseHeaders: [
{ name: "Content-Type", value: frame?.contentType ?? "text/plain" },
{ name: "Cache-Control", value: "no-store" },
{ name: "Access-Control-Allow-Origin", value: "*" },
],
body: frame?.body ?? "",
});
} catch (error) {
if (frame) frame.error = error;
// A failed protocol response must unblock img.decode, including on page closure.
await client.send("Fetch.failRequest", { requestId, errorReason: "Failed" }).catch(() => {});
}
}
/**
* Inline image URLs outlive the Node LRU in Chrome's resource cache. Deliver
* their bytes as no-store responses over CDP instead: the page sees short URLs,
* and frame requests never wait for the source videos' HTTP connections.
*/
export async function withVideoFrameSources<T>(
page: Page,
updates: VideoFrameUpdate[],
inject: (resolved: VideoFrameUpdate[]) => Promise<T>,
): Promise<T> {
if (!updates.some((update) => base64ImagePrefix.test(update.dataUri))) return inject(updates);
let pending = transports.get(page);
if (!pending) {
pending = createTransport(page);
transports.set(page, pending);
}
let transport: FrameTransport;
try {
transport = await pending;
} catch (error) {
transports.delete(page);
throw error;
}
const registered: Array<{ url: string; frame: FrameResponse }> = [];
try {
const resolved = updates.map((update) => {
const match = base64ImagePrefix.exec(update.dataUri);
if (!match) return update;
const frame = {
contentType: match[1] ?? "image/png",
body: update.dataUri.slice(match[0].length),
};
const url = `${transport.prefix}${transport.nextId++}`;
transport.responses.set(url, frame);
registered.push({ url, frame });
return {
...update,
dataUri: url,
crossOrigin: "anonymous" as const,
sourceLabel: "inline frame",
};
});
return await inject(resolved);
} catch (error) {
throw registered.find(({ frame }) => frame.error !== undefined)?.frame.error ?? error;
} finally {
for (const { url } of registered) transport.responses.delete(url);
}
}
@@ -3517,36 +3517,11 @@ async function executeRenderPipeline(input: {
});
},
});
// The URL-served frame path (PR #596) hands each injected `<img>` a
// fileServer URL instead of a base64 data URI, on the theory that
// shipping a short URL through `page.evaluate` beats shipping a
// multi-MB base64 string per frame. That holds when the fileServer
// is otherwise idle — but on video-heavy compositions, the same
// fileServer also serves every `<video>.src`. The runtime's
// drift-recovery branch (`runtime/media.ts:294-302`) issues
// `el.load()` on the underlying `<video>` during seeks, kicking off
// full-file downloads that occupy the fileServer's single Node
// event loop (it uses `readFileSync` and offers no `Accept-Ranges`).
// The injector's `<img>.decode()` then queues behind those video
// fetches and is never serviced before puppeteer's protocol timeout
// fires (`Runtime.callFunctionOn timed out`).
//
// Repro: synth 30 × 32 MB videos / 90 s comp on an 8-core / 30 GB
// host = 537 s wall (broken corpus) / 428 s (corpus-fixed), every
// render fails. Disabling the resolver (force base64-inline) gives
// 1:59 (119 s) wall and a clean MP4 on the same comp, with no
// regression on the 30 × 1.6 MB control corpus (137 s vs 135 s
// baseline).
//
// Until this is properly gated (e.g. only enable URL-served when the
// page has zero fileServer-bound `<video>.src` traffic), the inline
// path is the safe default. The cache memory ceiling
// (`frameDataUriCacheBytesLimitMb`, default 1500 MB above 8 GB
// hosts) already bounds the cost. `createCompiledFrameSrcResolver`
// and the `frameSrcResolver` option remain in their respective
// modules (`packages/producer/src/services/render/shared.ts`,
// `packages/engine/src/services/videoFrameInjector.ts`); the gating
// PR will re-import the builder here.
// The engine delivers cached frame bytes as no-store CDP responses.
// Frame requests bypass the file server's source-video downloads, which
// can occupy all HTTP connections during native-video drift recovery.
// The byte ceiling bounds the Node LRU; Chrome receives short URLs so
// its resource cache does not retain the corresponding inline strings.
const createRenderVideoFrameInjector = (): BeforeCaptureHook | null =>
createVideoFrameInjector(frameLookup, {
frameDataUriCacheLimit: cfg.frameDataUriCacheLimit,