dragon-iso/src/tests/TeamsISO.Engine.Tests/Pipeline/FrameProcessorTests.cs

110 lines
4.1 KiB
C#
Raw Normal View History

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<RawFrame> input,
Channel<ProcessedFrame> 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<FrameProcessor>.Instance);
[Fact]
public async Task ProcessOnce_NewFrameAvailable_EmitsScaledFrame()
{
var clock = new FakeFrameClock();
var input = Channel.CreateBounded<RawFrame>(4);
var output = Channel.CreateUnbounded<ProcessedFrame>();
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<RawFrame>(4);
var output = Channel.CreateUnbounded<ProcessedFrame>();
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<RawFrame>(4);
var output = Channel.CreateUnbounded<ProcessedFrame>();
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<RawFrame>(4);
var output = Channel.CreateUnbounded<ProcessedFrame>();
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);
}
}