From 301c8998bfdcac80ad3302e2d2ebe853e2ea6de1 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Sat, 5 Sep 2026 08:43:11 +0200 Subject: feat(packaging): bundle ffmpeg in the Windows installer by default winget install ffmpeg was considered and rejected as the mechanism: it needs network access and winget/App Installer present at the exact moment setup runs, and its failure mode is silent -- video just does not stream, with nothing pointing back at ffmpeg. Not viable for a non-technical install. MeshBay transcodes browser-incompatible video to H.264 (-c:v libx264, webrtc_server.py) -- a real encode, not remux -- so this needs a genuine GPL ffmpeg build; no LGPL-only build includes an H.264 encoder, since libx264 itself is GPL. packaging/win/fetch-ffmpeg.ps1 (new) Downloads, checksum-verifies and stages ffmpeg for the build. Source: BtbN/FFmpeg-Builds' Windows x86_64 gpl-shared preset -- shared DLLs rather than two independent static binaries, which is what nearly tripled this: the "full" static build many devs already have via winget is ~220 MB *per executable*. Pinned to one dated release tag (immutable once published) and its own sha256, not the "latest" alias BtbN repoints on every auto-build -- verified by hand first (downloaded, hash matched, ran a real encode+probe with libx264) before pinning. ffplay.exe (an SDL2 player, ~17 MB) is dropped; MeshBay never invokes it. Cached after the first build. Runs its own smoke test (encode + probe a real clip) so a broken fetch fails at build time, not for the first user who tries to watch something. packaging/win/LICENSE-ffmpeg.txt (new) GPLv3 notice + where the corresponding source is, required because this redistributes a GPL binary even though it is unmodified and only ever invoked as a subprocess. Ships alongside ffmpeg.exe in the installer. build-node-runtime.ps1 / build-win.ps1 Bundling is now the DEFAULT, replacing the old opt-in -FfmpegDir (which copied from a local directory and left most builds without ffmpeg at all). -SkipFfmpeg opts out for a smaller, streaming-less local-iteration build. Also fixes a real bug the ffmpeg change exposed rather than caused: the final `--help` smoke test did `$help -notmatch "meshbay-node"` against $help captured as a PowerShell ARRAY (one element per line) -- -notmatch on a collection is a FILTER, not a boolean test, and returns the non-matching elements; any non-empty array is truthy in if() regardless of content. Once --help wrapped past one line (it now does, with autostart/service in the verb list) this threw unconditionally. Fixed by joining to one string before matching, and pinned by a new test so a future edit cannot silently reintroduce the collection-vs-scalar trap. Verified: downloaded and hashed the pinned release by hand (matches), ran a real libx264 encode + ffprobe against the extracted build, fetch-ffmpeg.ps1 end to end (161 MB staged), a full build-node-runtime.ps1 run (308 MB node-runtime/) and a full installer build (MeshBay-Setup- 1.0.0.exe, 210.8 MB with ffmpeg bundled). Node suite 850 pass / 25 skip. Co-Authored-By: Claude Sonnet 5 --- packaging/win/fetch-ffmpeg.ps1 | 131 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 packaging/win/fetch-ffmpeg.ps1 (limited to 'packaging/win/fetch-ffmpeg.ps1') diff --git a/packaging/win/fetch-ffmpeg.ps1 b/packaging/win/fetch-ffmpeg.ps1 new file mode 100644 index 0000000..5f6123d --- /dev/null +++ b/packaging/win/fetch-ffmpeg.ps1 @@ -0,0 +1,131 @@ +<# +.SYNOPSIS + Download, verify and stage the ffmpeg/ffprobe MeshBay bundles by default. + +.DESCRIPTION + MeshBay transcodes browser-incompatible video to H.264 (webrtc_server.py, + `-c:v libx264`) -- a real encode, not just remux/probe -- so only a GPL + ffmpeg build works here; libx264 is GPL, no LGPL-only build includes it. + Asking an end user to separately install ffmpeg (`winget install ffmpeg`) + is not viable for a non-technical install: it needs network access and + winget/App Installer present at that exact moment, and fails silently + (streaming just does not work, with nothing pointing back at ffmpeg). + So this bundles it, verified, by default. + + Source: BtbN/FFmpeg-Builds (github.com/BtbN/FFmpeg-Builds), the "gpl-shared" + Windows x86_64 preset -- ffmpeg.exe/ffprobe.exe plus the shared DLLs they + both link against, rather than two independent static binaries (which is + what very nearly doubled this: the "full" static build many devs already + have via winget is ~220 MB *per executable*). ffplay.exe is dropped: it is + an SDL2 video player, not something the daemon ever invokes. + + Pinned to one dated release tag (autobuild-YYYY-MM-DD-HH-MM) rather than + the "latest" alias, which BtbN repoints to a new build on every run -- + fine for a person fetching ffmpeg today, wrong for a build script that + must produce the same output next month. Checksum verified against the + release's own checksums.sha256, and the hash is pinned here too, so a + compromised or altered mirror is a hard failure, not a silent swap. + + LICENSE-ffmpeg.txt is copied alongside ffmpeg.exe in the output -- ffmpeg + is unmodified, but it is still a GPLv3 binary MeshBay redistributes, and + that binary must carry its own license notice into the package. + +.PARAMETER OutDir + Where to place ffmpeg.exe, ffprobe.exe, the DLLs and the license notice. + Typically packages/meshbay-client/node-runtime/ (default build-node- + runtime.ps1 target), so they end up wherever the daemon already looks -- + that directory is on PATH once installed (W3/W4), so no code needs to name + ffmpeg's path explicitly. + +.PARAMETER SkipCache + Re-download even if a verified copy already sits in the cache dir. Off by + default so repeated builds do not refetch 73 MB every time. +#> +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)] + [string]$OutDir, + + [switch]$SkipCache +) + +$ErrorActionPreference = "Stop" + +# Pinned: one dated release, one asset, one hash. Re-pin deliberately (a new +# tag from https://github.com/BtbN/FFmpeg-Builds/releases, the exact asset +# name for that release -- it embeds ffmpeg's own git-describe, so it changes +# every time -- and the hash from that release's checksums.sha256) when +# ffmpeg needs an update. Never move this to the "latest" alias: it is +# reassigned on every BtbN auto-build and would make this script produce a +# different binary tomorrow with no change to this file. +$FFMPEG_TAG = "autobuild-2026-09-04-14-01" +$FFMPEG_ASSET = "ffmpeg-N-126404-g818e5d965b-win64-gpl-shared.zip" +$FFMPEG_SHA256 = "8575193e6a8d661a650e776f97b1379c14f6513d8fb1d650013e40916b758609" +$FFMPEG_URL = "https://github.com/BtbN/FFmpeg-Builds/releases/download/$FFMPEG_TAG/$FFMPEG_ASSET" + +# What actually ships. Every DLL ffmpeg.exe/ffprobe.exe in this build link +# against, plus the two executables. ffplay.exe (an SDL2 player, ~17 MB) is +# deliberately left out -- collect_all-style "take everything" is right for +# the PyInstaller spec's Python deps, wrong here where the vendor zip bundles +# a whole extra program MeshBay never runs. +$KEEP_FILES = @( + "ffmpeg.exe", "ffprobe.exe", + "avcodec-63.dll", "avdevice-63.dll", "avfilter-12.dll", "avformat-63.dll", + "avutil-61.dll", "swresample-7.dll", "swscale-10.dll" +) + +function Step($msg) { Write-Host "==> $msg" -ForegroundColor Cyan } + +$cacheDir = Join-Path $env:LOCALAPPDATA "meshbay-build-cache" +New-Item -ItemType Directory -Force -Path $cacheDir | Out-Null +$zipPath = Join-Path $cacheDir $FFMPEG_ASSET + +if ($SkipCache -or -not (Test-Path $zipPath) -or + (Get-FileHash $zipPath -Algorithm SHA256).Hash.ToLower() -ne $FFMPEG_SHA256) { + Step "downloading $FFMPEG_ASSET ($FFMPEG_TAG)" + $ua = "meshbay-build-script (+https://meshbay.org)" + Invoke-WebRequest -Uri $FFMPEG_URL -OutFile $zipPath -UseBasicParsing -UserAgent $ua +} +else { + Step "using cached $FFMPEG_ASSET" +} + +$actual = (Get-FileHash $zipPath -Algorithm SHA256).Hash.ToLower() +if ($actual -ne $FFMPEG_SHA256) { + Remove-Item $zipPath -Force -ErrorAction SilentlyContinue + throw "ffmpeg download checksum mismatch: got $actual, expected $FFMPEG_SHA256 -- refusing to use it" +} +Step "checksum verified" + +$extractDir = Join-Path $cacheDir "extracted" +Remove-Item $extractDir -Recurse -Force -ErrorAction SilentlyContinue +Expand-Archive -Path $zipPath -DestinationPath $extractDir +$binDir = Join-Path (Get-ChildItem $extractDir -Directory | Select-Object -First 1).FullName "bin" +if (-not (Test-Path $binDir)) { throw "expected bin\ under the extracted archive, found none" } + +New-Item -ItemType Directory -Force -Path $OutDir | Out-Null +foreach ($name in $KEEP_FILES) { + $src = Join-Path $binDir $name + if (-not (Test-Path $src)) { throw "expected file missing from the ffmpeg build: $name" } + Copy-Item $src (Join-Path $OutDir $name) -Force +} + +$noticeSrc = Join-Path $PSScriptRoot "LICENSE-ffmpeg.txt" +Copy-Item $noticeSrc (Join-Path $OutDir "LICENSE-ffmpeg.txt") -Force + +Step "smoke test: encode + probe" +$smokeOut = Join-Path $env:TEMP "meshbay-ffmpeg-smoke.mp4" +Remove-Item $smokeOut -ErrorAction SilentlyContinue +& (Join-Path $OutDir "ffmpeg.exe") -hide_banner -loglevel error ` + -f lavfi -i "testsrc=duration=1:size=320x240:rate=10" ` + -c:v libx264 -pix_fmt yuv420p -y $smokeOut +if ($LASTEXITCODE -ne 0 -or -not (Test-Path $smokeOut)) { + throw "bundled ffmpeg failed to encode a test clip with libx264" +} +$codec = & (Join-Path $OutDir "ffprobe.exe") -hide_banner -v error ` + -show_entries stream=codec_name -of default=noprint_wrappers=1:nokey=1 $smokeOut +if ($codec.Trim() -ne "h264") { throw "expected h264, ffprobe reported '$codec'" } +Remove-Item $smokeOut -ErrorAction SilentlyContinue + +$mb = "{0:N0} MB" -f ((Get-ChildItem $OutDir -File | Measure-Object Length -Sum).Sum / 1MB) +Write-Host "OK ffmpeg bundled to $OutDir ($mb)" -ForegroundColor Green -- cgit v1.2.3