2026-05-07 11:23:51 -04:00
|
|
|
using System.Threading.Channels;
|
|
|
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
|
using TeamsISO.Engine.Interop;
|
|
|
|
|
|
|
|
|
|
namespace TeamsISO.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,
|
2026-05-07 23:48:49 -04:00
|
|
|
ILogger<NdiSender> logger,
|
|
|
|
|
string? outputGroups = null)
|
2026-05-07 11:23:51 -04:00
|
|
|
{
|
|
|
|
|
_interop = interop;
|
|
|
|
|
_outputName = outputName;
|
|
|
|
|
_input = input;
|
|
|
|
|
_logger = logger;
|
2026-05-07 23:48:49 -04:00
|
|
|
_handle = interop.CreateSender(outputName, outputGroups);
|
2026-05-07 11:23:51 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
}
|