mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-07 10:06:21 +00:00
fix(render): consolidate preflight and local recovery (#2403)
* fix(render): fall back when libx264 is unavailable * fix(render): recover orphaned browsers before retry * fix(render): check disk space on write volumes * test(engine): accept resolved ffmpeg binary paths * fix(render): harden H.264 capability fallback * chore(ci): scope inherited Fallow findings * fix(render): diagnose encoder probe failures
This commit is contained in:
@@ -72,6 +72,15 @@ const preflightState = vi.hoisted(() => ({
|
||||
},
|
||||
}));
|
||||
|
||||
const ffmpegEncoderState = vi.hoisted(() => ({
|
||||
mode: "software" as "software" | "gpu",
|
||||
error: null as Error | null,
|
||||
}));
|
||||
const orphanCleanupState = vi.hoisted(() => ({
|
||||
calls: 0,
|
||||
killed: 0,
|
||||
}));
|
||||
|
||||
vi.mock("../utils/producer.js", () => ({
|
||||
loadProducer: vi.fn(async () => ({
|
||||
resolveConfig: vi.fn((overrides: Record<string, unknown>) => {
|
||||
@@ -120,6 +129,10 @@ vi.mock("../telemetry/events.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("../browser/ffmpeg.js", () => ({
|
||||
detectH264EncoderMode: vi.fn(() => {
|
||||
if (ffmpegEncoderState.error) throw ffmpegEncoderState.error;
|
||||
return ffmpegEncoderState.mode;
|
||||
}),
|
||||
findFFmpeg: vi.fn(() => "/usr/bin/ffmpeg"),
|
||||
getFFmpegInstallHint: vi.fn(() => "brew install ffmpeg"),
|
||||
}));
|
||||
@@ -128,6 +141,13 @@ vi.mock("../browser/preflight.js", () => ({
|
||||
runEnvironmentChecks: vi.fn(async () => preflightState.result),
|
||||
}));
|
||||
|
||||
vi.mock("../utils/orphanCleanup.js", () => ({
|
||||
killOrphanedProcesses: vi.fn(() => {
|
||||
orphanCleanupState.calls += 1;
|
||||
return orphanCleanupState.killed;
|
||||
}),
|
||||
}));
|
||||
|
||||
describe("renderLocal browser GPU config", () => {
|
||||
const savedEnv = new Map<string, string | undefined>();
|
||||
// Pre-resolve once. The first dynamic `import("./render.js")` in this file
|
||||
@@ -173,6 +193,10 @@ describe("renderLocal browser GPU config", () => {
|
||||
configState.writeConfigCalls = [];
|
||||
trackingState.shouldTrack = true;
|
||||
trackingState.renderObservations = [];
|
||||
ffmpegEncoderState.mode = "software";
|
||||
ffmpegEncoderState.error = null;
|
||||
orphanCleanupState.calls = 0;
|
||||
orphanCleanupState.killed = 0;
|
||||
resetTrialState();
|
||||
savedEnv.clear();
|
||||
savedEnv.set("HYPERFRAMES_FFMPEG_PATH", process.env.HYPERFRAMES_FFMPEG_PATH);
|
||||
@@ -185,6 +209,22 @@ describe("renderLocal browser GPU config", () => {
|
||||
delete process.env.HF_DE_PARALLEL_ROUTER;
|
||||
});
|
||||
|
||||
it("cleans orphaned browser trees before starting a local render", async () => {
|
||||
orphanCleanupState.killed = 1;
|
||||
|
||||
await renderLocal("/tmp/project", "/tmp/out.mp4", {
|
||||
fps: { num: 30, den: 1 },
|
||||
quality: "standard",
|
||||
format: "mp4",
|
||||
gpu: false,
|
||||
browserGpuMode: "software",
|
||||
hdrMode: "auto",
|
||||
quiet: true,
|
||||
});
|
||||
|
||||
expect(orphanCleanupState.calls).toBe(1);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
for (const [key, value] of savedEnv) {
|
||||
if (value === undefined) {
|
||||
@@ -323,6 +363,58 @@ describe("renderLocal browser GPU config", () => {
|
||||
expect(process.env.PRODUCER_HEADLESS_SHELL_PATH).toBe("/mock/chrome");
|
||||
});
|
||||
|
||||
it("falls back to hardware encoding when FFmpeg omits libx264", async () => {
|
||||
ffmpegEncoderState.mode = "gpu";
|
||||
|
||||
await renderLocal("/tmp/project", "/tmp/out.mp4", {
|
||||
fps: { num: 30, den: 1 },
|
||||
quality: "high",
|
||||
format: "mp4",
|
||||
gpu: false,
|
||||
browserGpuMode: "software",
|
||||
hdrMode: "force-sdr",
|
||||
quiet: true,
|
||||
});
|
||||
|
||||
expect(producerState.createdJobs[0]?.useGpu).toBe(true);
|
||||
});
|
||||
|
||||
it("lets the encoder surface its own error when capability detection fails", async () => {
|
||||
ffmpegEncoderState.error = new Error("encoder probe timed out");
|
||||
const warn = vi.spyOn(console, "warn").mockImplementation(() => undefined);
|
||||
|
||||
await renderLocal("/tmp/project", "/tmp/out.mp4", {
|
||||
fps: { num: 30, den: 1 },
|
||||
quality: "high",
|
||||
format: "mp4",
|
||||
gpu: false,
|
||||
browserGpuMode: "software",
|
||||
hdrMode: "force-sdr",
|
||||
quiet: true,
|
||||
});
|
||||
|
||||
expect(producerState.createdJobs[0]?.useGpu).toBe(false);
|
||||
expect(warn).not.toHaveBeenCalledWith(expect.stringContaining("encoder probe timed out"));
|
||||
});
|
||||
|
||||
it("diagnoses advisory encoder probe failures unless quiet", async () => {
|
||||
ffmpegEncoderState.error = new Error("encoder probe timed out");
|
||||
const warn = vi.spyOn(console, "warn").mockImplementation(() => undefined);
|
||||
|
||||
await renderLocal("/tmp/project", "/tmp/out.mp4", {
|
||||
fps: { num: 30, den: 1 },
|
||||
quality: "high",
|
||||
format: "mp4",
|
||||
gpu: false,
|
||||
browserGpuMode: "software",
|
||||
hdrMode: "force-sdr",
|
||||
quiet: false,
|
||||
});
|
||||
|
||||
expect(producerState.createdJobs[0]?.useGpu).toBe(false);
|
||||
expect(warn).toHaveBeenCalledWith(expect.stringContaining("encoder probe timed out"));
|
||||
});
|
||||
|
||||
it("resolves browser GPU from CLI flags, Docker mode, and env fallback", () => {
|
||||
// Default (no flag, no env): auto — engine probes and chooses.
|
||||
expect(resolveBrowserGpuForCli(false, undefined, undefined)).toBe("auto");
|
||||
|
||||
@@ -77,7 +77,9 @@ import { isDevMode } from "../utils/env.js";
|
||||
import { buildDockerRunArgs, resolveDockerPlatform } from "../utils/dockerRunArgs.js";
|
||||
import { normalizeErrorMessage } from "../utils/errorMessage.js";
|
||||
import { runEnvironmentChecks } from "../browser/preflight.js";
|
||||
import { detectH264EncoderMode } from "../browser/ffmpeg.js";
|
||||
import { chromeLaunchRemediation } from "../browser/linuxDeps.js";
|
||||
import { killOrphanedProcesses } from "../utils/orphanCleanup.js";
|
||||
import type { ProducerLogger, RenderJob } from "@hyperframes/producer";
|
||||
import {
|
||||
MAX_VP9_CPU_USED,
|
||||
@@ -1439,8 +1441,18 @@ export async function renderLocal(
|
||||
outputPath: string,
|
||||
options: RenderOptions,
|
||||
): Promise<SingleRenderResult> {
|
||||
const recoveredOrphanTrees = killOrphanedProcesses();
|
||||
if (recoveredOrphanTrees > 0 && !options.quiet) {
|
||||
console.warn(
|
||||
c.warn(
|
||||
` Recovered ${recoveredOrphanTrees} orphaned browser process ${recoveredOrphanTrees === 1 ? "tree" : "trees"} from an interrupted render.`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const preflight = await runEnvironmentChecks({
|
||||
projectDir,
|
||||
diskPaths: [tmpdir(), dirname(outputPath)],
|
||||
browserPath: options.browserPath,
|
||||
includeBrowser: true,
|
||||
includeDisk: true,
|
||||
@@ -1468,6 +1480,26 @@ export async function renderLocal(
|
||||
process.env.PRODUCER_HEADLESS_SHELL_PATH = preflight.browser.executablePath;
|
||||
}
|
||||
|
||||
if (!options.gpu && options.format === "mp4" && preflight.ffmpegPath) {
|
||||
let encoderMode: ReturnType<typeof detectH264EncoderMode> = "software";
|
||||
try {
|
||||
encoderMode = detectH264EncoderMode(preflight.ffmpegPath, false);
|
||||
} catch (error) {
|
||||
// Capability probing is advisory. Let the real encode surface the
|
||||
// authoritative FFmpeg error instead of failing here with a bare stack.
|
||||
if (!options.quiet) {
|
||||
const detail = error instanceof Error ? error.message : String(error);
|
||||
console.warn(c.warn(` Unable to probe H.264 encoder capabilities: ${detail}`));
|
||||
}
|
||||
}
|
||||
if (encoderMode === "gpu") {
|
||||
console.warn(
|
||||
c.warn(" FFmpeg does not include libx264; falling back to VideoToolbox H.264 encoding."),
|
||||
);
|
||||
options = { ...options, gpu: true };
|
||||
}
|
||||
}
|
||||
|
||||
const producer = await loadProducer();
|
||||
const deParallelRouterTrialArmed = maybeEnableDeParallelRouterTrial(
|
||||
options.quiet,
|
||||
|
||||
Reference in New Issue
Block a user