feat(setup): fetch ffmpeg on first run instead of bundling it

Ship a smaller installer that no longer carries ffmpeg.exe/ffprobe.exe
(the bulk of its size). On first run they are downloaded from a pinned
upstream archive (gyan 8.1.2), verified against a pinned SHA-256, and
unpacked with the OS tar.exe into the managed userData/bin dir -- the
same place as the self-updating yt-dlp, so they survive app updates and
portable re-extraction. A hard onboarding gate blocks the app until they
are present, with a "locate existing ffmpeg" folder-picker fallback for
offline machines and a Repair action in Settings > About.

The updater's streaming/checksum/redirect/idle-timeout download loop is
extracted to lib/verifiedDownload.streamVerifiedFile and shared by both
the app-installer download and the ffmpeg fetch; the updater's public
behaviour and error strings are unchanged (its boundary tests still pass).
No new npm dependency: extraction uses the System32 bsdtar resolved by
absolute path (audit F3).

Note: this commit also carries pre-existing, in-progress aria2c/network
and updater-token work that was already uncommitted in the working tree
and is entangled with the above in shared files (updater.ts, ipc.ts,
preload/index.ts, mockApi.ts, shared/ipc.ts, plus the settings/network
files and aria2c.exe). It was not cleanly separable by path, so it is
included here rather than split out.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-07 11:02:13 -04:00
parent cb25262a2d
commit eb53de2ea5
33 changed files with 1391 additions and 386 deletions
+7 -3
View File
@@ -5,7 +5,7 @@ import { join, parse } from 'path'
import { BrowserWindow, Notification, type WebContents } from 'electron'
import {
getYtdlpPath,
getBinDir,
getFfmpegDir,
getAria2cPath,
getFfmpegPath,
getFfprobePath,
@@ -243,7 +243,9 @@ export function buildCommand(opts: StartDownloadOptions, cookiesFile?: string):
youtubePoToken: settings.youtubePoToken
}
const extraArgs = resolveExtraArgs(opts, settings)
return buildArgs({ opts, outputTemplate, options, binDir: getBinDir(), access, extraArgs })
// binDir feeds only `--ffmpeg-location`, so it's the managed ffmpeg dir (userData/bin),
// where the first-run download / dev seed places ffmpeg.exe + ffprobe.exe.
return buildArgs({ opts, outputTemplate, options, binDir: getFfmpegDir(), access, extraArgs })
}
/** Build the exact command line for the current form state, without running it. */
@@ -285,9 +287,11 @@ export function startDownload(wc: WebContents, opts: StartDownloadOptions): Star
if (!existsSync(getFfmpegPath())) missingBins.push('ffmpeg.exe')
if (!existsSync(getFfprobePath())) missingBins.push('ffprobe.exe')
if (missingBins.length > 0) {
// The first-run setup gate normally guarantees these are present; this backstops a
// mid-session loss (AV quarantine, a cleared userData/bin) with a clear repair path.
return {
ok: false,
error: `${missingBins.join(' and ')} not found in ${getBinDir()}. Add the ffmpeg build's binaries to resources/bin/ (see the README there).`
error: `${missingBins.join(' and ')} not found. Open Settings → About and choose "Repair ffmpeg" to reinstall the media tools.`
}
}
// Reject anything that isn't an http(s) URL before it reaches yt-dlp's argv,