100 lines
3.6 KiB
C#
100 lines
3.6 KiB
C#
using System.Threading.Channels;
|
|
using Microsoft.Extensions.Logging;
|
|
using TeamsISO.Engine.Domain;
|
|
|
|
namespace TeamsISO.Engine.Pipeline;
|
|
|
|
/// <summary>
|
|
/// Per-ISO frame timing engine. Implements closest-frame strategy: at each tick,
|
|
/// pick the newest available raw frame (dropping older queued frames), scale and emit it.
|
|
/// If no new frame is available, re-emit the last frame. If no frame has arrived for
|
|
/// <see cref="_slateThreshold"/>, emit a no-signal slate instead.
|
|
/// </summary>
|
|
public sealed class FrameProcessor
|
|
{
|
|
private readonly FrameProcessingSettings _settings;
|
|
private readonly IFrameScaler _scaler;
|
|
private readonly SolidFrameRenderer _slateRenderer;
|
|
private readonly IFrameClock _clock;
|
|
private readonly ChannelReader<RawFrame> _input;
|
|
private readonly ChannelWriter<ProcessedFrame> _output;
|
|
private readonly TimeSpan _slateThreshold;
|
|
private readonly ILogger<FrameProcessor> _logger;
|
|
|
|
private RawFrame? _lastRawFrame;
|
|
private long _lastFrameTickTicks;
|
|
private long _framesIn;
|
|
private long _framesOut;
|
|
private long _framesDropped;
|
|
private long _framesDuplicated;
|
|
private long _framesSlated;
|
|
|
|
public FrameProcessor(
|
|
FrameProcessingSettings settings,
|
|
IFrameScaler scaler,
|
|
SolidFrameRenderer slateRenderer,
|
|
IFrameClock clock,
|
|
ChannelReader<RawFrame> input,
|
|
ChannelWriter<ProcessedFrame> output,
|
|
TimeSpan slateThreshold,
|
|
ILogger<FrameProcessor> logger)
|
|
{
|
|
_settings = settings;
|
|
_scaler = scaler;
|
|
_slateRenderer = slateRenderer;
|
|
_clock = clock;
|
|
_input = input;
|
|
_output = output;
|
|
_slateThreshold = slateThreshold;
|
|
_logger = logger;
|
|
}
|
|
|
|
public IsoHealthStats Stats =>
|
|
new(
|
|
FramesIn: Interlocked.Read(ref _framesIn),
|
|
FramesOut: Interlocked.Read(ref _framesOut),
|
|
FramesDropped: Interlocked.Read(ref _framesDropped),
|
|
FramesDuplicated: Interlocked.Read(ref _framesDuplicated),
|
|
LastFrameAt: _lastFrameTickTicks == 0 ? null : new DateTimeOffset(_lastFrameTickTicks, TimeSpan.Zero),
|
|
IncomingFps: 0,
|
|
IncomingWidth: _lastRawFrame?.Width ?? 0,
|
|
IncomingHeight: _lastRawFrame?.Height ?? 0);
|
|
|
|
public Task ProcessOnceAsync(CancellationToken cancellationToken)
|
|
{
|
|
// Drain the input channel non-blockingly, keeping only the newest frame.
|
|
RawFrame? newest = null;
|
|
while (_input.TryRead(out var frame))
|
|
{
|
|
if (newest is not null)
|
|
Interlocked.Increment(ref _framesDropped);
|
|
newest = frame;
|
|
Interlocked.Increment(ref _framesIn);
|
|
}
|
|
|
|
var (targetW, targetH) = _settings.ResolutionSize;
|
|
var nowTicks = _clock.NowTicks;
|
|
|
|
ProcessedFrame toEmit;
|
|
if (newest is not null)
|
|
{
|
|
_lastRawFrame = newest;
|
|
_lastFrameTickTicks = nowTicks;
|
|
toEmit = _scaler.Scale(newest, targetW, targetH, _settings.Aspect, nowTicks);
|
|
}
|
|
else if (_lastRawFrame is not null && (nowTicks - _lastFrameTickTicks) <= _slateThreshold.Ticks)
|
|
{
|
|
Interlocked.Increment(ref _framesDuplicated);
|
|
toEmit = _scaler.Scale(_lastRawFrame, targetW, targetH, _settings.Aspect, nowTicks);
|
|
}
|
|
else
|
|
{
|
|
Interlocked.Increment(ref _framesSlated);
|
|
toEmit = _slateRenderer.Render(targetW, targetH, b: 0x80, g: 0x80, r: 0x80, a: 0xFF, nowTicks);
|
|
}
|
|
|
|
Interlocked.Increment(ref _framesOut);
|
|
_output.TryWrite(toEmit);
|
|
return Task.CompletedTask;
|
|
}
|
|
}
|