aboutsummaryrefslogtreecommitdiffstats
path: root/packaging/win
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-05 08:43:11 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-05 08:43:11 +0200
commit301c8998bfdcac80ad3302e2d2ebe853e2ea6de1 (patch)
tree36b0606919f7fb1a7e2ace92d40d8f3e5a6d9d8d /packaging/win
parent171a36984feff5247786b32958b52673e13cc8f0 (diff)
downloadmeshbay-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.txt35
-rw-r--r--packaging/win/README.md35
-rw-r--r--packaging/win/build-node-runtime.ps157
-rw-r--r--packaging/win/build-win.ps111
-rw-r--r--packaging/win/fetch-ffmpeg.ps1131
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