Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/engine/src/services/streamingEncoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,8 @@ export async function spawnStreamingEncoder(

const ffmpeg: ChildProcess = spawn(getFfmpegBinary(), args, {
stdio: ["pipe", "pipe", "pipe"],
// See runFfmpeg.ts: keeps a console window off the user's desktop on Windows.
windowsHide: true,
});
trackChildProcess(ffmpeg);

Expand Down
2 changes: 2 additions & 0 deletions packages/engine/src/utils/ffprobe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ async function runFfprobe(
// Nothing is ever written to the child's stdin; leaving it as a pipe is
// what lets a stdin-reading invocation block indefinitely.
stdio: ["ignore", "pipe", "pipe"],
// See runFfmpeg.ts: keeps a console window off the user's desktop on Windows.
windowsHide: true,
});
trackChildProcess(proc);
// Decoded through StringDecoder rather than per-chunk toString(): a
Expand Down
3 changes: 3 additions & 0 deletions packages/engine/src/utils/gpuEncoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ export async function selectUsableGpuEncoder(
export async function detectGpuEncoder(): Promise<GpuEncoder> {
const ffmpeg = spawn(getFfmpegBinary(), ["-encoders"], {
stdio: ["pipe", "pipe", "pipe"],
// See runFfmpeg.ts: keeps a console window off the user's desktop on Windows.
windowsHide: true,
});
trackChildProcess(ffmpeg);
let stdout = "";
Expand Down Expand Up @@ -146,6 +148,7 @@ export function getProbeArgs(encoder: ConcreteGpuEncoder): string[] {
async function canUseGpuEncoder(encoder: ConcreteGpuEncoder): Promise<boolean> {
const ffmpeg = spawn(getFfmpegBinary(), getProbeArgs(encoder), {
stdio: ["ignore", "ignore", "pipe"],
windowsHide: true,
});
trackChildProcess(ffmpeg);
const outcome = await new ManagedChildProcess(ffmpeg, {
Expand Down
6 changes: 5 additions & 1 deletion packages/engine/src/utils/runFfmpeg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,11 @@ export function formatFfmpegError(

export async function runFfmpeg(args: string[], opts?: RunFfmpegOptions): Promise<RunFfmpegResult> {
const timeout = opts?.timeout ?? DEFAULT_TIMEOUT;
const ffmpeg = spawn(getFfmpegBinary(), args);
// windowsHide: ffmpeg/ffprobe are console-subsystem binaries, so without
// this Node opens a visible console window per spawn on Windows. A render
// shells out dozens of times across parallel workers, which flashes a burst
// of windows across the user's desktop. No-op on macOS and Linux.
const ffmpeg = spawn(getFfmpegBinary(), args, { windowsHide: true });
trackChildProcess(ffmpeg);
const managed = new ManagedChildProcess(ffmpeg, {
signal: opts?.signal,
Expand Down
40 changes: 40 additions & 0 deletions packages/engine/src/utils/runFfmpeg.windowsHide.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { EventEmitter } from "node:events";
import { describe, expect, it, vi } from "vitest";

// Hoisted so the mock factory below can reach it without a top-level variable.
const { spawnMock } = vi.hoisted(() => ({ spawnMock: vi.fn() }));

vi.mock("node:child_process", () => ({ spawn: spawnMock }));
vi.mock("child_process", () => ({ spawn: spawnMock }));

/** Minimal stand-in for the ChildProcess runFfmpeg awaits. */
function fakeFfmpeg() {
const proc = new EventEmitter() as EventEmitter & Record<string, unknown>;
proc.stdout = new EventEmitter();
proc.stderr = new EventEmitter();
proc.stdin = { write: vi.fn(), end: vi.fn() };
proc.kill = vi.fn();
proc.pid = 4242;
queueMicrotask(() => proc.emit("close", 0, null));
return proc;
}

describe("runFfmpeg spawn options", () => {
it("hides the console window so Windows renders do not flash terminals", async () => {
// Regression for the Windows popup report: ffmpeg is a console-subsystem
// binary, and Node defaults `windowsHide` to false, so every spawn opened a
// visible window. A render shells out dozens of times across parallel
// workers, which produced a burst of windows on the user's desktop.
// Asserted on the options actually handed to spawn rather than on the
// source text, so a future call site that drops the flag is caught by
// behaviour.
spawnMock.mockImplementation(() => fakeFfmpeg());

const { runFfmpeg } = await import("./runFfmpeg.js");
await runFfmpeg(["-version"]);

expect(spawnMock).toHaveBeenCalledTimes(1);
const options = spawnMock.mock.calls[0]?.[2] as { windowsHide?: boolean } | undefined;
expect(options?.windowsHide).toBe(true);
});
});
3 changes: 2 additions & 1 deletion packages/producer/src/services/audioExtractor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,8 @@ export function parseAudioElements(html: string): AudioElement[] {
*/
function runFFmpeg(args: string[]): Promise<void> {
return new Promise((resolve, reject) => {
const ffmpeg = spawn(getFfmpegBinary(), args);
// See runFfmpeg.ts: keeps a console window off the user's desktop on Windows.
const ffmpeg = spawn(getFfmpegBinary(), args, { windowsHide: true });
trackChildProcess(ffmpeg);
let stderr = "";

Expand Down
6 changes: 5 additions & 1 deletion packages/producer/src/services/distributed/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,11 @@ let cachedFfmpegVersion: string | null = null;
*/
export async function readFfmpegVersion(): Promise<string> {
if (cachedFfmpegVersion !== null) return cachedFfmpegVersion;
const { stdout } = await execFile("ffmpeg", ["-version"], { maxBuffer: 1024 * 1024 });
const { stdout } = await execFile("ffmpeg", ["-version"], {
maxBuffer: 1024 * 1024,
// See runFfmpeg.ts: keeps a console window off the user's desktop on Windows.
windowsHide: true,
});
const firstLine = stdout.split(/\r?\n/)[0]?.trim() ?? "";
if (!firstLine) {
throw new Error("ffmpeg -version returned empty output");
Expand Down
Loading