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
+42 -11
View File
@@ -18,6 +18,18 @@ export const IpcChannels = {
appUpdateProgress: 'app:update-progress',
ytdlpVersion: 'ytdlp:version',
ffmpegVersion: 'ffmpeg:version',
/** readiness (both binaries present) + versions of the managed ffmpeg/ffprobe */
ffmpegSetupStatus: 'ffmpeg:setup-status',
/** download + unpack ffmpeg/ffprobe from the pinned upstream archive (first-run setup) */
ffmpegSetupDownload: 'ffmpeg:setup-download',
/** abort the in-flight ffmpeg archive download */
ffmpegSetupCancel: 'ffmpeg:setup-cancel',
/** pick a folder that already holds ffmpeg.exe + ffprobe.exe and install from it */
ffmpegSetupLocate: 'ffmpeg:setup-locate',
/** main → renderer push channel for ffmpeg setup progress (download %, then extract) */
ffmpegSetupProgress: 'ffmpeg:setup-progress',
/** whether the optional aria2c.exe external downloader is present in the bin dir */
aria2cAvailable: 'aria2c:available',
probe: 'media:probe',
downloadStart: 'download:start',
downloadCancel: 'download:cancel',
@@ -275,9 +287,9 @@ export interface YtdlpVersionResult {
}
/**
* Versions of the bundled ffmpeg + ffprobe, for display in Settings. Each is the
* Versions of the managed ffmpeg + ffprobe, for display in Settings. Each is the
* parsed version token, or null when that binary is missing or unreadable. These
* ship with AeroFetch and aren't auto-updated (ffmpeg has no self-update and,
* are fetched on first run (not bundled) and aren't auto-updated (ffmpeg has no self-update and,
* unlike yt-dlp, doesn't break as sites change), so there's no ok/error here —
* just "what's installed".
*/
@@ -286,6 +298,33 @@ export interface FfmpegVersionResult {
ffprobe: string | null
}
/** Readiness of the managed ffmpeg/ffprobe: `ready` gates the first-run setup screen. */
export interface FfmpegSetupStatus {
/** true once both ffmpeg.exe and ffprobe.exe are present in the managed dir */
ready: boolean
/** installed versions (null when the binary is absent), for the About card */
ffmpeg: string | null
ffprobe: string | null
}
/** Live progress of the first-run ffmpeg acquisition (main → renderer). */
export interface FfmpegSetupProgress {
/** 'downloading' the archive, then a brief indeterminate 'extracting' phase */
phase: 'downloading' | 'extracting'
/** bytes received so far (downloading phase) */
received?: number
/** total archive bytes, when Content-Length is known */
total?: number
/** 0..1 download fraction, when total is known */
fraction?: number
}
/** Result of a download / locate setup action. */
export interface FfmpegSetupResult {
ok: boolean
error?: string
}
/** Result of checking the configured Gitea repo for a newer AeroFetch release. */
export interface AppUpdateInfo {
ok: boolean
@@ -684,13 +723,6 @@ export interface Settings {
* modern GPUs can turn this on for smoother high-DPI/large-window rendering.
*/
useHardwareAcceleration: boolean
/**
* Optional Gitea access token for the in-app updater. Empty = anonymous (works
* only where the release repo allows anonymous access). When the release repo
* is private / the instance requires sign-in, paste a read-only token so the
* updater can check + download. Stored locally in settings, never shipped.
*/
updateToken: string
}
/**
@@ -753,8 +785,7 @@ export const DEFAULT_SETTINGS: Settings = {
minimizeToTray: false,
launchAtStartup: false,
isSidebarCollapsed: false,
useHardwareAcceleration: false,
updateToken: ''
useHardwareAcceleration: false
}
/**