mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 12:54:29 +00:00
fix(producer): keep video captures viewport-bound on software (#1788)
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user