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);
}
}