using System; using System.Collections.Generic; using Xunit; using ytLive.Services; namespace ytLive.Tests; /// /// TASK 64 unit tests (pure): does the detector recognise a real green/blue screen, and does it /// keep its mouth shut about things that merely contain green? Plus the decoder filter chain the /// key turns into — asserted as a string, the same way the encoder's ffmpeg args are pinned. /// public class ChromaKeyDetectorTests { private const byte GreenR = 0x00, GreenG = 0xB1, GreenB = 0x40; // #00B140 — a real screen private const byte BlueR = 0x00, BlueG = 0x47, BlueB = 0xBB; // #0047BB [Fact] public void GreenScreen_IsDetected_WithTheColourActuallyShot() { var frame = Build(120, 68, (x, y) => InCentre(x, y, 120, 68) ? Skin() : (GreenR, GreenG, GreenB)); var suggestion = ChromaKeyDetector.Detect(frame); Assert.Equal(ChromaScreen.Green, suggestion.Screen); Assert.True(suggestion.IsHighConfidence, $"confidence {suggestion.Confidence:0.00} should be high"); Assert.Equal("#00B140", suggestion.ColorHex); } [Fact] public void BlueScreen_IsDetected() { var frame = Build(120, 68, (x, y) => InCentre(x, y, 120, 68) ? Skin() : (BlueR, BlueG, BlueB)); var suggestion = ChromaKeyDetector.Detect(frame); Assert.Equal(ChromaScreen.Blue, suggestion.Screen); Assert.True(suggestion.IsHighConfidence); } [Fact] public void LetterboxedScreen_IsStillDetected() { // The clip is wider than the screen: black bars top and bottom. Bars must not count // against the screen, or every letterboxed clip reads as "no backdrop". var frame = Build(120, 68, (x, y) => y < 12 || y >= 56 ? ((byte)0, (byte)0, (byte)0) : (GreenR, GreenG, GreenB)); var suggestion = ChromaKeyDetector.Detect(frame); Assert.Equal(ChromaScreen.Green, suggestion.Screen); Assert.True(suggestion.Confidence >= ChromaKeySuggestion.SuggestFloor, $"letterboxing dropped confidence to {suggestion.Confidence:0.00}"); } [Fact] public void LawnOrPitch_SaysNothing() { // Green, but the greens disagree with each other (hue 100 vs 145 alternating). Keying // this would eat the picture, so "None" is the correct answer. var frame = Build(120, 68, (x, y) => (x / 4 + y / 4) % 2 == 0 ? ((byte)20, (byte)150, (byte)30) // hue ≈ 125, dark-ish grass : ((byte)80, (byte)180, (byte)10)); // hue ≈ 95, sunlit grass var suggestion = ChromaKeyDetector.Detect(frame); Assert.Equal(ChromaScreen.None, suggestion.Screen); } [Fact] public void MixedScene_SaysNothing() { var frame = Build(120, 68, (x, y) => ((byte)(x % 255), (byte)(y % 255), (byte)((x + y) % 255))); var suggestion = ChromaKeyDetector.Detect(frame); Assert.Equal(ChromaScreen.None, suggestion.Screen); } [Fact] public void DarkFrames_AreNotAScreen() { var frame = Build(120, 68, (x, y) => ((byte)0, (byte)(y % 20), (byte)0)); Assert.Equal(ChromaScreen.None, ChromaKeyDetector.Detect(frame).Screen); } // ─── the filter chain the key turns into ─── [Fact] public void UnkeyedClip_KeepsThePlainScaleChain() => Assert.Equal("scale=1920:1080", VideoFilterChain.BuildVideo(1920, 1080, ChromaKeyOptions.Off)); [Fact] public void KeyedClip_ScalesThenKeysThenDespillsThenForcesBgra() { var options = new ChromaKeyOptions(true, "#00B140", 0.12, 0.08, Spill: true); Assert.Equal( "scale=1920:1080,colorkey=0x00B140:0.12:0.08,despill=type=green,format=bgra", VideoFilterChain.BuildVideo(1920, 1080, options)); } [Fact] public void BlueScreen_DespillsBlue() { var options = new ChromaKeyOptions(true, "#0047BB", 0.1, 0.05, Spill: true, ChromaScreen.Blue); Assert.Contains("despill=type=blue", VideoFilterChain.BuildVideo(1920, 1080, options)); } [Fact] public void SpillOff_AndBadColour_StillProduceAValidChain() { var noSpill = new ChromaKeyOptions(true, "not-a-colour", 2.0, -1, Spill: false); var chain = VideoFilterChain.BuildVideo(1280, 720, noSpill); // Out-of-range numbers clamp, an unparseable colour falls back to the app default, and // no despill term is appended — ffmpeg must never be handed a filter it will reject. Assert.Equal("scale=1280:720,colorkey=0x00B140:1:0,format=bgra", chain); } [Fact] public void Signature_IsTheSameForIdenticalLooks_AndDiffersOtherwise() { var a = new ChromaKeyOptions(true, "#00b140", 0.12, 0.08, true); var b = new ChromaKeyOptions(true, "#00B140", 0.12, 0.08, true); var c = new ChromaKeyOptions(true, "#00B140", 0.30, 0.08, true); Assert.Equal(a.Signature, b.Signature); // shared decoder for identical looks Assert.NotEqual(a.Signature, c.Signature); Assert.Equal("off", ChromaKeyOptions.Off.Signature); } // ─── helpers ─── private static bool InCentre(int x, int y, int w, int h) => x > w * 0.35 && x < w * 0.65 && y > h * 0.25 && y < h * 0.75; private static (byte R, byte G, byte B) Skin() => (0xE0, 0xB0, 0x90); private static VideoFrame Build(int width, int height, Func pixel) { var bytes = new byte[width * height * 4]; for (var y = 0; y < height; y++) for (var x = 0; x < width; x++) { var (r, g, b) = pixel(x, y); var i = (y * width + x) * 4; bytes[i] = b; bytes[i + 1] = g; bytes[i + 2] = r; bytes[i + 3] = 255; } return new VideoFrame(width, height, bytes); } }