105 lines
3.5 KiB
C#
105 lines
3.5 KiB
C#
|
|
using TeamsISO.Engine.Domain;
|
||
|
|
|
||
|
|
namespace TeamsISO.Engine.Discovery;
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 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 <see cref="_renameWindow"/>,
|
||
|
|
/// the existing <see cref="Participant.Id"/> transfers to the new source.
|
||
|
|
/// </summary>
|
||
|
|
public sealed class ParticipantTracker
|
||
|
|
{
|
||
|
|
private readonly TimeSpan _renameWindow;
|
||
|
|
private readonly Func<DateTimeOffset> _now;
|
||
|
|
private readonly List<MutableParticipant> _participants = new();
|
||
|
|
private readonly List<RecentlyRemoved> _recentlyRemoved = new();
|
||
|
|
|
||
|
|
public ParticipantTracker(TimeSpan renameWindow, Func<DateTimeOffset> now)
|
||
|
|
{
|
||
|
|
_renameWindow = renameWindow;
|
||
|
|
_now = now;
|
||
|
|
}
|
||
|
|
|
||
|
|
public IReadOnlyList<Participant> 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);
|
||
|
|
}
|