From 179a44adf5ff41c6a8a1b296a21e5bd166bb764e Mon Sep 17 00:00:00 2001 From: Zac Gaetano Date: Sun, 10 May 2026 09:41:31 -0400 Subject: [PATCH] feat: custom NDI output name template + enriched status bar --- .../Services/OutputNameTemplate.cs | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 src/TeamsISO.App/Services/OutputNameTemplate.cs diff --git a/src/TeamsISO.App/Services/OutputNameTemplate.cs b/src/TeamsISO.App/Services/OutputNameTemplate.cs new file mode 100644 index 0000000..491ea6d --- /dev/null +++ b/src/TeamsISO.App/Services/OutputNameTemplate.cs @@ -0,0 +1,107 @@ +using System.IO; +using System.Text; + +namespace TeamsISO.App.Services; + +/// +/// User-editable template for the NDI source name a participant's ISO is +/// published as. Default "TEAMSISO_{guid}" matches the original +/// hard-coded DefaultOutputName in IsoController; operators +/// can switch to "TEAMSISO_{name}" for human-readable output names +/// (recommended for downstream switchers that key on name patterns), or +/// "TEAMSISO_{machine}_{name}" when multiple TeamsISO machines feed +/// the same NDI network. +/// +/// Tokens expanded in : +/// {name} participant display name, sanitized (alphanumeric + underscore) +/// {guid} first 8 hex chars of the participant's Id, uppercase +/// {machine} sanitized PC hostname (Environment.MachineName) +/// {timestamp} current local time as yyyyMMdd_HHmmss +/// +/// Persisted to %LOCALAPPDATA%\TeamsISO\output-name-template.txt. +/// +public static class OutputNameTemplate +{ + public const string DefaultTemplate = "TEAMSISO_{guid}"; + + private static string TemplatePath => + Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "TeamsISO", "output-name-template.txt"); + + /// + /// Get the operator's current template, or the shipped default when no + /// override has been saved (or the override file is missing/unreadable). + /// + public static string Get() + { + try + { + if (File.Exists(TemplatePath)) + { + var raw = File.ReadAllText(TemplatePath).Trim(); + if (!string.IsNullOrEmpty(raw)) return raw; + } + } + catch + { + // Disk read failure → fall through to default. The next Set() call + // will overwrite cleanly. + } + return DefaultTemplate; + } + + public static void Set(string template) + { + try + { + var dir = Path.GetDirectoryName(TemplatePath); + if (!string.IsNullOrEmpty(dir)) Directory.CreateDirectory(dir); + File.WriteAllText(TemplatePath, template ?? string.Empty); + } + catch + { + // Best-effort persistence; the in-memory value still sticks for + // this session. + } + } + + /// + /// Expand tokens in for a specific participant. + /// Result is sanitized into NDI-safe characters: alphanumeric, underscore, + /// hyphen, period. NDI spec allows more, but a conservative set keeps + /// downstream switchers happy. + /// + public static string Render(string template, Guid participantId, string displayName) + { + var safeName = SanitizeForNdi(displayName); + var guid = participantId.ToString("N")[..8].ToUpperInvariant(); + var machine = SanitizeForNdi(Environment.MachineName); + var timestamp = DateTimeOffset.Now.ToString("yyyyMMdd_HHmmss"); + + var result = template + .Replace("{name}", safeName) + .Replace("{guid}", guid) + .Replace("{machine}", machine) + .Replace("{timestamp}", timestamp); + + // Final sanitize on the rendered result — protects against a template + // that includes literal characters NDI doesn't accept. + return SanitizeForNdi(result); + } + + private static string SanitizeForNdi(string s) + { + if (string.IsNullOrEmpty(s)) return string.Empty; + var sb = new StringBuilder(s.Length); + foreach (var c in s) + { + if (char.IsLetterOrDigit(c) || c is '_' or '-' or '.') + sb.Append(c); + else if (char.IsWhiteSpace(c)) + sb.Append('_'); + // else: skip + } + return sb.ToString(); + } +}