mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-07 18:26:17 +00:00
fix(engine): hide ffmpeg console windows on Windows (#3381)
ffmpeg and ffprobe are console-subsystem binaries and Node defaults windowsHide to false, so every spawn opened a visible console window on Windows. A render shells out dozens of times across parallel workers, which flashed a burst of windows across the user's desktop. Applied at every production spawn site rather than only the two named in the report, since they all share the cause: runFfmpeg, both gpuEncoder probes, ffprobe, streamingEncoder, audioExtractor and the distributed version check. windowsHide is a no-op on macOS and Linux. The dev-only parity and regression harnesses are left alone; they never run on a user's desktop. Closes #3379
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 = "";
|
||||
@@ -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, {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user