110 lines
4.1 KiB
C#
110 lines
4.1 KiB
C#
|
|
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);
|
||
|
|
}
|
||
|
|
}
|