dragon-iso/src/Dragon-ISO.Engine/Pipeline/NdiSender.cs
ZGaetano f11f2b947f
Some checks failed
CI / build-and-test (push) Failing after 24s
feat(engine): NdiSender forwards the configured frame rate to CreateSender
Adds optional frameRateN/frameRateD ctor params (defaulted to 60000/1001)
that NdiSender passes straight through to INdiInterop.CreateSender, so the
sender advertises the operator's actual target cadence instead of a fixed
59.94. Default keeps every existing test green.
2026-06-13 08:28:25 -04:00

73 lines
2.4 KiB
C#

using System.Threading.Channels;
using Microsoft.Extensions.Logging;
using DragonISO.Engine.Interop;
namespace DragonISO.Engine.Pipeline;
/// <summary>
/// Pulls processed frames from a channel and forwards them to <see cref="INdiInterop.SendFrame"/>.
/// </summary>
public sealed class NdiSender : IDisposable
{
private readonly INdiInterop _interop;
private readonly string _outputName;
private readonly ChannelReader<ProcessedFrame> _input;
private readonly ILogger<NdiSender> _logger;
private readonly NdiSenderHandle _handle;
private long _framesSent;
public NdiSender(
INdiInterop interop,
string outputName,
ChannelReader<ProcessedFrame> input,
ILogger<NdiSender> logger,
string? outputGroups = null,
int frameRateN = 60000,
int frameRateD = 1001)
{
_interop = interop;
_outputName = outputName;
_input = input;
_logger = logger;
_handle = interop.CreateSender(outputName, outputGroups, frameRateN, frameRateD);
}
public long FramesSent => Interlocked.Read(ref _framesSent);
/// <summary>
/// Awaits one frame and forwards it. Returns false if the channel is completed.
/// Test seam.
/// </summary>
public async ValueTask<bool> SendNextAsync(CancellationToken cancellationToken)
{
if (!await _input.WaitToReadAsync(cancellationToken))
return false;
if (!_input.TryRead(out var frame))
return false;
_interop.SendFrame(_handle, frame);
Interlocked.Increment(ref _framesSent);
return true;
}
/// <summary>Long-running send loop. Run on a dedicated thread.</summary>
public Task RunAsync(CancellationToken cancellationToken) =>
Task.Factory.StartNew(async () =>
{
try
{
while (!cancellationToken.IsCancellationRequested)
{
var more = await SendNextAsync(cancellationToken);
if (!more) break;
}
}
catch (OperationCanceledException) { }
catch (Exception ex)
{
_logger.LogError(ex, "NdiSender loop crashed for output {Output}.", _outputName);
throw;
}
}, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Default).Unwrap();
public void Dispose() => _handle.Dispose();
}