[Windows] #3379's windowsHide fix landed in engine only — studio-server, lint and cli still spawn console binaries unhidden (fires during preview/Studio, not just render)
Describe the bug
Follow-up to #3379 (ffmpeg console windows, fixed) — the fix landed in packages/engine only. Console-subsystem binaries are still spawned without windowsHide: true from studio-server, lint and several cli paths, so on Windows console windows still pop up. Distinct from #3476 (detached: true spawns of Node itself) and #3430 (chrome-headless-shell).
The practical difference from #3379: those were render-time. These fire during ordinary Studio use. previewLifecycle starts the preview server with detached: true, so that server has no console of its own. Every console-subsystem child it spawns therefore gets a brand-new visible console window from Windows for the duration of the call — an ffprobe flash of ~100–400 ms, a waveform or proxy encode for seconds. Scrubbing a timeline or opening the colour-grading panel is enough to trigger it.
packages/engine/src/utils/runFfmpeg.ts already carries the explanatory comment after #3379:
windowsHide: ffmpeg/ffprobe are console-subsystem binaries, so without this Node opens a visible console window per spawn on Windows.
That reasoning applies verbatim to the sites below.
Root cause
Verified by reading the shipped bundle of 0.8.15 (node_modules/hyperframes/dist/cli.js) — none of these pass windowsHide:
| dist/cli.js | spawns | options actually passed |
|---|---|---|
:68030 |
execFileP(getFfmpegBinary(), ["-hide_banner","-filters"]) |
{ maxBuffer, timeout } |
:102626 |
spawn(ffmpegPath, ["-hide_banner","-filters"]) |
{ stdio } |
:100507 |
execFileSync("taskkill", …) — inside a process.platform === "win32" branch |
{ stdio, timeout } |
:100542 |
execFileSync("powershell.exe", ["-NoProfile","-NonInteractive","-Command", …]) — win32-only branch |
{ encoding, timeout } |
:100571 |
execFileSync("powershell.exe", …) — win32-only branch |
{ encoding, timeout } |
The two powershell.exe calls and the taskkill call sit behind explicit process.platform === "win32" guards, i.e. they exist only to run on Windows, and still omit the one option that matters there. getProcessIdentity/getParentPid run one PowerShell per ancestry hop, so a single hyperframes preview --status / --list / --stop spawns several.
Corresponding sources (paths from a main checkout):
packages/studio-server/src/helpers/mediaMetadata.ts—execFile(ffprobe, …); called on preview-HTML build and byGET /api/projects/:id/media/metadata, which is uncached (one ffprobe per request) and is hit by the colour-grading panelpackages/studio-server/src/helpers/waveform.ts—spawn(ffmpeg, …)for audio peakspackages/studio-server/src/helpers/proxyTranscoder.ts— the-filtersHDR probe and the proxy transcodepackages/studio-server/src/helpers/mediaValidation.ts—spawnSync("ffprobe", …)on upload, no options at allpackages/lint/src/hevcPreviewLint.ts— one ffprobe per video asset, per lint runpackages/cli/src/utils/orphanCleanup.ts—taskkill,powershell.exe×2packages/cli/src/browser/manager.ts—execSync("where chrome")→cmd.exepackages/cli/src/capture/captureCompositionFrame.ts—spawn(ffmpegPath, args)with no options objectpackages/cli/src/background-removal/pipeline.ts,packages/cli/src/browser/ffmpeg.ts,packages/cli/src/utils/webmAlphaCheck.ts,packages/cli/src/browser/preflight.ts
Steps to reproduce
- On Windows, in any project with a video asset:
npx hyperframes preview --background - Open the Studio, scrub the timeline, open the colour-grading panel
- Watch the desktop — console windows flash open and closed
- Also visible with just
npx hyperframes preview --status(PowerShell spawns fromorphanCleanup)
Expected behavior
No visible console windows during preview/Studio use, matching the post-#3379 behaviour of render.
Actual behavior
Console windows flash open and closed repeatedly while the Studio is open, and on every preview --status/--list/--stop.
Suggested fix
Add windowsHide: true at each site (a no-op on macOS/Linux, so it can be applied unconditionally). For the calls that pass no options object at all, add one.
It may be worth adding a lint rule or a shared spawnHidden() helper so this class of regression stops recurring — this is now the third issue in the same family (#3379, #3476, this one).
Two smaller things noticed while tracing, mentioned only because they are in the same code paths and not filed separately:
routes/preview.tscallsprobeAssetCodec(file)per?hf-proxy=asset request, deliberately before the 304 shortcut, androutes/media.tsGET …/media/metadatahas no cache — both are uncapped ffprobe-per-request paths.packages/cli/src/utils/autoUpdate.tspassesdetached: trueandwindowsHide: true, which is the combination Windows resolves in favour ofDETACHED_PROCESS(documented:CREATE_NO_WINDOWis ignored whenDETACHED_PROCESSis set). Same conflict as #3430's root cause, so thewindowsHidethere is not doing what it looks like it does.
Environment
Version 0.8.15 (latest)
Node.js v22.23.2 and v24.19.0 (win32 x64) — reproduced on both
OS Windows 11 Home 10.0.26200
CPU AMD Ryzen 7 5700U
FFmpeg 9.0 (Gyan build, winget)Source: heygen-com/hyperframes