mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
feat(engine,cli): drawElement fast-capture config + CLI flag (#1916)
## drawElement fast-capture — config + CLI flag (stack 1/6) Foundation layer for the drawElement fast-capture feature: the config surface and CLI/Docker plumbing that the rest of the stack builds on. ### What this adds - **`packages/engine/src/config.ts`** — new config fields for fast capture: `useDrawElement` / `enableDrawElementWorkerEncode` (macOS-GPU `drawElementImage` capture + worker-offloaded JPEG encode), resolved from env in `resolveConfig` (env `HF_DE_WORKER_ENCODE`). Wired alongside main's existing `staticFrameDedup` (unified downstream in 4/6). - **`packages/cli/src/commands/render.ts`** — `--experimental-fast-capture` flag → sets `experimentalFastCapture`; `--debug` passthrough. - **`packages/cli/src/utils/dockerRunArgs.ts`** — pass the fast-capture env through to the container. - **`.github/workflows/fast-video-validation.yml`** — CI job validating fast-capture renders. - `.oxlintrc.json` / `.fallowrc.jsonc` — ignore-pattern housekeeping for the new paths. ### Notes - Config-only + entrypoint; no capture behavior yet (that's 2/6–4/6). - Tests: `config.test.ts`, `dockerRunArgs.test.ts` added. --- **Stack (drawElement fast-capture, rebased onto current `main`, supersedes #1295 + #1444):** 1. **#1916 config + CLI** ← you are here 2. #1917 drawElementImage capture service 3. #1918 3D projection + compositor-effect risk gate 4. #1919 frame-capture core (routing, worker-encode, static-dedup unification) 5. #1920 producer render stages + remote bg-image localizer 6. #1921 lint rule + player media sync ⚠️ Intermediate PRs (1–5) are split by package boundary for review and **do not each compile independently** (cross-file deps); the complete feature is green at the stack tip (#1921) — tsc-clean on engine + producer, 231 tests pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
@@ -358,6 +358,18 @@ export default defineCommand({
|
||||
"memory thrash on constrained machines. Default: auto-detected from " +
|
||||
"total RAM (<= 8 GB). Env: PRODUCER_LOW_MEMORY_MODE.",
|
||||
},
|
||||
"experimental-fast-capture": {
|
||||
type: "boolean",
|
||||
description:
|
||||
"EXPERIMENTAL. Capture frames via Chrome's drawElementImage API " +
|
||||
"instead of Page.captureScreenshot — reads DOM paint records directly, " +
|
||||
"~46% faster on GPU. Transparent (PNG) renders on SwiftShader (Docker) " +
|
||||
"auto-fall back to screenshot capture. Incompatible with page-side " +
|
||||
"shader compositing. Default: false. Env: PRODUCER_EXPERIMENTAL_FAST_CAPTURE.",
|
||||
// No `default` — an omitted flag must stay `undefined` so the `!= null`
|
||||
// guard below leaves PRODUCER_EXPERIMENTAL_FAST_CAPTURE untouched and the
|
||||
// env fallback survives (matches the --low-memory-mode idiom).
|
||||
},
|
||||
},
|
||||
// `run` is the citty handler for `hyperframes render` — sequential flag
|
||||
// validation + render dispatch. Inherited CRITICAL on main (CRAP 1290);
|
||||
@@ -513,6 +525,13 @@ export default defineCommand({
|
||||
process.env.PRODUCER_LOW_MEMORY_MODE = args["low-memory-mode"] ? "true" : "false";
|
||||
}
|
||||
|
||||
// ── Override: experimental fast capture (drawElementImage) ───────────
|
||||
if (args["experimental-fast-capture"] != null) {
|
||||
process.env.PRODUCER_EXPERIMENTAL_FAST_CAPTURE = args["experimental-fast-capture"]
|
||||
? "true"
|
||||
: "false";
|
||||
}
|
||||
|
||||
// ── Validate max-concurrent-renders ─────────────────────────────────
|
||||
if (args["max-concurrent-renders"] != null) {
|
||||
const parsed = parseInt(args["max-concurrent-renders"], 10);
|
||||
@@ -915,6 +934,7 @@ export default defineCommand({
|
||||
entryFile,
|
||||
outputResolution,
|
||||
pageSideCompositing: args["page-side-compositing"] !== false,
|
||||
experimentalFastCapture: args["experimental-fast-capture"] === true,
|
||||
pageNavigationTimeoutMs,
|
||||
protocolTimeout,
|
||||
playerReadyTimeout,
|
||||
@@ -984,6 +1004,8 @@ interface RenderOptions {
|
||||
/** Output resolution preset; see `resolveDeviceScaleFactor` for constraints. */
|
||||
outputResolution?: CanvasResolution;
|
||||
pageSideCompositing?: boolean;
|
||||
/** EXPERIMENTAL. drawElementImage frame capture (--experimental-fast-capture). */
|
||||
experimentalFastCapture?: boolean;
|
||||
/**
|
||||
* Puppeteer `page.goto()` timeout for the entry HTML, in milliseconds.
|
||||
* When omitted, the engine default (60s) applies. Surfaced as
|
||||
@@ -1293,6 +1315,7 @@ async function renderDocker(
|
||||
outputResolution: options.outputResolution,
|
||||
pageSideCompositing: options.pageSideCompositing,
|
||||
debug: options.debug,
|
||||
experimentalFastCapture: options.experimentalFastCapture,
|
||||
pageNavigationTimeoutMs: options.pageNavigationTimeoutMs,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -173,6 +173,7 @@ describe("buildDockerRunArgs", () => {
|
||||
quiet: true,
|
||||
debug: true,
|
||||
entryFile: "compositions/intro.html",
|
||||
experimentalFastCapture: true,
|
||||
},
|
||||
});
|
||||
// Each value must reach the container exactly once. If a future option
|
||||
@@ -195,6 +196,24 @@ describe("buildDockerRunArgs", () => {
|
||||
expect(args).toContain("--hdr");
|
||||
expect(args).toContain("--composition");
|
||||
expect(args).toContain("compositions/intro.html");
|
||||
expect(args).toContain("--experimental-fast-capture");
|
||||
});
|
||||
|
||||
it("forwards --experimental-fast-capture only when enabled", () => {
|
||||
const on = buildDockerRunArgs({
|
||||
...FIXED_INPUT,
|
||||
options: { ...BASE, experimentalFastCapture: true },
|
||||
});
|
||||
expect(on).toContain("--experimental-fast-capture");
|
||||
|
||||
const off = buildDockerRunArgs({
|
||||
...FIXED_INPUT,
|
||||
options: { ...BASE, experimentalFastCapture: false },
|
||||
});
|
||||
expect(off).not.toContain("--experimental-fast-capture");
|
||||
|
||||
const absent = buildDockerRunArgs({ ...FIXED_INPUT, options: BASE });
|
||||
expect(absent).not.toContain("--experimental-fast-capture");
|
||||
});
|
||||
|
||||
it("forwards --format png-sequence to the container", () => {
|
||||
|
||||
@@ -56,6 +56,8 @@ export interface DockerRenderOptions {
|
||||
/** Output resolution preset (e.g. "landscape-4k"). Forwarded as `--resolution`. */
|
||||
outputResolution?: string;
|
||||
pageSideCompositing?: boolean;
|
||||
/** EXPERIMENTAL. drawElementImage frame capture; forwarded as `--experimental-fast-capture`. */
|
||||
experimentalFastCapture?: boolean;
|
||||
/**
|
||||
* Puppeteer page-navigation timeout, in milliseconds. Forwarded to the
|
||||
* in-container CLI as `--browser-timeout <seconds>` (the CLI takes
|
||||
@@ -144,6 +146,7 @@ export function buildDockerRunArgs(input: DockerRunArgsInput): string[] {
|
||||
...(options.entryFile ? ["--composition", options.entryFile] : []),
|
||||
...(options.outputResolution ? ["--resolution", options.outputResolution] : []),
|
||||
...(options.pageSideCompositing === false ? ["--no-page-side-compositing"] : []),
|
||||
...(options.experimentalFastCapture ? ["--experimental-fast-capture"] : []),
|
||||
...(options.pageNavigationTimeoutMs != null
|
||||
? ["--browser-timeout", String(options.pageNavigationTimeoutMs / 1000)]
|
||||
: []),
|
||||
|
||||
@@ -221,6 +221,36 @@ describe("resolveConfig", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("useDrawElement (PRODUCER_EXPERIMENTAL_FAST_CAPTURE)", () => {
|
||||
it("defaults to false", () => {
|
||||
const config = resolveConfig();
|
||||
expect(config.useDrawElement).toBe(false);
|
||||
});
|
||||
|
||||
it("enabled when PRODUCER_EXPERIMENTAL_FAST_CAPTURE=true", () => {
|
||||
setEnv("PRODUCER_EXPERIMENTAL_FAST_CAPTURE", "true");
|
||||
const config = resolveConfig();
|
||||
expect(config.useDrawElement).toBe(true);
|
||||
});
|
||||
|
||||
it("explicit override wins over the env var", () => {
|
||||
setEnv("PRODUCER_EXPERIMENTAL_FAST_CAPTURE", "true");
|
||||
const config = resolveConfig({ useDrawElement: false });
|
||||
expect(config.useDrawElement).toBe(false);
|
||||
});
|
||||
|
||||
it("forces page-side compositing off when enabled (incompatible strategies)", () => {
|
||||
const config = resolveConfig({ useDrawElement: true, enablePageSideCompositing: true });
|
||||
expect(config.useDrawElement).toBe(true);
|
||||
expect(config.enablePageSideCompositing).toBe(false);
|
||||
});
|
||||
|
||||
it("leaves page-side compositing on when fast capture is off", () => {
|
||||
const config = resolveConfig({ useDrawElement: false });
|
||||
expect(config.enablePageSideCompositing).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("lowMemoryMode", () => {
|
||||
it("forces on for truthy PRODUCER_LOW_MEMORY_MODE values", () => {
|
||||
setEnv("PRODUCER_LOW_MEMORY_MODE", "true");
|
||||
|
||||
@@ -63,6 +63,21 @@ export interface EngineConfig {
|
||||
* `HF_STATIC_DEDUP` in {false,0,off}. Only arms in screenshot capture mode.
|
||||
*/
|
||||
staticFrameDedup: boolean;
|
||||
/**
|
||||
* EXPERIMENTAL. Use drawElementImage for frame capture (requires the
|
||||
* CanvasDrawElement Chrome flag, added globally in buildChromeArgs).
|
||||
* Surfaced via the CLI `--experimental-fast-capture` flag.
|
||||
* Env fallback: `PRODUCER_EXPERIMENTAL_FAST_CAPTURE`.
|
||||
*/
|
||||
useDrawElement: boolean;
|
||||
/**
|
||||
* EXPERIMENTAL. Pipeline JPEG encode into an in-page OffscreenCanvas Worker
|
||||
* for the drawElement fast-capture path (macOS hardware GPU only). The worker
|
||||
* encodes frame N while the main thread seeks+paints frame N+1, targeting
|
||||
* ~1.65–1.96× wall-time speedup. No-op unless `useDrawElement` is also true.
|
||||
* Default: off. Env: `HF_DE_WORKER_ENCODE=true`.
|
||||
*/
|
||||
enableDrawElementWorkerEncode: boolean;
|
||||
/**
|
||||
* Low-memory render profile. When `true`, the orchestrator collapses the
|
||||
* pipeline to its cheapest shape on memory-constrained hosts: it skips the
|
||||
@@ -235,6 +250,8 @@ export const DEFAULT_CONFIG: EngineConfig = {
|
||||
protocolTimeout: 300_000,
|
||||
forceScreenshot: false,
|
||||
staticFrameDedup: true,
|
||||
useDrawElement: false,
|
||||
enableDrawElementWorkerEncode: false,
|
||||
// Auto-detected per host in `resolveConfig`; defaults off for the raw
|
||||
// DEFAULT_CONFIG (used directly by tests and worker-sizing fallbacks).
|
||||
lowMemoryMode: false,
|
||||
@@ -412,6 +429,11 @@ export function resolveConfig(overrides?: Partial<EngineConfig>): EngineConfig {
|
||||
|
||||
forceScreenshot: envBool("PRODUCER_FORCE_SCREENSHOT", DEFAULT_CONFIG.forceScreenshot),
|
||||
staticFrameDedup: resolveStaticFrameDedup(),
|
||||
useDrawElement: envBool("PRODUCER_EXPERIMENTAL_FAST_CAPTURE", DEFAULT_CONFIG.useDrawElement),
|
||||
enableDrawElementWorkerEncode: envBool(
|
||||
"HF_DE_WORKER_ENCODE",
|
||||
DEFAULT_CONFIG.enableDrawElementWorkerEncode,
|
||||
),
|
||||
lowMemoryMode: resolveLowMemoryMode(),
|
||||
enablePageSideCompositing: envBool(
|
||||
"HF_PAGE_SIDE_COMPOSITING",
|
||||
@@ -493,6 +515,18 @@ export function resolveConfig(overrides?: Partial<EngineConfig>): EngineConfig {
|
||||
...cleanEnv,
|
||||
...overrides,
|
||||
};
|
||||
|
||||
// drawElement capture and page-side shader compositing are mutually
|
||||
// incompatible capture strategies (drawElement reads paint records directly
|
||||
// and bypasses the page-side prepare→composite→resolve protocol). When
|
||||
// experimental fast capture is on, force page-side compositing off so shader
|
||||
// transitions fall back to the Node-side layered blend rather than silently
|
||||
// dropping. This keeps the flag self-consistent and avoids a per-session
|
||||
// incompatibility warning on every fast-capture render.
|
||||
if (merged.useDrawElement) {
|
||||
merged.enablePageSideCompositing = false;
|
||||
}
|
||||
|
||||
return {
|
||||
...merged,
|
||||
vp9CpuUsed: normalizeVp9CpuUsed(merged.vp9CpuUsed),
|
||||
|
||||
Reference in New Issue
Block a user