mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
fix(engine): support current FFmpeg filter scripts (#2324)
This commit is contained in:
@@ -4,7 +4,7 @@ import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
|
||||
// The mix filter graph is written to a temp file and passed via
|
||||
// -filter_complex_script (not inlined via -filter_complex) so the command
|
||||
// a file-valued filter option (not inlined via -filter_complex) so the command
|
||||
// line doesn't scale with track count — production code deletes that file
|
||||
// the moment the (real) ffmpeg process exits. The mock captures each call's
|
||||
// filter content synchronously, while the file still exists, into an
|
||||
@@ -15,7 +15,9 @@ const { runFfmpegMock, capturedFilterScripts } = vi.hoisted(() => {
|
||||
return {
|
||||
capturedFilterScripts,
|
||||
runFfmpegMock: vi.fn(async (args: string[]) => {
|
||||
const idx = args.indexOf("-filter_complex_script");
|
||||
const legacyIdx = args.indexOf("-filter_complex_script");
|
||||
const currentIdx = args.indexOf("-/filter_complex");
|
||||
const idx = legacyIdx >= 0 ? legacyIdx : currentIdx;
|
||||
if (idx >= 0) {
|
||||
const { readFileSync } = await import("node:fs");
|
||||
capturedFilterScripts.push(readFileSync(args[idx + 1], "utf8"));
|
||||
@@ -348,6 +350,57 @@ describe("processCompositionAudio", () => {
|
||||
expect((filter?.match(/atrim=/g) ?? []).length).toBe(trackCount);
|
||||
});
|
||||
|
||||
it("retries with the current file-valued filter option when a nightly removes the legacy alias", async () => {
|
||||
const baseDir = mkdtempSync(join(tmpdir(), "hf-audio-base-"));
|
||||
const workDir = mkdtempSync(join(tmpdir(), "hf-audio-work-"));
|
||||
tempDirs.push(baseDir, workDir);
|
||||
|
||||
writeFileSync(join(baseDir, "voice.wav"), "stub");
|
||||
|
||||
runFfmpegMock
|
||||
.mockImplementationOnce(async () => {
|
||||
capturedFilterScripts.push("");
|
||||
return { success: true, durationMs: 1, stderr: "", exitCode: 0 };
|
||||
})
|
||||
.mockImplementationOnce(async () => {
|
||||
capturedFilterScripts.push("");
|
||||
return {
|
||||
success: false,
|
||||
durationMs: 1,
|
||||
stderr: "Unrecognized option 'filter_complex_script'.\nError splitting the argument list",
|
||||
exitCode: 8,
|
||||
};
|
||||
});
|
||||
|
||||
const result = await processCompositionAudio(
|
||||
[
|
||||
{
|
||||
id: "voice",
|
||||
src: "voice.wav",
|
||||
start: 0,
|
||||
end: 2,
|
||||
mediaStart: 0,
|
||||
layer: 0,
|
||||
volume: 1,
|
||||
type: "audio",
|
||||
},
|
||||
],
|
||||
baseDir,
|
||||
workDir,
|
||||
join(baseDir, "out.m4a"),
|
||||
2,
|
||||
);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(runFfmpegMock).toHaveBeenCalledTimes(3);
|
||||
const legacyArgs = runFfmpegMock.mock.calls[1]?.[0] as string[];
|
||||
const currentArgs = runFfmpegMock.mock.calls[2]?.[0] as string[];
|
||||
expect(legacyArgs).toContain("-filter_complex_script");
|
||||
expect(currentArgs).toContain("-/filter_complex");
|
||||
expect(currentArgs).not.toContain("-filter_complex_script");
|
||||
expect(capturedFilterScripts[2]).toContain("amix=inputs=1");
|
||||
});
|
||||
|
||||
it("prepares percent-encoded non-Latin audio srcs from decoded filesystem paths", async () => {
|
||||
const baseDir = mkdtempSync(join(tmpdir(), "hf-audio-base-"));
|
||||
const workDir = mkdtempSync(join(tmpdir(), "hf-audio-work-"));
|
||||
|
||||
@@ -32,6 +32,13 @@ function escapeExpressionCommas(expression: string): string {
|
||||
return expression.replace(/\\/g, "\\\\").replace(/,/g, "\\,");
|
||||
}
|
||||
|
||||
function legacyFilterScriptOptionIsUnsupported(stderr: string): boolean {
|
||||
return (
|
||||
/filter_complex_script/i.test(stderr) &&
|
||||
/(?:unrecognized option|option (?:was )?not found)/i.test(stderr)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Upper bound on volume-automation keyframes folded into the FFmpeg `volume`
|
||||
* expression. The expression nests one `if(lt(...))` per keyframe, and
|
||||
@@ -406,10 +413,14 @@ async function mixAudioTracks(
|
||||
// argument scale linearly with track count until it exceeds the OS
|
||||
// command-line length limit — spawn ENAMETOOLONG, seen in practice at 146
|
||||
// tracks — even though every individual filter segment is short. FFmpeg's
|
||||
// own `-filter_complex_script <file>` reads the same graph from disk
|
||||
// instead, sidestepping the argv limit for the one component of this
|
||||
// command line that actually grows with the composition.
|
||||
const runMix = (ignoreAutomation: boolean) => {
|
||||
// file-valued filter options read the same graph from disk instead,
|
||||
// sidestepping the argv limit for the one component of this command line
|
||||
// that actually grows with the composition. FFmpeg deprecated
|
||||
// `-filter_complex_script` in favour of `-/filter_complex`, then removed the
|
||||
// alias from nightly builds; older stable builds do not understand the new
|
||||
// spelling. Prefer the legacy spelling for broad compatibility and retry
|
||||
// only when FFmpeg explicitly says that option is unavailable.
|
||||
const runMix = async (ignoreAutomation: boolean) => {
|
||||
const inputs: string[] = [];
|
||||
tracks.forEach((track) => inputs.push("-i", track.srcPath));
|
||||
const scriptDir = mkdtempSync(join(outputDir, ".filter-complex-"));
|
||||
@@ -435,9 +446,17 @@ async function mixAudioTracks(
|
||||
"-y",
|
||||
outputPath,
|
||||
];
|
||||
return runFfmpeg(args, { signal, timeout: ffmpegProcessTimeout }).finally(() =>
|
||||
rmSync(scriptDir, { recursive: true, force: true }),
|
||||
);
|
||||
try {
|
||||
const legacyResult = await runFfmpeg(args, { signal, timeout: ffmpegProcessTimeout });
|
||||
if (legacyResult.success || !legacyFilterScriptOptionIsUnsupported(legacyResult.stderr)) {
|
||||
return legacyResult;
|
||||
}
|
||||
const currentArgs = [...args];
|
||||
currentArgs[currentArgs.indexOf("-filter_complex_script")] = "-/filter_complex";
|
||||
return await runFfmpeg(currentArgs, { signal, timeout: ffmpegProcessTimeout });
|
||||
} finally {
|
||||
rmSync(scriptDir, { recursive: true, force: true });
|
||||
}
|
||||
};
|
||||
|
||||
let result = await runMix(false);
|
||||
|
||||
Reference in New Issue
Block a user