summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--docs/PACKAGING-GUIDE.md6
-rw-r--r--docs/WINDOWS-PORT.md9
-rw-r--r--packages/meshbay-client/package.json4
-rw-r--r--packages/meshbay-node/tests/test_packaging_win.py87
-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
9 files changed, 335 insertions, 40 deletions
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-<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