using System.Threading.Channels; using Microsoft.Extensions.Logging.Abstractions; using TeamsISO.Engine.Domain; using TeamsISO.Engine.Pipeline; using TeamsISO.Engine.Tests.Fakes; namespace TeamsISO.Engine.Tests.Pipeline; public class FrameProcessorTests { private static readonly FrameProcessingSettings Settings1080p30 = new(TargetFramerate.Fps30, TargetResolution.R1080p, AspectMode.Pillarbox, AudioMode.Auto); private static RawFrame MakeFrame(int width, int height, long ts) => new(width, height, ts, new byte[width * height * 4], PixelFormat.Bgra); private static FrameProcessor NewProcessor( FakeFrameClock clock, Channel input, Channel output, FrameProcessingSettings? settings = null) => new( settings: settings ?? Settings1080p30, scaler: new PassthroughFrameScaler(), slateRenderer: new SolidFrameRenderer(), clock: clock, input: input.Reader, output: output.Writer, slateThreshold: TimeSpan.FromSeconds(2.5), logger: NullLogger.Instance); [Fact] public async Task ProcessOnce_NewFrameAvailable_EmitsScaledFrame() { var clock = new FakeFrameClock(); var input = Channel.CreateBounded(4); var output = Channel.CreateUnbounded(); var proc = NewProcessor(clock, input, output); input.Writer.TryWrite(MakeFrame(640, 360, ts: 100)); clock.Advance(TimeSpan.FromMilliseconds(34)); await proc.ProcessOnceAsync(CancellationToken.None); output.Reader.TryRead(out var frame).Should().BeTrue(); frame!.Width.Should().Be(1920); frame.Height.Should().Be(1080); } [Fact] public async Task ProcessOnce_NoNewFrame_ReEmitsLastFrame() { var clock = new FakeFrameClock(); var input = Channel.CreateBounded(4); var output = Channel.CreateUnbounded(); var proc = NewProcessor(clock, input, output); input.Writer.TryWrite(MakeFrame(640, 360, ts: 100)); clock.Advance(TimeSpan.FromMilliseconds(34)); await proc.ProcessOnceAsync(CancellationToken.None); output.Reader.TryRead(out _).Should().BeTrue(); clock.Advance(TimeSpan.FromMilliseconds(34)); await proc.ProcessOnceAsync(CancellationToken.None); output.Reader.TryRead(out var second).Should().BeTrue(); second!.Width.Should().Be(1920); proc.Stats.FramesDuplicated.Should().Be(1); } [Fact] public async Task ProcessOnce_NoFrameForLongerThanSlateThreshold_EmitsSlate() { var clock = new FakeFrameClock(); var input = Channel.CreateBounded(4); var output = Channel.CreateUnbounded(); var proc = NewProcessor(clock, input, output); input.Writer.TryWrite(MakeFrame(640, 360, ts: 100)); clock.Advance(TimeSpan.FromMilliseconds(34)); await proc.ProcessOnceAsync(CancellationToken.None); output.Reader.TryRead(out _); clock.Advance(TimeSpan.FromSeconds(3)); await proc.ProcessOnceAsync(CancellationToken.None); output.Reader.TryRead(out var slate).Should().BeTrue(); slate!.Width.Should().Be(1920); slate.Height.Should().Be(1080); slate.Pixels.Span[0].Should().Be(0x80); } [Fact] public async Task ProcessOnce_PicksNewestFrame_DropsOlder() { var clock = new FakeFrameClock(); var input = Channel.CreateBounded(4); var output = Channel.CreateUnbounded(); var proc = NewProcessor(clock, input, output); input.Writer.TryWrite(MakeFrame(640, 360, ts: 100)); input.Writer.TryWrite(MakeFrame(640, 360, ts: 200)); input.Writer.TryWrite(MakeFrame(640, 360, ts: 300)); clock.Advance(TimeSpan.FromMilliseconds(34)); await proc.ProcessOnceAsync(CancellationToken.None); proc.Stats.FramesIn.Should().Be(3); proc.Stats.FramesDropped.Should().Be(2); } }