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 --- docs/PACKAGING-GUIDE.md | 6 +- docs/WINDOWS-PORT.md | 9 +- packages/meshbay-client/package.json | 4 +- packages/meshbay-node/tests/test_packaging_win.py | 87 ++++++++++++++ packaging/win/LICENSE-ffmpeg.txt | 35 ++++++ packaging/win/README.md | 37 ++++-- packaging/win/build-node-runtime.ps1 | 57 ++++++---- packaging/win/build-win.ps1 | 11 +- packaging/win/fetch-ffmpeg.ps1 | 131 ++++++++++++++++++++++ 9 files changed, 336 insertions(+), 41 deletions(-) create mode 100644 packaging/win/LICENSE-ffmpeg.txt create mode 100644 packaging/win/fetch-ffmpeg.ps1 diff --git a/docs/PACKAGING-GUIDE.md b/docs/PACKAGING-GUIDE.md index 4dcda30..9649826 100644 --- a/docs/PACKAGING-GUIDE.md +++ b/docs/PACKAGING-GUIDE.md @@ -86,9 +86,9 @@ It asks two things, both skippable: Running setup again (an upgrade, a repair install) asks neither question if the firewall rules are already there. -**ffmpeg** is required for video streaming and is *not* in the installer unless -it was built with `-FfmpegDir`. Otherwise install it separately -(`winget install ffmpeg`) so the node finds it on `PATH`. +**ffmpeg** — required for video streaming, **bundled in the installer by +default** (verified, checksummed, GPLv3-licensed; `LICENSE-ffmpeg.txt` ships +alongside it). Nothing to install separately. ### First run diff --git a/docs/WINDOWS-PORT.md b/docs/WINDOWS-PORT.md index ceba2b8..da281c3 100644 --- a/docs/WINDOWS-PORT.md +++ b/docs/WINDOWS-PORT.md @@ -333,9 +333,12 @@ what the W3 autostart launcher points at (`platform._node_exe`). The embeddable zip needs pip to produce that wrapper, and the wrapper bakes in an **absolute** interpreter path that breaks the moment the tree is installed elsewhere. -**ffmpeg** is not bundled by default (the node finds it on `PATH`); pass -`-FfmpegDir` to `build-win.ps1` to copy `ffmpeg.exe`/`ffprobe.exe` in beside the -daemon for a self-contained installer. +**ffmpeg is bundled by default** (`packaging/win/fetch-ffmpeg.ps1` — downloaded +from a pinned, checksum-verified BtbN/FFmpeg-Builds release, ~161 MB, GPLv3 +because the transcode path needs libx264 and no LGPL-only build has one; +`LICENSE-ffmpeg.txt` ships with it). `winget install ffmpeg` was considered and +rejected as the mechanism: it needs network + winget present at install time +and fails silently. `-SkipFfmpeg` opts out for a smaller local-iteration build. **Still open:** Authenticode signing (Phase 13.9 — unsigned ⇒ SmartScreen), a Windows CI runner (Phase 18.3), `electron-updater`. First clean-machine diff --git a/packages/meshbay-client/package.json b/packages/meshbay-client/package.json index 55b2dcf..6e15807 100644 --- a/packages/meshbay-client/package.json +++ b/packages/meshbay-client/package.json @@ -32,7 +32,9 @@ { "from": "node-runtime", "to": "node-runtime", - "filter": ["**/*"] + "filter": [ + "**/*" + ] }, { "from": "../../packaging/win/firewall.ps1", diff --git a/packages/meshbay-node/tests/test_packaging_win.py b/packages/meshbay-node/tests/test_packaging_win.py index 22ce44b..31816d2 100644 --- a/packages/meshbay-node/tests/test_packaging_win.py +++ b/packages/meshbay-node/tests/test_packaging_win.py @@ -316,6 +316,29 @@ def test_service_status_reports_state_without_admin(): "calls status/run/end directly, unelevated") +def test_the_help_smoke_test_joins_multiline_output_before_matching(): + """ + `& exe --help 2>&1` is an ARRAY once the output wraps past one line, which + it now does with `autostart`/`service` in the verb list. `$array -notmatch + X` is a FILTER, not a boolean test -- it returns the *non-matching* + elements, 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 an unjoined check throws unconditionally the moment --help exceeds one + line: found when this specific verb list finally grew past that point, + which is exactly the kind of one-off silent breakage a passing build + yesterday gives no warning of today. + """ + src = (WIN / "build-node-runtime.ps1").read_text(encoding="utf-8") + body = src.split("smoke test: meshbay-node --help", 1)[1] + body = body[:800] + assert '-join "`n"' in body, ( + "the captured --help output must be joined to a single string before " + "any -match/-notmatch check, or a multi-line result silently always " + "fails the smoke test") + # And the join has to happen at capture time, not on some other variable. + assert '(& $exe --help 2>&1) -join' in body + + def test_main_js_drives_the_service_task_for_all_three_actions(): """The hard requirement: Start/Stop/Restart from the Node page must control the Scheduled Task when service mode is active, not just spawn a @@ -409,3 +432,67 @@ def test_the_bundled_daemon_goes_on_the_user_path_and_comes_back_off(): # PATH points at the real .exe dir, so `where meshbay-node` resolves to the # binary the client and the W3 launcher use too — not a shim. assert 'MB_NODE_BIN "$INSTDIR\\resources\\node-runtime"' in nsh + + +# ── ffmpeg: bundled by default (fetch-ffmpeg.ps1) ─────────────────────────── + +def test_fetch_ffmpeg_and_its_license_notice_exist(): + assert (WIN / "fetch-ffmpeg.ps1").exists() + notice = WIN / "LICENSE-ffmpeg.txt" + assert notice.exists() + text = notice.read_text(encoding="utf-8") + assert "GPL" in text + assert "ffmpeg.org" in text or "FFmpeg/FFmpeg" in text, ( + "the notice must point at where the corresponding source actually is") + + +def test_ffmpeg_is_pinned_to_a_dated_release_not_the_moving_latest_alias(): + """ + BtbN repoints the "latest" release on every auto-build (their asset + filenames even embed a fresh git-describe each time), so a URL built from + that alias silently changes what a build fetches. The dated tag + (autobuild-YYYY-MM-DD-HH-MM) is immutable once published -- that is what + makes the pinned checksum mean anything. + """ + src = (WIN / "fetch-ffmpeg.ps1").read_text(encoding="utf-8") + tag_match = re.search(r'\$FFMPEG_TAG\s*=\s*"([^"]+)"', src) + assert tag_match, "no $FFMPEG_TAG pin found" + assert re.match(r"autobuild-\d{4}-\d{2}-\d{2}-\d{2}-\d{2}$", tag_match.group(1)), ( + f"{tag_match.group(1)!r} is not a dated release tag") + assert "/releases/latest/" not in src and "/releases/download/latest/" not in src + + sha_match = re.search(r'\$FFMPEG_SHA256\s*=\s*"([0-9a-f]+)"', src) + assert sha_match, "no $FFMPEG_SHA256 pin found" + assert len(sha_match.group(1)) == 64, "not a full sha256 hex digest" + + assert '$FFMPEG_URL' in src and '$FFMPEG_TAG' in src.split("$FFMPEG_URL", 1)[1][:200], ( + "the download URL must be built from the pinned tag") + + +def test_ffplay_is_excluded_from_the_bundle(): + """The vendor zip carries an SDL2 player MeshBay never invokes (~17 MB); + bundling it would be the collect_all-everything instinct applied where a + fixed allowlist is right instead.""" + src = (WIN / "fetch-ffmpeg.ps1").read_text(encoding="utf-8") + keep_block = src.split("$KEEP_FILES", 1)[1].split(")", 1)[0] + assert "ffplay" not in keep_block + assert "ffmpeg.exe" in keep_block and "ffprobe.exe" in keep_block + + +def test_ffmpeg_bundling_is_the_default_not_opt_in(): + """ + Video streaming needs a real H.264 encoder (libx264, GPL -- no LGPL-only + ffmpeg build has one), and asking an end user to separately run + `winget install ffmpeg` is not viable for a non-technical install (needs + network + winget present at that moment, fails silently). So bundling + must be the default, with an explicit opt-out for local iteration -- + the reverse of the old -FfmpegDir opt-in this replaced. + """ + runtime_src = (WIN / "build-node-runtime.ps1").read_text(encoding="utf-8") + assert "SkipFfmpeg" in runtime_src + assert "FfmpegDir" not in runtime_src, "the old opt-in mechanism should be gone, not parallel" + assert "fetch-ffmpeg.ps1" in runtime_src + + win_src = (WIN / "build-win.ps1").read_text(encoding="utf-8") + assert "SkipFfmpeg" in win_src + assert "FfmpegDir" not in win_src 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-.exe` | -### 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: +### Video (ffmpeg) + +**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 -- cgit v1.2.3