using System; using System.Diagnostics; using System.IO; using System.Threading; using Xunit; using ytLive.Models; using ytLive.Services; using ytLive.Services.Compositor; using ytLive.Services.Encoder; namespace ytLive.Tests; /// /// TASK 47 Good Dog (take two, 2026-09-26): a decoded alert clip must actually /// reach the OUTPUT canvas. Three live sessions on build 89 all logged a healthy /// decode — Alert clip start … frames=240 audioChunks=156 failed=False, /// alert audio in the live mix — and still painted NOTHING in the box. The layer /// tests pass because they drive a FAKE decoder straight into RenderFrame: /// nothing ever proves the decoded frame survives the trip through the real /// compositor. This is that proof — a REAL ffmpeg clip, the REAL /// , the REAL and the /// REAL , end to end. It is the one test that runs a /// real codec, and deliberately so: the gap it closes is exactly the one a fake /// cannot see. /// [Collection("RealApp")] public sealed class AlertClipOutputTests { private const int BoxW = 320; private const int BoxH = 180; private const int BoxX = 100; private const int BoxY = 100; private const int MasterW = 1920; private const int MasterH = 1080; private readonly RealAppHost _app; public AlertClipOutputTests(RealAppHost app) => _app = app; [Fact] public void AlertClip_RealDecode_PaintsPixelsInsideTheAlertBoxRect() { _app.Run(Run); } private void Run() { var locator = new FfmpegLocator(); var ffmpeg = locator.LocateAsync().GetAwaiter().GetResult(); var clip = Path.Combine(Path.GetTempPath(), $"ytLive-test-alert-{Guid.NewGuid():N}.mp4"); try { MakeClip(ffmpeg, clip); var box = new Source { Type = SourceType.AlertBox, Name = "Stream Alerts", X = BoxX, Y = BoxY, Width = BoxW, Height = BoxH, AlertVideoPath = clip, AlertUseDefaultVideo = false, }; var scene = new Scene { Name = "Live", Elements = { box } }; using var layer = new AlertOverlayLayer( new AlertRenderer(), clipDecoderFactory: (path, w, h) => new AlertClipDecoder( path, w, h, locator, () => new FfmpegDecodeProcess(), frameRateProbe: new FfmpegFrameRateProbe(locator, () => new FfmpegDecodeProcess())), clipPathResolver: b => b.AlertVideoPath); layer.UpdateConfiguredBoxes(new[] { scene }); layer.Enqueue(new ChatMessage { Kind = ChatEventKind.SuperChat, AuthorName = "Funder", SuperChatDisplayString = "$5.00", }); Assert.True(layer.IsPlaying); // The decoder is paced to the media clock, so the first frame arrives in // wall-clock time — poll for it instead of racing it. var deadline = DateTime.UtcNow.AddSeconds(20); while (DateTime.UtcNow < deadline && layer.RenderFrame(box) == null) Thread.Sleep(25); layer.Advance(0.4); // past the 0.3s fade-in → opaque var played = layer.RenderFrame(box); Assert.True(played != null, $"the real decoder produced no frame for {clip} — alpha0={AlphaStats(played)}"); Assert.Equal(255, played!.BgraPixels[3]); var options = new CompositorOptions { SourceRectWidth = MasterW, SourceRectHeight = MasterH, OutputWidth = MasterW, OutputHeight = MasterH, }; var compositor = new SceneCompositor(); var withClip = compositor.Render(scene, e => ReferenceEquals(e, box) ? layer.RenderFrame(box) : null, null, options); var idle = compositor.Render(scene, _ => null, null, options); // The proof: the box rect is a COLORFUL picture that the idle canvas does // not have. Opaque black in both (the compositor fills its base) is not a // pass — the clip's own pixels have to be there. var colorful = ColorfulPixels(withClip, BoxX, BoxY, BoxW, BoxH); var changed = ChangedPixels(withClip, idle, BoxX, BoxY, BoxW, BoxH); Assert.True(colorful > BoxW * BoxH / 2, $"the decoded alert frame must paint the box rect in colour, only {colorful}/{BoxW * BoxH} px colourful"); Assert.True(changed > BoxW * BoxH / 2, $"the alert box rect must differ from the idle canvas, only {changed}/{BoxW * BoxH} px changed"); } finally { try { if (File.Exists(clip)) File.Delete(clip); } catch { /* temp file */ } } } /// Pixels with actual colour in a rect — opaque black (the compositor's /// base fill) does not count, a decoded frame does. private static int ColorfulPixels(VideoFrame frame, int x, int y, int w, int h) { var lit = 0; for (var row = y; row < y + h && row < frame.Height; row++) for (var col = x; col < x + w && col < frame.Width; col++) { var i = (row * frame.Width + col) * 4; if (frame.BgraPixels[i] != 0 || frame.BgraPixels[i + 1] != 0 || frame.BgraPixels[i + 2] != 0) lit++; } return lit; } private static int ChangedPixels(VideoFrame a, VideoFrame b, int x, int y, int w, int h) { var changed = 0; for (var row = y; row < y + h && row < a.Height; row++) for (var col = x; col < x + w && col < a.Width; col++) { var i = (row * a.Width + col) * 4; if (a.BgraPixels[i] != b.BgraPixels[i] || a.BgraPixels[i + 1] != b.BgraPixels[i + 1] || a.BgraPixels[i + 2] != b.BgraPixels[i + 2] || a.BgraPixels[i + 3] != b.BgraPixels[i + 3]) changed++; } return changed; } private static string AlphaStats(VideoFrame? frame) => frame == null ? "no frame" : $"alpha={frame.BgraPixels[3]}"; /// A 3s colourful clip with a tone — the same shape the alert box plays. private static void MakeClip(string ffmpeg, string path) { var psi = new ProcessStartInfo(ffmpeg) { UseShellExecute = false, RedirectStandardError = true, }; foreach (var a in new[] { "-y", "-hide_banner", "-loglevel", "error", "-f", "lavfi", "-i", $"testsrc2=size={BoxW}x{BoxH}:rate=30", "-f", "lavfi", "-i", "sine=frequency=440:sample_rate=48000", "-t", "3", "-c:v", "mpeg4", "-q:v", "3", "-pix_fmt", "yuv420p", "-c:a", "aac", "-shortest", path, }) psi.ArgumentList.Add(a); using var proc = Process.Start(psi)!; var stderr = proc.StandardError.ReadToEnd(); Assert.True(proc.WaitForExit(60_000), "ffmpeg did not finish generating the test clip"); Assert.True(proc.ExitCode == 0, $"ffmpeg failed to generate the test clip: {stderr}"); Assert.True(File.Exists(path), $"ffmpeg produced no clip at {path}"); } }