feat(discovery): add NdiSource record and Teams source string parser
Some checks failed
CI / build-and-test (push) Failing after 22s

This commit is contained in:
Zac Gaetano 2026-05-07 15:11:00 +00:00
parent b07e3e78e0
commit aaf3184a8e
3 changed files with 126 additions and 0 deletions

View file

@ -0,0 +1,70 @@
using TeamsISO.Engine.Domain;
namespace TeamsISO.Engine.Discovery;
/// <summary>
/// Parses NDI source strings emitted by Microsoft Teams.
///
/// Examples Teams emits:
/// "MACHINE (Teams - Display Name)"
/// "MACHINE (Teams)" — auto-mixed active speaker
/// "MACHINE (Teams Audio)" — audio-only mix
/// "MACHINE (Teams Screen Share)" — screen share
/// </summary>
public static class NdiSourceParser
{
public static NdiSource? Parse(string fullName)
{
if (string.IsNullOrWhiteSpace(fullName))
return null;
// Find the last '(' so machine names can themselves contain parens.
var openParen = fullName.LastIndexOf('(');
if (openParen <= 0 || !fullName.EndsWith(')'))
{
// Try outer parens for cases like "MACHINE (Teams - Smith, Bob (PM))"
// by walking from the first '(' that opens a "Teams" inner.
var firstParen = fullName.IndexOf(" (Teams", StringComparison.Ordinal);
if (firstParen <= 0)
return null;
openParen = firstParen + 1;
if (!fullName.EndsWith(')'))
return null;
}
else
{
// Re-anchor to the first " (Teams" if present, so display names containing
// their own parens (e.g. "Smith, Bob (PM)") don't get truncated.
var firstParen = fullName.IndexOf(" (Teams", StringComparison.Ordinal);
if (firstParen > 0)
openParen = firstParen + 1;
}
var machine = fullName[..openParen].TrimEnd();
if (machine.Length == 0)
return null;
var inner = fullName.Substring(openParen + 1, fullName.Length - openParen - 2).Trim();
if (!inner.StartsWith("Teams", StringComparison.Ordinal))
return null;
if (inner == "Teams")
return new NdiSource(fullName, machine, NdiSourceKind.ActiveSpeaker, DisplayName: null);
if (inner == "Teams Audio")
return new NdiSource(fullName, machine, NdiSourceKind.Audio, DisplayName: null);
if (inner == "Teams Screen Share")
return new NdiSource(fullName, machine, NdiSourceKind.ScreenShare, DisplayName: null);
const string prefix = "Teams - ";
if (inner.StartsWith(prefix, StringComparison.Ordinal))
{
var display = inner[prefix.Length..].Trim();
if (display.Length == 0)
return null;
return new NdiSource(fullName, machine, NdiSourceKind.Participant, display);
}
return null;
}
}

View file

@ -0,0 +1,10 @@
namespace TeamsISO.Engine.Domain;
/// <summary>
/// Raw discovery record parsed from an NDI source string emitted by Microsoft Teams.
/// </summary>
public sealed record NdiSource(
string FullName,
string MachineName,
NdiSourceKind Kind,
string? DisplayName);

View file

@ -0,0 +1,46 @@
using TeamsISO.Engine.Discovery;
using TeamsISO.Engine.Domain;
namespace TeamsISO.Engine.Tests.Domain;
public class NdiSourceParserTests
{
[Theory]
[InlineData("WORKSTATION-01 (Teams - Jane Doe)", "WORKSTATION-01", NdiSourceKind.Participant, "Jane Doe")]
[InlineData("PROD-PC (Teams - Élise O'Connor)", "PROD-PC", NdiSourceKind.Participant, "Élise O'Connor")]
[InlineData("HOST (Teams - Smith, Bob (PM))", "HOST", NdiSourceKind.Participant, "Smith, Bob (PM)")]
public void Parse_Participant_ExtractsMachineAndDisplayName(
string fullName, string expectedMachine, NdiSourceKind expectedKind, string expectedDisplay)
{
var result = NdiSourceParser.Parse(fullName);
result.Should().NotBeNull();
result!.MachineName.Should().Be(expectedMachine);
result.Kind.Should().Be(expectedKind);
result.DisplayName.Should().Be(expectedDisplay);
result.FullName.Should().Be(fullName);
}
[Theory]
[InlineData("HOST (Teams)", NdiSourceKind.ActiveSpeaker)]
[InlineData("HOST (Teams Audio)", NdiSourceKind.Audio)]
[InlineData("HOST (Teams Screen Share)", NdiSourceKind.ScreenShare)]
public void Parse_NonParticipantKinds_ClassifyCorrectly(string fullName, NdiSourceKind expectedKind)
{
var result = NdiSourceParser.Parse(fullName);
result.Should().NotBeNull();
result!.Kind.Should().Be(expectedKind);
result.DisplayName.Should().BeNull();
}
[Theory]
[InlineData("Plain NDI Source")]
[InlineData("HOST (Some Other Software)")]
[InlineData("(Teams - No Machine)")]
public void Parse_NonTeamsSource_ReturnsNull(string fullName)
{
var result = NdiSourceParser.Parse(fullName);
result.Should().BeNull();
}
}