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);
}