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