using TeamsISO.Engine.Domain; namespace TeamsISO.Engine.Discovery; /// /// Maintains the operator-facing participant list, applying the rename heuristic /// from the v1 spec: if a participant source disappears and another participant /// source with the same MachineName appears within , /// the existing transfers to the new source. /// public sealed class ParticipantTracker { private readonly TimeSpan _renameWindow; private readonly Func _now; private readonly List _participants = new(); private readonly List _recentlyRemoved = new(); public ParticipantTracker(TimeSpan renameWindow, Func now) { _renameWindow = renameWindow; _now = now; } public IReadOnlyList Participants => _participants.Select(m => m.ToRecord()).ToList(); public void Apply(DiscoveryEvent ev) { switch (ev) { case DiscoveryEvent.Added a when a.Source.Kind == NdiSourceKind.Participant: HandleAdded(a.Source); break; case DiscoveryEvent.Removed r when r.Source.Kind == NdiSourceKind.Participant: HandleRemoved(r.Source); break; } } private void HandleAdded(NdiSource source) { var now = _now(); PruneRecentlyRemoved(now); var match = _recentlyRemoved.FirstOrDefault(rr => rr.MachineName == source.MachineName); if (match is not null) { var existing = _participants.First(p => p.Id == match.Id); existing.DisplayName = source.DisplayName!; existing.CurrentSource = source; existing.LastSeen = now; _recentlyRemoved.Remove(match); return; } _participants.Add(new MutableParticipant( Id: Guid.NewGuid(), DisplayName: source.DisplayName!, CurrentSource: source, FirstSeen: now, LastSeen: now)); } private void HandleRemoved(NdiSource source) { var now = _now(); var existing = _participants.FirstOrDefault(p => p.CurrentSource is not null && p.CurrentSource.FullName == source.FullName); if (existing is null) return; existing.CurrentSource = null; _recentlyRemoved.Add(new RecentlyRemoved(existing.Id, source.MachineName, now)); } private void PruneRecentlyRemoved(DateTimeOffset now) { _recentlyRemoved.RemoveAll(rr => now - rr.RemovedAt > _renameWindow); } private sealed class MutableParticipant { public Guid Id { get; init; } public string DisplayName { get; set; } public NdiSource? CurrentSource { get; set; } public DateTimeOffset FirstSeen { get; init; } public DateTimeOffset LastSeen { get; set; } public MutableParticipant(Guid Id, string DisplayName, NdiSource? CurrentSource, DateTimeOffset FirstSeen, DateTimeOffset LastSeen) { this.Id = Id; this.DisplayName = DisplayName; this.CurrentSource = CurrentSource; this.FirstSeen = FirstSeen; this.LastSeen = LastSeen; } public Participant ToRecord() => new(Id, DisplayName, CurrentSource, FirstSeen, LastSeen); } private sealed record RecentlyRemoved(Guid Id, string MachineName, DateTimeOffset RemovedAt); }