using System.Globalization;
namespace ytLive.Services;
///
/// TASK 64: the key's settings for one layer, in the app's own terms. is the
/// screen as the creator's camera saw it (whatever the detector measured), not a canonical green.
///
public sealed record ChromaKeyOptions(
bool Enabled,
string Color,
double Similarity,
double Softness,
bool Spill,
ChromaScreen Screen = ChromaScreen.Green)
{
public static readonly ChromaKeyOptions Off = new(false, "#00B140", 0.12, 0.08, true);
/// Creator-facing defaults when a detector suggestion is accepted: a little
/// tolerance around the measured colour, a soft edge, and spill cleanup on.
public static ChromaKeyOptions For(ChromaKeySuggestion suggestion)
=> new(true, suggestion.ColorHex ?? "#00B140", 0.12, 0.08, true, suggestion.Screen);
/// Identifies this look, so decode sessions can be shared per look rather than
/// duplicated per layer (two layers keyed identically reuse one decoder).
public string Signature => Enabled
? FormattableString.Invariant(
$"{Color.ToUpperInvariant()}|{Similarity:0.###}|{Softness:0.###}|{Spill}|{Screen}")
: "off";
}
///
/// TASK 64: builds the ffmpeg filter chain for a clip. Keying happens IN THE DECODE PIPE, not in
/// this process: the bundled ffmpeg keys with slices of its own CPU cost, emits straight-alpha
/// BGRA (verified against the shipped binary: keyed pixels come out with alpha 0), and — the
/// point — the pipe is upstream of BOTH consumers (the shared preview bitmap and the compositor's
/// frame), so the creator's preview and the stream cannot disagree.
/// Order matters: scale first (cheaper, and the edge is computed at output size), then key,
/// then despill, then force BGRA so the alpha survives the pipe.
///
public static class VideoFilterChain
{
/// The video half of a clip's `-vf`. Unkeyed clips keep the old single-scale chain.
public static string BuildVideo(int width, int height, ChromaKeyOptions? key = null)
{
var scale = $"scale={width}:{height}";
if (key is not { Enabled: true }) return scale;
var parts = new List(4)
{
scale,
$"colorkey={NormalizeColor(key.Color)}:{Number(key.Similarity)}:{Number(key.Softness)}",
};
if (key.Spill)
parts.Add($"despill=type={(key.Screen == ChromaScreen.Blue ? "blue" : "green")}");
parts.Add("format=bgra");
return string.Join(",", parts);
}
/// ffmpeg takes `0xRRGGBB`. Anything unparseable falls back to the app default
/// rather than producing a filter string ffmpeg would reject at spawn time.
private static string NormalizeColor(string? hex)
{
var value = (hex ?? string.Empty).Trim();
if (value.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) value = value[2..];
if (value.StartsWith('#')) value = value[1..];
if (value.Length != 6) return "0x00B140";
foreach (var c in value)
if (!Uri.IsHexDigit(c)) return "0x00B140";
return "0x" + value.ToUpperInvariant();
}
private static string Number(double value)
=> Math.Clamp(value, 0, 1).ToString("0.###", CultureInfo.InvariantCulture);
}