diff --git a/src/TeamsISO.Engine/Discovery/DiscoveryEvent.cs b/src/TeamsISO.Engine/Discovery/DiscoveryEvent.cs new file mode 100644 index 0000000..eb3fd2b --- /dev/null +++ b/src/TeamsISO.Engine/Discovery/DiscoveryEvent.cs @@ -0,0 +1,9 @@ +using TeamsISO.Engine.Domain; + +namespace TeamsISO.Engine.Discovery; + +public abstract record DiscoveryEvent +{ + public sealed record Added(NdiSource Source) : DiscoveryEvent; + public sealed record Removed(NdiSource Source) : DiscoveryEvent; +} diff --git a/src/tests/TeamsISO.Engine.Tests/Fakes/FakeFrameClock.cs b/src/tests/TeamsISO.Engine.Tests/Fakes/FakeFrameClock.cs new file mode 100644 index 0000000..e28c94c --- /dev/null +++ b/src/tests/TeamsISO.Engine.Tests/Fakes/FakeFrameClock.cs @@ -0,0 +1,39 @@ +using TeamsISO.Engine.Pipeline; + +namespace TeamsISO.Engine.Tests.Fakes; + +/// +/// Manual-tick clock. Tests advance the clock and trigger the awaiter explicitly. +/// +public sealed class FakeFrameClock : IFrameClock +{ + private long _nowTicks; + private TaskCompletionSource? _pendingTick; + private readonly object _gate = new(); + + public long NowTicks => Interlocked.Read(ref _nowTicks); + + public void Advance(TimeSpan by) + { + Interlocked.Add(ref _nowTicks, by.Ticks); + TaskCompletionSource? toSignal; + lock (_gate) + { + toSignal = _pendingTick; + _pendingTick = null; + } + toSignal?.TrySetResult(true); + } + + public ValueTask WaitForNextTickAsync(CancellationToken cancellationToken) + { + TaskCompletionSource tcs; + lock (_gate) + { + _pendingTick ??= new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + tcs = _pendingTick; + } + cancellationToken.Register(() => tcs.TrySetResult(false)); + return new ValueTask(tcs.Task); + } +} diff --git a/src/tests/TeamsISO.Engine.Tests/Fakes/FakeNdiInterop.cs b/src/tests/TeamsISO.Engine.Tests/Fakes/FakeNdiInterop.cs new file mode 100644 index 0000000..35ae09c --- /dev/null +++ b/src/tests/TeamsISO.Engine.Tests/Fakes/FakeNdiInterop.cs @@ -0,0 +1,71 @@ +using System.Collections.Concurrent; +using TeamsISO.Engine.Interop; +using TeamsISO.Engine.Pipeline; + +namespace TeamsISO.Engine.Tests.Fakes; + +/// +/// In-memory test double for . Tests configure source lists and frame +/// queues; the fake feeds those into engine code as if a real NDI runtime were present. +/// +public sealed class FakeNdiInterop : INdiInterop +{ + public List Sources { get; } = new(); + public ConcurrentDictionary> ReceiverFrames { get; } = new(); + public ConcurrentDictionary> SentFrames { get; } = new(); + public string RuntimeVersion { get; set; } = "6.0.0"; + public Dictionary ReceiverCreatedCount { get; } = new(); + public Dictionary SenderCreatedCount { get; } = new(); + + public NdiFindHandle CreateFinder() => new FakeFindHandle(); + public IReadOnlyList GetCurrentSources(NdiFindHandle finder) => Sources.ToArray(); + + public NdiReceiverHandle CreateReceiver(string sourceFullName) + { + ReceiverCreatedCount[sourceFullName] = ReceiverCreatedCount.GetValueOrDefault(sourceFullName) + 1; + ReceiverFrames.GetOrAdd(sourceFullName, _ => new ConcurrentQueue()); + return new FakeReceiverHandle(sourceFullName); + } + + public RawFrame? CaptureFrame(NdiReceiverHandle receiver, int timeoutMs) + { + var key = ((FakeReceiverHandle)receiver).Source; + if (ReceiverFrames.TryGetValue(key, out var q) && q.TryDequeue(out var frame)) + return frame; + return null; // simulate timeout + } + + public NdiSenderHandle CreateSender(string outputName) + { + SenderCreatedCount[outputName] = SenderCreatedCount.GetValueOrDefault(outputName) + 1; + SentFrames.GetOrAdd(outputName, _ => new List()); + return new FakeSenderHandle(outputName); + } + + public void SendFrame(NdiSenderHandle sender, ProcessedFrame frame) + { + var key = ((FakeSenderHandle)sender).Output; + SentFrames[key].Add(frame); + } + + public string GetRuntimeVersion() => RuntimeVersion; + + private sealed class FakeFindHandle : NdiFindHandle + { + public override void Dispose() { } + } + + private sealed class FakeReceiverHandle : NdiReceiverHandle + { + public string Source { get; } + public FakeReceiverHandle(string source) => Source = source; + public override void Dispose() { } + } + + private sealed class FakeSenderHandle : NdiSenderHandle + { + public string Output { get; } + public FakeSenderHandle(string output) => Output = output; + public override void Dispose() { } + } +}