Skip to content

[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) #3500

Description

@BenT034

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.tsexecFile(ffprobe, …); called on preview-HTML build and by GET /api/projects/:id/media/metadata, which is uncached (one ffprobe per request) and is hit by the colour-grading panel
  • packages/studio-server/src/helpers/waveform.tsspawn(ffmpeg, …) for audio peaks
  • packages/studio-server/src/helpers/proxyTranscoder.ts — the -filters HDR probe and the proxy transcode
  • packages/studio-server/src/helpers/mediaValidation.tsspawnSync("ffprobe", …) on upload, no options at all
  • packages/lint/src/hevcPreviewLint.ts — one ffprobe per video asset, per lint run
  • packages/cli/src/utils/orphanCleanup.tstaskkill, powershell.exe ×2
  • packages/cli/src/browser/manager.tsexecSync("where chrome")cmd.exe
  • packages/cli/src/capture/captureCompositionFrame.tsspawn(ffmpegPath, args) with no options object
  • packages/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

  1. On Windows, in any project with a video asset: npx hyperframes preview --background
  2. Open the Studio, scrub the timeline, open the colour-grading panel
  3. Watch the desktop — console windows flash open and closed
  4. Also visible with just npx hyperframes preview --status (PowerShell spawns from orphanCleanup)

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.ts calls probeAssetCodec(file) per ?hf-proxy= asset request, deliberately before the 304 shortcut, and routes/media.ts GET …/media/metadata has no cache — both are uncapped ffprobe-per-request paths.
  • packages/cli/src/utils/autoUpdate.ts passes detached: true and windowsHide: true, which is the combination Windows resolves in favour of DETACHED_PROCESS (documented: CREATE_NO_WINDOW is ignored when DETACHED_PROCESS is set). Same conflict as [Windows] chrome-headless-shell.exe opens a visible console window on every render #3430's root cause, so the windowsHide there 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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions