From b14a45647166e60e2e51dc018ccb1bb7331003a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Wed, 14 Oct 2026 19:19:31 +0000 Subject: [PATCH] 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. --- packages/cli/src/commands/init.test.ts | 31 +++++++++++++++++++++++++- packages/cli/src/commands/init.ts | 2 +- packages/cli/src/commands/render.ts | 2 +- 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/commands/init.test.ts b/packages/cli/src/commands/init.test.ts index ded2b14ca5..259d2837be 100644 --- a/packages/cli/src/commands/init.test.ts +++ b/packages/cli/src/commands/init.test.ts @@ -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, + ); +}); diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index 80f082d4cf..b3bf347f1b 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -211,7 +211,7 @@ export function transcodeToMp4(inputPath: string, outputPath: string): Promise resolvePromise(code === 0)); diff --git a/packages/cli/src/commands/render.ts b/packages/cli/src/commands/render.ts index 36d508644d..fba7f97037 100644 --- a/packages/cli/src/commands/render.ts +++ b/packages/cli/src/commands/render.ts @@ -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);