using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Threading; using System.Threading.Tasks; using Xunit; using ytLive.Services; using ytLive.Services.Encoder; namespace ytLive.Tests; /// /// TASK 47 Good Dog: the alert clip decoder paces VIDEO to real time via the /// frame-rate probe. The 2026-09-26 live failure was a healthy decoder whose /// frames all dumped in the first ~1-2s (no probe = no pacing), leaving the box /// frozen on the last frame while the audio ran its ~8s — read as "no video /// plays". This drives the REAL against a fake /// process + probe and records the pacing delay it requests per frame (the same /// seam the media path's decoder test uses — the repo never runs a real codec). /// public class AlertClipDecoderTests { [Fact] public async Task AlertClipDecoder_PacesVideoFramesToTheProbedFrameRate() { const int w = 1, h = 1; // frame bytes = 4 var payload = new byte[12]; // three frames var delays = new List(); using var decoder = new AlertClipDecoder( "clip.mp4", w, h, new FakeLocator(), () => new FakeDecodeProcess(payload), frameRateProbe: new FakeFrameRateProbe(100.0), delay: (t, _) => { delays.Add(t); return Task.CompletedTask; }); var forwarded = 0; var completed = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); decoder.FrameAvailable += _ => forwarded++; decoder.Completed += () => completed.TrySetResult(true); decoder.Start(); await completed.Task.WaitAsync(TimeSpan.FromSeconds(5)); Assert.Equal(3, forwarded); // One pacing delay per emitted frame, each = 1/100s (the probed rate). Assert.Equal(forwarded, delays.Count); foreach (var d in delays) Assert.True(Math.Abs((d - TimeSpan.FromMilliseconds(10)).TotalMilliseconds) < 0.001, $"expected ~10ms, got {d.TotalMilliseconds}ms"); } private sealed class FakeDecodeProcess : IDecodeProcess { private readonly MemoryStream _stream; public FakeDecodeProcess(byte[] bytes) => _stream = new MemoryStream(bytes); public void Start(ProcessStartInfo startInfo) { } public Stream StandardOutput => _stream; public bool HasExited => _stream.Position >= _stream.Length; public int ExitCode => 0; public void Kill() { } public Task WaitForExitAsync(CancellationToken ct = default) => Task.CompletedTask; public void Dispose() => _stream.Dispose(); } private sealed class FakeFrameRateProbe : IFrameRateProbe { private readonly double _fps; public FakeFrameRateProbe(double fps) => _fps = fps; public Task ProbeAsync(string path, CancellationToken ct = default) => Task.FromResult(_fps); } private sealed class FakeLocator : IFfmpegLocator { public Task LocateAsync(CancellationToken ct = default) => Task.FromResult("ffmpeg.exe"); } }