FFmpeg locator (TASK 4 ship step 2) + licensing: notices, About, LGPL-shared pin

IFfmpegLocator seam that resolves ffmpeg.exe (PATH -> cache -> pinned download),
plus the licensing compliance that makes a paid GA product defensible.

NOTE FOR USERS: this change shows NO difference in the app's behavior except a
new top-bar "About" button (opens THIRD-PARTY-NOTICES.txt). It is scaffolding
for the encoder/streaming work.

- Services/Encoder/: IFfmpegLocator + FfmpegLocator. Pin is BtbN lgpl-shared
  autobuild-2026-08-09-13-03 (NOT gyan.dev/GPL or static: LGPLv2.1 §6 static
  relink material avoided by dynamic linking); extracts ffmpeg.exe + libav*.dll
  via a staging dir so a crash never leaves a partial cache
- THIRD-PARTY-NOTICES.txt: LGPL/BSD/MIT notices + source offer, copied to the
  build output, surfaced by the About button; v1 gate = bundle full license
  texts (TASK 4 requirement 9)
- ai.md "Licensing - do not violate" guardrails (never GPL/nonfree/static/latest-
  tag, never link FFmpeg in, never drop notices); TASKS.md + Services/index.md
  updated
- Tests: FfmpegLocatorTests - hermetic decision-ladder integration test + shared-
  build DLL extraction + edge cases; docs updated (78 tests passing, 0 warnings)
This commit is contained in:
2026-08-10 11:02:26 -07:00
parent 18a21010bb
commit 8c7938aca0
10 changed files with 529 additions and 6 deletions
+47 -3
View File
@@ -61,8 +61,10 @@ insert/idempotent/heal + HasBackdrop gate, IsLiveCapture, DisplaySource, INPC),
(the five canonical scenes, Live-only backdrop policy, EnforceBackdropPolicy), WebcamSafeguardTests
(the per-scene size clamp incl. the Chat half-screen-area cap), SceneCompositorTests (the full-scene
composite integration test: backdrop + round webcam + mirrored/bordered images + flash; the vertical
tier 1080×1920 crop/scale), StretchMathTests (UniformToFill cover-crop + bilinear)
72 passing.
tier 1080×1920 crop/scale), StretchMathTests (UniformToFill cover-crop + bilinear), FfmpegLocatorTests
(the PATH → cache → download decision ladder with a fake downloader serving a real in-memory zip; shared
build DLL extraction) —
78 passing.
### Real-MainWindow tests MUST be hermetic (DB pollution bug)
@@ -112,7 +114,7 @@ C# / WPF (.NET 8) following MVVM:
- `Helpers/OAuthCredentials.cs` contains the real ClientId/ClientSecret. Auth is complete and the session **persists via Windows DPAPI** (`Helpers/TokenStore.cs``%APPDATA%\ytLlive\ytLlive.auth`, CurrentUser scope), reloaded best-effort at startup with a proactive refresh of a near-expiry access token. Sign-in/Change Account lives **inside the Start Stream dialog** (two-state flow — no separate Connect button). A **graceful End Livestream signs out**: `StopStream()` clears the session + token, so the next go-live needs a fresh sign-in; a crash never runs End, so the token survives and the creator stays signed in. `YouTubeAuthService` takes an optional `HttpClient` + `sessionChanged` callback (test seam + save hook; services are still constructed in `MainViewModel`)
- Scene/source/asset layout persists (SQLite, schema v6); the OAuth session persists (DPAPI); the paid-unlock state does not (yet — itch.io key verification pending)
- `YouTubeStreamService` uses hardcoded `1080p`/`60fps` and per-broadcast streams — must switch to the v3 `variable` reusable stream
- Webcam capture is shipped (milestone 1); the live desktop/game backdrop is shipped (ship task #1); **the output compositor (TASK 4 ship step 1) is the next build** — full plan in `TASKS.md`; window capture (non-backdrop), the encoder + RTMP push, and audio capture follow it
- Webcam capture is shipped (milestone 1); the live desktop/game backdrop is shipped (ship task #1); **the output compositor (TASK 4 ship step 1) is SHIPPED**, **the FFmpeg locator (TASK 4 ship step 2) is SHIPPED** — full plan in `TASKS.md`; the encoder subprocess + RTMP push, audio capture, and the frame-pipeline wiring follow (each its own PR)
- `StreamConfig` defaults (`TargetBitrate=6000`, `Resolution="1920x1080"`) are stale — the live dropdown drives `StreamHealth.CurrentBitrate`/`FPS` instead
### Screen backdrop capture (TASK 3 ship task #1)
@@ -312,6 +314,48 @@ stays XAML (editing view); the compositor is the output view.
resolver in the encoder step, not the compositor step. The master buffer (the compositor's return
value) is the seam a future D3D11 compositor would honor identically.
### FFmpeg locator (TASK 4 ship step 2 — shipped 2026-08-10, plan in TASKS.md)
The encoder's one external dependency is `ffmpeg.exe`; it's never shipped in the repo. `IFfmpegLocator`
resolves an absolute path on demand: **PATH probe first** (the user's own install wins — their choice,
their responsibility), then the cache (`%APPDATA%\ytLlive\tools\ffmpeg.exe`), then a **pinned** BtbN
LGPL-**shared** win64 zip (~75 MB) from which `ffmpeg.exe` **and the `libav*.dll` family** are extracted
(staged temp-write + move so a crash never corrupts the cache; Windows resolves the DLLs from the exe's
own directory). BtbN LGPL-shared (not gyan.dev, not static): it drops GPL-only libx264/x265 while keeping
NVENC/QSV/AMF + libopenh264 + native AAC, and dynamic linking means LGPL compliance is "license text +
source offer" with no static-relink (§6) material — see the Licensing guardrails below. The pin is a
dated autobuild tag (immutable); BtbN retention keeps the last 14 daily + each month-end for 2 years, so
a cold cache can outlive the pin → the seam throws a clear, logged error (recoverable; the pin is one
const). Constructor-injected search dirs / tools dir / downloader (`Func<string, CancellationToken,
Task<byte[]>>`) keep it hermetic: tests fake the network with a real in-memory zip. Constructed in the
encoder step (not yet — this PR ships the seam + impl + tests only).
### Licensing — do not violate (GA = paid product; see `THIRD-PARTY-NOTICES.txt`)
This product is closed-source and paid. Every third-party component must stay inside the LGPL/BSD/MIT
guardrails below — written down so a future "quick fix" never reintroduces a GPL binary. **NEVER:**
- **Use a GPL FFmpeg build** — gyan.dev's builds are GPLv3 and ship libx264; BtbN's `gpl` variant is
GPL too. GPL in a distributed paid product is the #1 lawsuit risk. Only BtbN `lgpl` / `lgpl-shared`
builds are allowed.
- **Distribute the static lgpl build** — LGPLv2.1 §6 wants relinkable object files for static linking.
The **shared** (dynamic-DLL) build sidesteps that: compliance is "license text + source offer +
unmodified binaries". The pin is `lgpl-shared`; when the pin is refreshed, keep the shared variant.
- **Use BtbN's `nonfree` variant** — it adds fdk-aac (Fraunhofer code licensing). The native FFmpeg AAC
encoder is fine (no Fraunhofer code) but grants no AAC patent license — accepted low-risk posture for
RTMP→YouTube, since encoder vendors cover their implementations (Cisco OpenH264, NVIDIA NVENC, Intel
QSV, AMD AMF).
- **Link FFmpeg into the app** — it stays a separate subprocess fed frames over a pipe; that separation
keeps the app's own code out of LGPL reach.
- **Drop `THIRD-PARTY-NOTICES.txt`** from the shipped app or the About screen, or alter the FFmpeg
copyright/LGPL notices inside the downloaded binaries. Automating the download counts as distribution
— the obligations are not optional.
- **Pin to a moving target** — the `latest` BtbN release tag floats. Only immutable autobuild tags give
a reproducible source offer. Record the tag + variant beside the URL (TASKS.md) every time the pin moves.
- **Forget the v1 license-texts gate** — `THIRD-PARTY-NOTICES.txt` links the canonical license texts; at
**v1 (GA)** the full texts of every license it names MUST ship alongside it (TASK 4 requirement 9 is the
release blocker). Queued early is wrong; the release pass owns it.
## Design Principle
> This software is so intuitive that even the most right-brained person can easily intuit and use it.