fix(producer): keep video captures viewport-bound on software (#1788)

This commit is contained in:
Miguel Ángel
2026-06-29 15:29:19 -07:00
committed by GitHub
parent f3177872a1
commit c9613cd826
4 changed files with 56 additions and 2 deletions
@@ -58,6 +58,7 @@ import {
import { defaultLogger } from "../../logger.js";
import { runEncodeStage } from "../render/stages/encodeStage.js";
import { runCaptureStage } from "../render/stages/captureStage.js";
import { resolveVideoCaptureBeyondViewport } from "../render/captureBeyondViewport.js";
import {
type ChunkSliceJson,
type LockedRenderConfig,
@@ -480,6 +481,11 @@ export async function renderChunk(
)
: null;
const videoCaptureBeyondViewport = resolveVideoCaptureBeyondViewport(
planVideos?.videos.length ?? 0,
"software",
);
// ── Per-chunk work + frames directories ──
// Suffix workDir with pid + random bytes so concurrent invocations on
// the SAME `(planDir, chunkIndex)` (e.g. a scheduler that double-fires
@@ -518,7 +524,9 @@ export async function renderChunk(
// declare `data-composition-variables` leave this undefined and the
// engine skips the `evaluateOnNewDocument` injection.
variables: encoder.variables,
...((planVideos?.videos.length ?? 0) > 0 ? { captureBeyondViewport: true } : {}),
...(videoCaptureBeyondViewport !== undefined
? { captureBeyondViewport: videoCaptureBeyondViewport }
: {}),
// lock the BeginFrame warmup loop to a fixed iteration count so
// `beginFrameTimeTicks` is host-independent. Only chunks ever set this.
lockWarmupTicks: true,
@@ -0,0 +1,17 @@
import { describe, expect, it } from "vitest";
import { resolveVideoCaptureBeyondViewport } from "./captureBeyondViewport.js";
describe("resolveVideoCaptureBeyondViewport", () => {
it("leaves no-video renders on the engine default", () => {
expect(resolveVideoCaptureBeyondViewport(0, "software")).toBeUndefined();
expect(resolveVideoCaptureBeyondViewport(0, "hardware")).toBeUndefined();
});
it("keeps video renders on the fast viewport-bound path under software rendering", () => {
expect(resolveVideoCaptureBeyondViewport(1, "software")).toBe(false);
});
it("preserves the beyond-viewport video workaround under hardware rendering", () => {
expect(resolveVideoCaptureBeyondViewport(1, "hardware")).toBe(true);
});
});
@@ -0,0 +1,14 @@
export type ResolvedBrowserGpuMode = "software" | "hardware";
/**
* Native video surfaces can need Chrome's beyond-viewport compositor on
* hardware-accelerated captures, but that path is a full-surface software
* re-rasterization tax on SwiftShader/CPU render hosts.
*/
export function resolveVideoCaptureBeyondViewport(
videoCount: number,
browserGpuMode: ResolvedBrowserGpuMode,
): boolean | undefined {
if (videoCount <= 0) return undefined;
return browserGpuMode === "hardware";
}
@@ -67,6 +67,8 @@ import {
LOW_MEMORY_TOTAL_MB_THRESHOLD,
assertConfiguredFfmpegBinariesExist,
type CapturePerfSummary,
resolveBrowserGpuMode,
resolveHeadlessShellPath,
} from "@hyperframes/engine";
import { join, dirname, resolve } from "path";
import { randomUUID } from "crypto";
@@ -86,6 +88,7 @@ import { formatCaptureFrameName } from "../utils/paths.js";
import { resolveEffectiveHdrMode } from "./render/hdrMode.js";
import { buildRenderPerfSummary, pushWorkerDedupPerfs } from "./render/perfSummary.js";
import { getCaptureStageBrowserConsole } from "./render/captureStageError.js";
import { resolveVideoCaptureBeyondViewport } from "./render/captureBeyondViewport.js";
import {
type CaptureCalibrationSample,
type CaptureCostEstimate,
@@ -1246,6 +1249,16 @@ export async function executeRenderJob(
const framesDir = join(workDir, "captured-frames");
if (!existsSync(framesDir)) mkdirSync(framesDir, { recursive: true });
const resolvedBrowserGpuMode = await resolveBrowserGpuMode(cfg.browserGpuMode, {
chromePath: resolveHeadlessShellPath(cfg),
browserTimeout: cfg.browserTimeout,
});
updateCaptureObservability({ browserGpuMode: resolvedBrowserGpuMode });
const videoCaptureBeyondViewport = resolveVideoCaptureBeyondViewport(
composition.videos.length,
resolvedBrowserGpuMode,
);
const captureOptions: CaptureOptions = {
width,
height,
@@ -1254,7 +1267,9 @@ export async function executeRenderJob(
quality: needsAlpha ? undefined : job.config.quality === "draft" ? 80 : 95,
variables: job.config.variables,
deviceScaleFactor,
...(composition.videos.length > 0 ? { captureBeyondViewport: true } : {}),
...(videoCaptureBeyondViewport !== undefined
? { captureBeyondViewport: videoCaptureBeyondViewport }
: {}),
};
resolvedCaptureBeyondViewport =
captureOptions.captureBeyondViewport ?? resolvedCaptureBeyondViewport;