mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 12:54:29 +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:
@@ -1,12 +1,13 @@
|
||||
import { execSync } from "node:child_process";
|
||||
import { execFileSync, execSync } from "node:child_process";
|
||||
import { existsSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("node:child_process", () => ({ execSync: vi.fn() }));
|
||||
vi.mock("node:child_process", () => ({ execFileSync: vi.fn(), execSync: vi.fn() }));
|
||||
vi.mock("node:fs", () => ({ existsSync: vi.fn() }));
|
||||
|
||||
const mockExec = vi.mocked(execSync);
|
||||
const mockExecFile = vi.mocked(execFileSync);
|
||||
const mockExists = vi.mocked(existsSync);
|
||||
|
||||
// The common-dir fallback list is platform-gated (empty on win32), so pin the
|
||||
@@ -53,3 +54,39 @@ describe("findFFmpeg", () => {
|
||||
expect(findFFmpeg()).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveH264EncoderMode", () => {
|
||||
it("falls back to VideoToolbox when libx264 is absent", async () => {
|
||||
const { resolveH264EncoderMode } = await import("./ffmpeg.js");
|
||||
const encoders = `
|
||||
V....D h264_videotoolbox VideoToolbox H.264 Encoder
|
||||
`;
|
||||
|
||||
expect(resolveH264EncoderMode(encoders, false)).toBe("gpu");
|
||||
});
|
||||
|
||||
it("does not treat a compiled Linux hardware encoder as usable", async () => {
|
||||
const { resolveH264EncoderMode } = await import("./ffmpeg.js");
|
||||
const encoders = `
|
||||
V....D h264_vaapi H.264/AVC (VAAPI)
|
||||
`;
|
||||
|
||||
expect(() => resolveH264EncoderMode(encoders, false)).toThrow(
|
||||
"neither libx264 nor VideoToolbox",
|
||||
);
|
||||
});
|
||||
|
||||
it("inspects the configured FFmpeg binary", async () => {
|
||||
mockExecFile.mockReturnValue(
|
||||
" V....D h264_videotoolbox VideoToolbox H.264 Encoder\n" as never,
|
||||
);
|
||||
const { detectH264EncoderMode } = await import("./ffmpeg.js");
|
||||
|
||||
expect(detectH264EncoderMode("/custom/ffmpeg", false)).toBe("gpu");
|
||||
expect(mockExecFile).toHaveBeenCalledWith(
|
||||
"/custom/ffmpeg",
|
||||
["-hide_banner", "-encoders"],
|
||||
expect.objectContaining({ encoding: "utf-8" }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// fallow-ignore-file code-duplication
|
||||
import { execSync } from "node:child_process";
|
||||
import { execFileSync, execSync } from "node:child_process";
|
||||
import { existsSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { detectLinuxDistro, ffmpegInstallCommand } from "./linuxDeps.js";
|
||||
@@ -7,6 +7,34 @@ import { detectLinuxDistro, ffmpegInstallCommand } from "./linuxDeps.js";
|
||||
export const FFMPEG_PATH_ENV = "HYPERFRAMES_FFMPEG_PATH";
|
||||
export const FFPROBE_PATH_ENV = "HYPERFRAMES_FFPROBE_PATH";
|
||||
|
||||
export type H264EncoderMode = "software" | "gpu";
|
||||
|
||||
/**
|
||||
* Select the H.264 encoder class supported by an FFmpeg build.
|
||||
*
|
||||
* Some macOS FFmpeg distributions expose VideoToolbox but omit libx264. The
|
||||
* default CPU render path must not pass libx264-only options such as `-preset`
|
||||
* to those builds.
|
||||
*/
|
||||
export function resolveH264EncoderMode(
|
||||
ffmpegEncodersOutput: string,
|
||||
gpuRequested: boolean,
|
||||
): H264EncoderMode {
|
||||
if (gpuRequested) return "gpu";
|
||||
if (/\blibx264\b/.test(ffmpegEncodersOutput)) return "software";
|
||||
if (/\bh264_videotoolbox\b/.test(ffmpegEncodersOutput)) return "gpu";
|
||||
throw new Error("This FFmpeg build has neither libx264 nor VideoToolbox H.264 encoding.");
|
||||
}
|
||||
|
||||
export function detectH264EncoderMode(ffmpegPath: string, gpuRequested: boolean): H264EncoderMode {
|
||||
const encoders = execFileSync(ffmpegPath, ["-hide_banner", "-encoders"], {
|
||||
encoding: "utf-8",
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
timeout: 5000,
|
||||
});
|
||||
return resolveH264EncoderMode(encoders, gpuRequested);
|
||||
}
|
||||
|
||||
function chooseBestPathCandidate(
|
||||
name: "ffmpeg" | "ffprobe",
|
||||
candidates: string[],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// fallow-ignore-file code-duplication
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { parseToolVersion, runEnvironmentChecks } from "./preflight.js";
|
||||
import { checkDisk, parseToolVersion, runEnvironmentChecks } from "./preflight.js";
|
||||
import * as manager from "./manager.js";
|
||||
import * as linuxDeps from "./linuxDeps.js";
|
||||
|
||||
@@ -218,3 +218,15 @@ describe("parseToolVersion", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("checkDisk", () => {
|
||||
it("checks the requested render volume", () => {
|
||||
const freeDiskMb = vi.fn(() => 512);
|
||||
|
||||
expect(checkDisk("/external/render-output", freeDiskMb)).toMatchObject({
|
||||
ok: false,
|
||||
detail: "0.5 GB free at /external/render-output",
|
||||
});
|
||||
expect(freeDiskMb).toHaveBeenCalledWith("/external/render-output");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -38,6 +38,7 @@ export interface EnvironmentCheckResult {
|
||||
|
||||
export interface EnvironmentCheckOptions {
|
||||
projectDir?: string;
|
||||
diskPaths?: string[];
|
||||
browserPath?: string;
|
||||
includeBrowser?: boolean;
|
||||
includeDisk?: boolean;
|
||||
@@ -226,8 +227,11 @@ async function checkChrome(browserPath?: string): Promise<EnvironmentCheckOutcom
|
||||
};
|
||||
}
|
||||
|
||||
function checkDisk(projectDir = "."): EnvironmentCheckOutcome {
|
||||
const freeMb = getFreeDiskMb(projectDir);
|
||||
export function checkDisk(
|
||||
path = ".",
|
||||
freeDiskMb: (path: string) => number | null = getFreeDiskMb,
|
||||
): EnvironmentCheckOutcome {
|
||||
const freeMb = freeDiskMb(path);
|
||||
if (freeMb === null) {
|
||||
return { name: "Disk", ok: true, level: "ok", detail: "Unable to check" };
|
||||
}
|
||||
@@ -238,11 +242,11 @@ function checkDisk(projectDir = "."): EnvironmentCheckOutcome {
|
||||
ok: false,
|
||||
level: "error",
|
||||
title: "Low disk space",
|
||||
detail: `${freeGb} GB free`,
|
||||
detail: `${freeGb} GB free at ${path}`,
|
||||
hint: "Renders produce large temp files. Free disk space before rendering.",
|
||||
};
|
||||
}
|
||||
return { name: "Disk", ok: true, level: "ok", detail: `${freeGb} GB free` };
|
||||
return { name: "Disk", ok: true, level: "ok", detail: `${freeGb} GB free at ${path}` };
|
||||
}
|
||||
|
||||
function checkWindowsUncPath(projectDir = process.cwd()): EnvironmentCheckOutcome | undefined {
|
||||
@@ -257,6 +261,7 @@ function checkWindowsUncPath(projectDir = process.cwd()): EnvironmentCheckOutcom
|
||||
};
|
||||
}
|
||||
|
||||
// fallow-ignore-next-line complexity
|
||||
export async function runEnvironmentChecks(
|
||||
options: EnvironmentCheckOptions = {},
|
||||
): Promise<EnvironmentCheckResult> {
|
||||
@@ -281,7 +286,8 @@ export async function runEnvironmentChecks(
|
||||
}
|
||||
|
||||
if (options.includeDisk) {
|
||||
outcomes.push(checkDisk(options.projectDir));
|
||||
const diskPaths = [...new Set(options.diskPaths ?? [options.projectDir ?? "."])];
|
||||
outcomes.push(...diskPaths.map((path) => checkDisk(path)));
|
||||
}
|
||||
|
||||
if (options.includeWindowsUnc) {
|
||||
|
||||
Reference in New Issue
Block a user