diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-09-05 08:43:11 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-09-05 08:43:11 +0200 |
| commit | 301c8998bfdcac80ad3302e2d2ebe853e2ea6de1 (patch) | |
| tree | 36b0606919f7fb1a7e2ace92d40d8f3e5a6d9d8d /packaging/win | |
| parent | 171a36984feff5247786b32958b52673e13cc8f0 (diff) | |
| download | meshbay-301c8998bfdcac80ad3302e2d2ebe853e2ea6de1.tar.gz | |
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 <noreply@anthropic.com>
Diffstat (limited to 'packaging/win')
| -rw-r--r-- | packaging/win/LICENSE-ffmpeg.txt | 35 | ||||
| -rw-r--r-- | packaging/win/README.md | 35 | ||||
| -rw-r--r-- | packaging/win/build-node-runtime.ps1 | 57 | ||||
| -rw-r--r-- | packaging/win/build-win.ps1 | 11 | ||||
| -rw-r--r-- | packaging/win/fetch-ffmpeg.ps1 | 131 |
5 files changed, 236 insertions, 33 deletions
diff --git a/packaging/win/LICENSE-ffmpeg.txt b/packaging/win/LICENSE-ffmpeg.txt new file mode 100644 index 0000000..2f43acc --- /dev/null +++ b/packaging/win/LICENSE-ffmpeg.txt @@ -0,0 +1,35 @@ +FFmpeg — third-party notice +============================ + +This package redistributes an unmodified build of FFmpeg (ffmpeg.exe, +ffprobe.exe, and the shared libraries they link against). MeshBay uses it to +transcode video into a browser-compatible format (H.264/libx264) for +streaming; it is a separate program invoked as a subprocess, not code linked +into MeshBay itself. + +Because that build includes libx264, it is licensed under the GNU General +Public License, version 3 (GPLv3) — no LGPL-only FFmpeg build includes an +H.264 encoder, since libx264 itself is GPL. The full license text is at: + + https://www.gnu.org/licenses/gpl-3.0.txt + +Source code +----------- + +This build was produced, unmodified, by the BtbN/FFmpeg-Builds project from +FFmpeg's own public source. Corresponding source for the exact version +bundled here is available from: + + FFmpeg itself: https://ffmpeg.org / https://github.com/FFmpeg/FFmpeg + This build's scripts: https://github.com/BtbN/FFmpeg-Builds + This exact release: https://github.com/BtbN/FFmpeg-Builds/releases/tag/autobuild-2026-09-04-14-01 + FFmpeg revision: N-126404-g818e5d965b (2026-09-04) + +Neither FFmpeg's source nor MeshBay's own build process modifies this binary +in any way; it is carried through unchanged from the release named above. + +No warranty +----------- + +FFmpeg is provided "as is", without warranty of any kind, per the terms of +the GPLv3 linked above. diff --git a/packaging/win/README.md b/packaging/win/README.md index 7bbec13..b5ab1cd 100644 --- a/packaging/win/README.md +++ b/packaging/win/README.md @@ -17,7 +17,9 @@ Linux). `meshbay-common` rides along inside the node runtime. │ └─ node-runtime\ │ ├─ meshbay-node.exe frozen daemon (PyInstaller onedir) │ ├─ _internal\ … its Python + deps (aiortc, av, aioquic, …) -│ └─ ffmpeg.exe, ffprobe.exe only if built with -FfmpegDir +│ ├─ ffmpeg.exe, ffprobe.exe bundled by default, see Video (ffmpeg) below +│ ├─ av*.dll, swscale/swresample*.dll what those two link against +│ └─ LICENSE-ffmpeg.txt GPLv3 notice + where the source is └─ Uninstall MeshBay.exe ``` @@ -91,18 +93,35 @@ That runs [`build-win.ps1`](build-win.ps1): | 5 | [`build-node-runtime.ps1`](build-node-runtime.ps1) — PyInstaller freeze → `packages/meshbay-client/node-runtime/` | | 6 | `electron-builder --win nsis` → `packages/meshbay-client/dist/MeshBay-Setup-<version>.exe` | -### ffmpeg +### Video (ffmpeg) -Not bundled by default — the node resolves `ffmpeg`/`ffprobe` from `PATH` at -startup, and video streaming needs them. To make the installer self-contained: +**Bundled by default** — [`fetch-ffmpeg.ps1`](fetch-ffmpeg.ps1) downloads, +checksum-verifies and stages it into `node-runtime/` on every build, ~161 MB. +Not optional in practice: MeshBay transcodes browser-incompatible video to +H.264 (`-c:v libx264`, a real encode, not remux), and no LGPL-only ffmpeg +build includes an H.264 *encoder* — libx264 itself is GPL. Asking an end user +to separately run `winget install ffmpeg` was considered and rejected: it +needs network access and `winget`/App Installer present at that exact moment, +and its failure mode is silent — video just does not stream, with nothing +pointing back at ffmpeg. + +Source: [BtbN/FFmpeg-Builds](https://github.com/BtbN/FFmpeg-Builds), the +Windows x86_64 **gpl-shared** preset — `ffmpeg.exe`/`ffprobe.exe` plus the +DLLs they both link against, rather than two independent static binaries +(the "full" static build many devs already have via `winget install ffmpeg` +is ~220 MB *per executable*; the equivalent Linux install used a shared +build for the same reason). `ffplay.exe` (an SDL2 player, ~17 MB) is dropped +— the daemon never invokes it. Pinned to one dated release tag, not the +`latest` alias BtbN repoints on every auto-build; both the tag and the sha256 +are hardcoded in the script and re-pinning is a deliberate edit, not +automatic. `LICENSE-ffmpeg.txt` (GPLv3 notice + where the source is) rides +along in the same directory — required, since this redistributes a GPL +binary even though it is unmodified and invoked only as a subprocess. ```powershell -npm run dist:win -- -FfmpegDir "C:\path\to\ffmpeg\bin" -# or: $env:MESHBAY_FFMPEG_DIR = "C:\path\to\ffmpeg\bin"; npm run dist:win +npm run dist:win -- -SkipFfmpeg # smaller, streaming-less build for local iteration only ``` -They are copied beside `meshbay-node.exe`, which is on the daemon's search path. - ### Iterating ```powershell diff --git a/packaging/win/build-node-runtime.ps1 b/packaging/win/build-node-runtime.ps1 index 5097b37..20eacc8 100644 --- a/packaging/win/build-node-runtime.ps1 +++ b/packaging/win/build-node-runtime.ps1 @@ -13,17 +13,22 @@ console-script wrapper bakes in an absolute interpreter path and does not survive being installed somewhere else). - ffmpeg/ffprobe are NOT bundled by default. Pass -FfmpegDir (or set - MESHBAY_FFMPEG_DIR) to copy them in beside the daemon; otherwise the node - resolves them from PATH at startup (meshbay_node.platform.check_media_tools) - and streaming needs ffmpeg installed separately. + ffmpeg/ffprobe are bundled by DEFAULT (fetch-ffmpeg.ps1) -- a real, silent + Windows install cannot ask an end user to separately run + `winget install ffmpeg`, and video streaming needs a genuine H.264 + encoder (libx264, GPL; no LGPL-only build has one), so there is no + smaller "it'll resolve from PATH" fallback worth defaulting to. Pass + -SkipFfmpeg for a smaller, streaming-less build for local iteration. .PARAMETER Python Interpreter used to build. Must be 3.12+ and able to install the packages. Default: a throwaway venv this script creates under build/_node-build-venv. -.PARAMETER FfmpegDir - Directory containing ffmpeg.exe and ffprobe.exe to bundle. Optional. +.PARAMETER SkipFfmpeg + Skip bundling ffmpeg. The node then resolves it from PATH at startup + (meshbay_node.platform.check_media_tools), and streaming needs ffmpeg + installed separately -- fine for a quick local iteration, not for a build + anyone else will install. .PARAMETER KeepBuildVenv Do not delete the throwaway build venv on success (faster re-runs). @@ -31,7 +36,7 @@ [CmdletBinding()] param( [string]$Python = "", - [string]$FfmpegDir = $env:MESHBAY_FFMPEG_DIR, + [switch]$SkipFfmpeg, [switch]$KeepBuildVenv ) @@ -104,17 +109,13 @@ if (-not (Test-Path (Join-Path $frozen "meshbay-node.exe"))) { throw "PyInstaller did not produce meshbay-node.exe at $frozen" } -# --- 4. optional ffmpeg ---------------------------------------------- -if ($FfmpegDir) { - foreach ($tool in @("ffmpeg.exe", "ffprobe.exe")) { - $src = Join-Path $FfmpegDir $tool - if (-not (Test-Path $src)) { throw "$tool not found in $FfmpegDir" } - Copy-Item $src (Join-Path $frozen $tool) - Step "bundled $tool" - } +# --- 4. ffmpeg (bundled by default) ----------------------------------- +if ($SkipFfmpeg -or $env:MESHBAY_SKIP_FFMPEG -eq "1") { + Write-Host " !! ffmpeg not bundled (-SkipFfmpeg) -- the node will look for it on PATH, and streaming needs it installed separately" -ForegroundColor Yellow } else { - Write-Host " ffmpeg not bundled -- the node will look for it on PATH" -ForegroundColor Yellow + Step "fetching ffmpeg (verified against a pinned checksum; cached after the first build)" + & (Join-Path $WinDir "fetch-ffmpeg.ps1") -OutDir $frozen } # --- 5. default.env (shared TMDB token) ------------------------------ @@ -174,12 +175,28 @@ if ($createdVenv -and -not $KeepBuildVenv) { # --- 7. smoke test ----------------------------------------------- # Capture, do NOT pipe to Select-Object -First: that stops the native process -# mid-write and reports a spurious non-zero exit. +# mid-write and reports a spurious non-zero exit. `& exe ... 2>&1` returns an +# ARRAY of lines once the output wraps past one line (which --help's now does, +# with autostart/service added) -- `$array -notmatch X` is a FILTER, not a +# boolean test: it returns the *non-matching* lines, and any non-empty array +# is truthy in `if()` regardless of what is in it. Almost every help line +# lacks the literal string "meshbay-node", so this threw unconditionally the +# moment --help grew past one line, having never actually been exercised +# against multi-line output before. Join to one string first, so this is a +# real substring test again. One retry after a short pause too: right after +# extracting ~180 MB of freshly-written DLLs (ffmpeg) an antivirus real-time +# scan can transiently slow or interfere with the very next process launch -- +# print the actual captured output on a genuine failure instead of a bare +# assertion, so it is diagnosable from the log rather than needing a re-run. Step "smoke test: meshbay-node --help" $exe = Join-Path $OutDir "meshbay-node.exe" -$help = & $exe --help 2>&1 -if ($LASTEXITCODE -ne 0) { throw "frozen meshbay-node --help exited $LASTEXITCODE" } -if ($help -notmatch "meshbay-node") { throw "frozen --help output looks wrong" } +$help = (& $exe --help 2>&1) -join "`n" +if ($LASTEXITCODE -ne 0 -or $help -notmatch "meshbay-node") { + Start-Sleep -Seconds 3 + $help = (& $exe --help 2>&1) -join "`n" +} +if ($LASTEXITCODE -ne 0) { throw "frozen meshbay-node --help exited $LASTEXITCODE`n$help" } +if ($help -notmatch "meshbay-node") { throw "frozen --help output looks wrong:`n$help" } $mb = (Get-ChildItem $OutDir -Recurse | Measure-Object Length -Sum).Sum / 1MB Write-Host "" diff --git a/packaging/win/build-win.ps1 b/packaging/win/build-win.ps1 index 4784071..0a30ad5 100644 --- a/packaging/win/build-win.ps1 +++ b/packaging/win/build-win.ps1 @@ -18,9 +18,10 @@ 5. build-node-runtime.ps1 (PyInstaller freeze of the daemon) 6. electron-builder --win nsis -.PARAMETER FfmpegDir - Passed through to build-node-runtime.ps1 -- directory with ffmpeg.exe / - ffprobe.exe to bundle. Optional; without it the node uses PATH. +.PARAMETER SkipFfmpeg + Passed through to build-node-runtime.ps1 -- skip bundling ffmpeg (fetched + and verified by default; see fetch-ffmpeg.ps1). Smaller, streaming-less + build for local iteration only. .PARAMETER NoElectronBump Keep the pinned Electron instead of upgrading to the latest release. @@ -31,7 +32,7 @@ #> [CmdletBinding()] param( - [string]$FfmpegDir = $env:MESHBAY_FFMPEG_DIR, + [switch]$SkipFfmpeg, [switch]$NoElectronBump, [switch]$SkipNodeRuntime ) @@ -90,7 +91,7 @@ try { } else { Step "building the bundled node (PyInstaller)" $rtArgs = @{} - if ($FfmpegDir) { $rtArgs["FfmpegDir"] = $FfmpegDir } + if ($SkipFfmpeg) { $rtArgs["SkipFfmpeg"] = $true } & (Join-Path $WinDir "build-node-runtime.ps1") @rtArgs if ($LASTEXITCODE -ne 0) { throw "build-node-runtime.ps1 failed" } } 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 |