fix(cli): a long video conversion in init never stalls on ffmpeg's own log (#5616)

init's transcodeToMp4 piped ffmpeg's stdout and stderr and never read them.
ffmpeg's progress log filled the pipe buffer and ffmpeg blocked in write(),
so init hung with its spinner running. Both streams are now ignored.

render --docker --quiet piped the container's stdout without reading it; it
is now ignored. stderr is still read and kept as a bounded tail.
This commit is contained in:
Miguel Ángel
2026-10-14 19:19:31 +00:00
committed by GitHub
parent cd87cf1110
commit b14a456471
3 changed files with 32 additions and 3 deletions
+30 -1
View File
@@ -1,6 +1,7 @@
import { describe, expect, it } from "vitest";
import { describe, expect, it, vi } from "vitest";
import { spawnSync } from "node:child_process";
import {
chmodSync,
copyFileSync,
existsSync,
mkdirSync,
@@ -17,6 +18,7 @@ import {
injectTailwindBrowserScript,
nameTooLongMessage,
resolveVideoDurationSeconds,
transcodeToMp4,
} from "./init.js";
const cliEntry = resolve(fileURLToPath(import.meta.url), "..", "..", "cli.ts");
@@ -698,3 +700,30 @@ describe("applyResolutionPreset", () => {
}
});
});
describe("transcodeToMp4", () => {
it.skipIf(process.platform === "win32")(
"finishes when ffmpeg logs far more than a pipe buffer holds",
async () => {
const dir = mkdtempSync(join(tmpdir(), "hf-init-ffmpeg-log-"));
const ffmpeg = join(dir, "ffmpeg");
const output = join(dir, "out.mp4");
// Blocking writes, like ffmpeg's own log: 1 MB stalls on a pipe nobody reads.
const script = `const fs = require("node:fs");
const line = Buffer.alloc(1024, "x");
for (let i = 0; i < 1024; i++) fs.writeSync(2, line);
fs.writeFileSync(process.argv.at(-1), "");`;
writeFileSync(ffmpeg, `#!${process.execPath}\n${script}\n`);
chmodSync(ffmpeg, 0o755);
vi.stubEnv("HYPERFRAMES_FFMPEG_PATH", ffmpeg);
try {
await expect(transcodeToMp4(join(dir, "in.mov"), output)).resolves.toBe(true);
expect(existsSync(output)).toBe(true);
} finally {
vi.unstubAllEnvs();
rmSync(dir, { recursive: true, force: true });
}
},
5_000,
);
});
+1 -1
View File
@@ -211,7 +211,7 @@ export function transcodeToMp4(inputPath: string, outputPath: string): Promise<b
"-y",
outputPath,
],
{ stdio: "pipe", windowsHide: true },
{ stdio: "ignore", windowsHide: true },
);
child.on("close", (code) => resolvePromise(code === 0));
+1 -1
View File
@@ -847,7 +847,7 @@ async function renderDocker(
const stderrTail = createStderrTail();
// stderr is piped so the failure can name its cause; it is still echoed live.
const child = spawn("docker", dockerArgs, {
stdio: options.quiet ? ["pipe", "pipe", "pipe"] : ["inherit", "inherit", "pipe"],
stdio: options.quiet ? ["pipe", "ignore", "pipe"] : ["inherit", "inherit", "pipe"],
});
child.stderr?.on("data", (chunk: Buffer) => {
process.stderr.write(chunk);