using System;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Xunit;
using ytLive.Services;
using ytLive.Services.Encoder;
namespace ytLive.Tests;
///
/// TASK 64 against a REAL clip (creator's `test_greenscreen_moving_subject.mp4`, 1920×1080, 5 s, a
/// subject moving over a green field). The detector had only ever seen synthetic frames; this runs
/// the bundled ffmpeg, our probe, our detector, and our keyed decode on actual video.
/// It also pins the decision that the key colour comes from the measured screen rather than a
/// canonical green — on this clip the field is #00FE00 and a hardcoded #00B140 keys nothing
/// (measured: 0.0% of pixels transparent), while the detected colour keys 97.8%.
///
public class ChromaKeyRealClipTests
{
private const string ClipName = "test_greenscreen_moving_subject.mp4";
private static string ClipPath => Path.Combine(AppContext.BaseDirectory, "Fixtures", ClipName);
private static string FfmpegPath => Path.Combine(AppContext.BaseDirectory, "ffmpeg", "ffmpeg.exe");
[Fact]
public async Task RealGreenScreenClip_IsDetected_AndKeysWithTheMeasuredColour()
{
Assert.True(File.Exists(ClipPath), $"missing fixture: {ClipPath}");
Assert.True(File.Exists(FfmpegPath), $"the bundled ffmpeg should sit beside the test binary: {FfmpegPath}");
// 1. The probe reads one real frame of the clip.
var probe = new FfmpegChromaProbe(new FixedLocator(FfmpegPath), () => new FfmpegDecodeProcess());
var frame = await probe.ProbeAsync(ClipPath);
Assert.NotNull(frame);
// 2. The detector says "green screen", with high confidence.
var suggestion = ChromaKeyDetector.Detect(frame!);
Assert.Equal(ChromaScreen.Green, suggestion.Screen);
Assert.True(suggestion.IsHighConfidence,
$"a clean green field should be high confidence, got {suggestion.Confidence:0.00}");
Assert.NotNull(suggestion.ColorHex);
// …and the colour it names is the field as shot: green-dominant, almost no red or blue.
var (r, g, b) = ParseHex(suggestion.ColorHex!);
Assert.True(g > 0xE0 && r < 0x20 && b < 0x20,
$"the detected colour should be the measured field, got {suggestion.ColorHex}");
// 3. That colour actually keys the clip through our own pipe: most pixels go transparent,
// the moving subject survives.
var keyed = ChromaKeyOptions.For(suggestion);
var firstFrame = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
using var pipe = new VideoClipPipe(
ClipPath, 320, 180, new FixedLocator(FfmpegPath), () => new FfmpegDecodeProcess(),
audio: false, chromaKey: keyed);
pipe.FrameAvailable += f => firstFrame.TrySetResult(f);
await pipe.StartAsync();
var keyedFrame = await firstFrame.Task.WaitAsync(TimeSpan.FromSeconds(30));
await pipe.StopAsync();
var transparent = 0;
for (var i = 3; i < keyedFrame.BgraPixels.Length; i += 4)
if (keyedFrame.BgraPixels[i] == 0) transparent++;
var ratio = (double)transparent / (keyedFrame.Width * keyedFrame.Height);
Assert.True(ratio > 0.5,
$"the keyed decode left only {ratio:P1} transparent — the filter chain is not keying this clip");
Assert.True(ratio < 1.0, "everything vanished; the subject should survive the key");
}
///
/// Throughput guard (2026-11-05): the pipe used to copy a growing pending buffer on every 64 KB
/// read, so a 1080p frame cost ~0.5 GB of copying and playback pinned at ~2 fps whatever the
/// pacing said. A 30 fps clip must here deliver at least 15 fps (measured 29 in a Debug build) —
/// a floor wide enough for slow hardware and far above the 2 fps regression.
///
[Fact]
public async Task RealClip_PlaysAtItsNativeRate()
{
Assert.True(File.Exists(ClipPath), $"missing fixture: {ClipPath}");
var locator = new FixedLocator(FfmpegPath);
var probe = new FfmpegFrameRateProbe(locator, () => new FfmpegDecodeProcess());
var frames = 0;
using var pipe = new VideoClipPipe(
ClipPath, 1920, 1080, locator, () => new FfmpegDecodeProcess(),
frameRateProbe: probe, audio: false);
pipe.FrameAvailable += _ => Interlocked.Increment(ref frames);
var clock = System.Diagnostics.Stopwatch.StartNew();
await pipe.StartAsync();
await Task.Delay(2000);
await pipe.StopAsync();
clock.Stop();
var fps = frames / clock.Elapsed.TotalSeconds;
Assert.True(fps >= 15, $"playback ran at {fps:0.0} fps — frames are being lost in the pipe again");
}
private static (int R, int G, int B) ParseHex(string hex)
{
var value = hex.TrimStart('#');
return (Convert.ToInt32(value[..2], 16), Convert.ToInt32(value.Substring(2, 2), 16),
Convert.ToInt32(value.Substring(4, 2), 16));
}
private sealed class FixedLocator : IFfmpegLocator
{
private readonly string _path;
public FixedLocator(string path) => _path = path;
public Task LocateAsync(CancellationToken ct = default) => Task.FromResult(_path);
}
}