mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 09:16:55 +00:00
fix(cli): a check page forces a WebGL drawing buffer but keeps no list of 2d canvases (#6650)
This commit is contained in:
@@ -336,8 +336,10 @@ export async function openSettledCompositionPage(
|
||||
options.onBrowserReady?.();
|
||||
const page = await chromeBrowser.newPage();
|
||||
await installPageFunctionGuard(page);
|
||||
const { installMediaRenderIdBridge } = await import("@hyperframes/engine");
|
||||
const { installMediaRenderIdBridge, forcePreservedDrawingBuffer } =
|
||||
await import("@hyperframes/engine");
|
||||
await installMediaRenderIdBridge(page);
|
||||
await page.evaluateOnNewDocument(forcePreservedDrawingBuffer);
|
||||
await page.setViewport(viewport);
|
||||
if (answer) await answerPageInProcess(page, url, answer);
|
||||
await options.beforeNavigate?.(page);
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { createServer, type Server } from "node:http";
|
||||
import type { AddressInfo } from "node:net";
|
||||
import { afterAll, beforeAll, expect, it } from "vitest";
|
||||
import { openSettledCompositionPage } from "./captureCompositionFrame.js";
|
||||
|
||||
// A WebGL scene drawn on seek, as a Three.js composition does: with the context's default buffer the
|
||||
// pixels are gone once the frame is presented, so the motion check read every sample as the same blank.
|
||||
const HTML = `<!doctype html><html><body style="margin:0;background:#000">
|
||||
<div id="root" data-composition-id="main" data-width="320" data-height="180" data-duration="1">
|
||||
<canvas id="gl" width="64" height="36"></canvas></div>
|
||||
<script>
|
||||
window.__renderReady = true;
|
||||
const gl = document.getElementById("gl").getContext("webgl");
|
||||
window.draw = (r) => { gl.clearColor(r, 0, 0, 1); gl.clear(gl.COLOR_BUFFER_BIT); };
|
||||
window.draw(1);
|
||||
window.readBack = () => {
|
||||
const scratch = document.createElement("canvas");
|
||||
scratch.width = 1;
|
||||
scratch.height = 1;
|
||||
const context = scratch.getContext("2d");
|
||||
context.drawImage(document.getElementById("gl"), 0, 0, 1, 1);
|
||||
return Array.from(context.getImageData(0, 0, 1, 1).data);
|
||||
};
|
||||
</script></body></html>`;
|
||||
|
||||
let server: Server;
|
||||
let url: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
server = createServer((_req, res) => {
|
||||
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }).end(HTML);
|
||||
});
|
||||
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||||
url = `http://127.0.0.1:${(server.address() as AddressInfo).port}/`;
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
server.closeAllConnections();
|
||||
server.close();
|
||||
});
|
||||
|
||||
it("keeps a WebGL canvas readable after it is presented, so motion drawn on it can be seen", async () => {
|
||||
const { browser, page } = await openSettledCompositionPage(HTML, url, {
|
||||
renderReadyTimeoutMs: 5_000,
|
||||
renderReadyWarningSuffix: "test",
|
||||
browserGpuMode: "software",
|
||||
});
|
||||
try {
|
||||
expect(await page.evaluate(() => (window as any).readBack())).toEqual([255, 0, 0, 255]);
|
||||
// The page keeps no list of the 2d canvases it makes, which the check's own scratch canvases would fill.
|
||||
expect(await page.evaluate(() => "__hf_canvas_2d" in window)).toBe(false);
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
}, 90_000);
|
||||
@@ -115,6 +115,7 @@ export {
|
||||
} from "./services/pageNavigationTimeoutErrorHint.js";
|
||||
|
||||
// ── Frame capture pipeline ──────────────────────────────────────────────────────
|
||||
export { forcePreservedDrawingBuffer } from "./services/drawElementService.js";
|
||||
export {
|
||||
createCaptureSession,
|
||||
initializeSession,
|
||||
|
||||
@@ -86,9 +86,7 @@ export function resolveDrawElementCaptureMode(
|
||||
* captureDrawElementFrame can hide them from paint records and drawImage
|
||||
* their live content underneath the drawElementImage output.
|
||||
*
|
||||
* WebGL contexts additionally get `preserveDrawingBuffer: true` forced —
|
||||
* without it the drawing buffer is cleared after each compositor present and
|
||||
* drawImage(glCanvas) reads blank.
|
||||
* Register forcePreservedDrawingBuffer first: this wrapper only records.
|
||||
*
|
||||
* Must be registered via page.evaluateOnNewDocument BEFORE navigation.
|
||||
*/
|
||||
@@ -107,11 +105,10 @@ export function instrumentAcceleratedCanvases(): void {
|
||||
type: string,
|
||||
attrs?: Record<string, unknown>,
|
||||
) {
|
||||
const isGl = type === "webgl" || type === "webgl2" || type === "experimental-webgl";
|
||||
const isAccel = isGl || type === "webgpu";
|
||||
const finalAttrs = isGl ? { ...attrs, preserveDrawingBuffer: true } : attrs;
|
||||
const isAccel =
|
||||
type === "webgl" || type === "webgl2" || type === "experimental-webgl" || type === "webgpu";
|
||||
// oxlint-disable-next-line no-explicit-any
|
||||
const ctx = (orig as any).call(this, type, finalAttrs);
|
||||
const ctx = (orig as any).call(this, type, attrs);
|
||||
if (ctx && isAccel) {
|
||||
const list = w.__hf_accel_canvases ?? [];
|
||||
if (!list.includes(this)) list.push(this);
|
||||
@@ -131,6 +128,24 @@ export function instrumentAcceleratedCanvases(): void {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* WebGL contexts keep their drawing buffer after a frame is presented, so drawImage(glCanvas) reads the frame
|
||||
* instead of blank. Must be registered via page.evaluateOnNewDocument BEFORE navigation.
|
||||
*/
|
||||
export function forcePreservedDrawingBuffer(): void {
|
||||
const orig = HTMLCanvasElement.prototype.getContext;
|
||||
// oxlint-disable-next-line no-explicit-any
|
||||
(HTMLCanvasElement.prototype as any).getContext = function (
|
||||
this: HTMLCanvasElement,
|
||||
type: string,
|
||||
attrs?: Record<string, unknown>,
|
||||
) {
|
||||
const isGl = type === "webgl" || type === "webgl2" || type === "experimental-webgl";
|
||||
// oxlint-disable-next-line no-explicit-any
|
||||
return (orig as any).call(this, type, isGl ? { ...attrs, preserveDrawingBuffer: true } : attrs);
|
||||
};
|
||||
}
|
||||
|
||||
export interface GpuBackendInfo {
|
||||
/** SwiftShader (software rasterizer) — e.g. Docker headless-shell. */
|
||||
isSwiftShader: boolean;
|
||||
|
||||
@@ -84,6 +84,7 @@ import {
|
||||
injectDrawElementCanvas,
|
||||
captureDrawElementFrame,
|
||||
resolveDrawElementCaptureMode,
|
||||
forcePreservedDrawingBuffer,
|
||||
instrumentAcceleratedCanvases,
|
||||
initDrawElementWorkerEncode,
|
||||
cleanupDrawElementWorkerEncode,
|
||||
@@ -1639,6 +1640,7 @@ async function constructCaptureSession(
|
||||
// composites their live content via drawImage instead (see
|
||||
// instrumentAcceleratedCanvases). Must be registered before navigation.
|
||||
if (useDrawElement) {
|
||||
await page.evaluateOnNewDocument(forcePreservedDrawingBuffer);
|
||||
await page.evaluateOnNewDocument(instrumentAcceleratedCanvases);
|
||||
}
|
||||
// The opacity → autoAlpha tween rewrite was RETIRED with the requestPaint
|
||||
|
||||
Reference in New Issue
Block a user