feat(pipeline,interop): add RawFrame, ProcessedFrame, IFrameClock and INdiInterop test seam
Some checks failed
CI / build-and-test (push) Has been cancelled

This commit is contained in:
Zac Gaetano 2026-05-07 15:12:36 +00:00
parent 3f8b5f1a7b
commit f562303b47
8 changed files with 116 additions and 0 deletions

View file

@ -0,0 +1,32 @@
using TeamsISO.Engine.Pipeline;
namespace TeamsISO.Engine.Interop;
/// <summary>
/// Test seam over the NDI SDK. Production: P/Invoke shim. Tests: <c>FakeNdiInterop</c>.
/// All methods are synchronous; the engine threads are responsible for orchestration.
/// </summary>
public interface INdiInterop
{
// ----- Discovery -----
NdiFindHandle CreateFinder();
/// <summary>Snapshots the currently-known sources visible to the finder.</summary>
IReadOnlyList<string> GetCurrentSources(NdiFindHandle finder);
// ----- Receive -----
NdiReceiverHandle CreateReceiver(string sourceFullName);
/// <summary>
/// Blocks for up to <paramref name="timeoutMs"/> waiting for a frame.
/// Returns null on timeout. Returned <see cref="RawFrame"/> ownership transfers to the caller.
/// </summary>
RawFrame? CaptureFrame(NdiReceiverHandle receiver, int timeoutMs);
// ----- Send -----
NdiSenderHandle CreateSender(string outputName);
void SendFrame(NdiSenderHandle sender, ProcessedFrame frame);
// ----- Runtime probe -----
string GetRuntimeVersion();
}

View file

@ -0,0 +1,7 @@
namespace TeamsISO.Engine.Interop;
/// <summary>Opaque handle to an NDI Find instance. Implementation-private.</summary>
public abstract class NdiFindHandle : IDisposable
{
public abstract void Dispose();
}

View file

@ -0,0 +1,6 @@
namespace TeamsISO.Engine.Interop;
public abstract class NdiReceiverHandle : IDisposable
{
public abstract void Dispose();
}

View file

@ -0,0 +1,6 @@
namespace TeamsISO.Engine.Interop;
public abstract class NdiSenderHandle : IDisposable
{
public abstract void Dispose();
}

View file

@ -0,0 +1,14 @@
namespace TeamsISO.Engine.Pipeline;
/// <summary>
/// Test seam over the wall clock. Production: <see cref="PeriodicTimerFrameClock"/>.
/// Tests: <c>FakeFrameClock</c> in TeamsISO.Engine.Tests.
/// </summary>
public interface IFrameClock
{
/// <summary>Current monotonic time as ticks (100 ns).</summary>
long NowTicks { get; }
/// <summary>Awaits the next tick at the current period.</summary>
ValueTask<bool> WaitForNextTickAsync(CancellationToken cancellationToken);
}

View file

@ -0,0 +1,21 @@
namespace TeamsISO.Engine.Pipeline;
public sealed class PeriodicTimerFrameClock : IFrameClock, IDisposable
{
private readonly PeriodicTimer _timer;
public PeriodicTimerFrameClock(double framesPerSecond)
{
if (framesPerSecond <= 0)
throw new ArgumentOutOfRangeException(nameof(framesPerSecond));
var periodMs = 1000.0 / framesPerSecond;
_timer = new PeriodicTimer(TimeSpan.FromMilliseconds(periodMs));
}
public long NowTicks => DateTime.UtcNow.Ticks;
public ValueTask<bool> WaitForNextTickAsync(CancellationToken cancellationToken) =>
_timer.WaitForNextTickAsync(cancellationToken);
public void Dispose() => _timer.Dispose();
}

View file

@ -0,0 +1,11 @@
namespace TeamsISO.Engine.Pipeline;
/// <summary>
/// A frame after framerate, resolution, and aspect normalization. Ready to send.
/// </summary>
public sealed record ProcessedFrame(
int Width,
int Height,
long TimestampTicks,
ReadOnlyMemory<byte> Pixels,
PixelFormat Format);

View file

@ -0,0 +1,19 @@
namespace TeamsISO.Engine.Pipeline;
/// <summary>
/// A frame as captured from an NDI receiver. Pixel buffer is opaque to the engine — its
/// shape is determined by the NDI receive format. Timestamp is the source's reported time.
/// </summary>
public sealed record RawFrame(
int Width,
int Height,
long TimestampTicks,
ReadOnlyMemory<byte> Pixels,
PixelFormat Format);
public enum PixelFormat
{
Bgra,
Uyvy,
Rgba
}