using System.IO; using ytLive.Helpers; namespace ytLive.Services.Encoder; /// /// The default : resolve the bundled FFmpeg that ships /// next to the app, falling back to PATH for anyone who deliberately installed their own. /// /// Nothing is downloaded. This type used to pin a BtbN release URL, download /// the zip on first run, and execute it — which is Windows Store policy 10.2.2 (dynamic /// code inclusion, cited in TASKS/research-store-certification.md §4) and already /// cost a real failure: the pin aged out of BtbN's 14-day retention and the download /// 404'd on the creator's first real recording attempt (2026-09-01). The binaries /// are now fetched at BUILD time by scripts/fetch-ffmpeg.sh (pinned URL + SHA-256) /// and copied into ffmpeg/ beside the executable. /// /// Order is bundled-first, deliberately. The old code probed PATH first /// ("the user's own install wins"). With a bundle in the package that is the wrong way /// round: a random PATH ffmpeg may be a different major version or a GPL build, and /// the bundle is the one we pinned, license-vetted, and tested against. PATH stays as the /// escape hatch, which is what it is actually good for. /// /// The lgpl-shared variant is a licensing choice, not a packaging one: dynamic /// linking makes LGPL compliance "license text + source offer" with no static-relink /// (LGPL §6) material required. That means the libav*.dll family must sit in the /// same folder as the exe — Windows resolves them from the executable's own directory. /// public sealed class FfmpegLocator : IFfmpegLocator { public const string FileName = "ffmpeg.exe"; /// Sibling probe binary — bundled alongside ffmpeg so the media /// frame-rate probe () can /// derive its path from the located ffmpeg directory. public const string ProbeFileName = "ffprobe.exe"; /// Folder, relative to the app base directory, that the build copies the /// bundled build into. One folder for the exe and every DLL. public const string BundleFolderName = "ffmpeg"; /// Base directory the bundle is resolved against. Injectable so tests can /// point at a temp tree; defaults to the running app's base directory. private readonly string _bundleRoot; private readonly string[] _searchDirs; public FfmpegLocator(string[]? searchDirs = null, string? bundleRoot = null) { _searchDirs = searchDirs ?? ParsePath(); _bundleRoot = bundleRoot ?? AppContext.BaseDirectory; } public Task LocateAsync(CancellationToken cancellationToken = default) { var bundled = Path.Combine(_bundleRoot, BundleFolderName, FileName); if (IsUsable(bundled)) return Task.FromResult(bundled); foreach (var dir in _searchDirs) { if (string.IsNullOrEmpty(dir)) continue; var candidate = Path.Combine(dir, FileName); if (IsUsable(candidate)) return Task.FromResult(candidate); } // Deliberately loud. The previous cache-then-download ladder had a "found // something on disk" path that could satisfy a missing bundle with a stale copy, // which is exactly the silent-drift a certification-gated dependency must not // have. If the package forgot ffmpeg/, the app says so and stops. throw new IOException( $"FFmpeg was not found. The bundled copy is missing from " + $"'{Path.Combine(_bundleRoot, BundleFolderName)}' — this build is incomplete " + "(reinstall or re-fetch with scripts/fetch-ffmpeg.sh). Installing an LGPL-shared " + "FFmpeg and putting 'ffmpeg' on PATH also works."); } private static bool IsUsable(string path) => File.Exists(path) && new FileInfo(path).Length > 0; private static string[] ParsePath() { var raw = Environment.GetEnvironmentVariable("PATH") ?? ""; return raw.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries); } }