using System.Threading.Channels; using Microsoft.Extensions.Logging; using TeamsISO.Engine.Domain; namespace TeamsISO.Engine.Pipeline; /// /// 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 /// , emit a no-signal slate instead. /// public sealed class FrameProcessor { private readonly FrameProcessingSettings _settings; private readonly IFrameScaler _scaler; private readonly SolidFrameRenderer _slateRenderer; private readonly IFrameClock _clock; private readonly ChannelReader _input; private readonly ChannelWriter _output; private readonly TimeSpan _slateThreshold; private readonly ILogger _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 input, ChannelWriter output, TimeSpan slateThreshold, ILogger 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; } }