dragon-iso/src/TeamsISO.App/ViewModels/GlobalSettingsViewModel.cs
Zac Gaetano 01ef4250d7
All checks were successful
CI / build-and-test (push) Successful in 31s
feat(ui): real Wild Dragon mark in rail + automated transcoder topology
Two related deliverables addressing the user's morning asks.

1. Branding: Dragon WHITE.png and Wild Dragon Logo WHITE.png from the brand kit are copied into src/TeamsISO.App/Assets/ and registered as <Resource> items in the .csproj. The rail's placeholder 'W' glyph is replaced by the real dragon mark (40x40, HighQuality bitmap scaling) with a 'Wild Dragon' caption underneath.

2. NDI Access Manager automation: NdiAccessManagerConfig service reads/writes %APPDATA%\\NDI\\ndi-config.v1.json, working in JsonNode trees so we don't clobber unrelated keys. ApplyTranscoderTopology() sets groups.send=[teamsiso-input] and groups.recv=[public, teamsiso-input] so all local senders (Teams + anything else) broadcast on the private group while local receivers can still see public sources too. Engine-side, the user's per-pipeline OutputGroups override pushes TeamsISO outputs back onto Public so downstream switchers see clean ISOs.

Atomic write: temp + replace, with timestamped backup of the prior config. ReadCurrentGroups() can be used by future UI to show what's currently configured. RestoreDefaults() reverts.

Settings panel grows an 'Apply transcoder topology' button under the NDI Network section. Click writes the system config, sets the engine's discovery=teamsiso-input / output=public, refreshes the bound text boxes, and pops a dialog with a 'restart Teams' reminder + the backup path.
2026-05-08 07:19:31 -04:00

125 lines
5.8 KiB
C#

using System.Windows;
using TeamsISO.App.Services;
using TeamsISO.Engine.Controller;
using TeamsISO.Engine.Domain;
namespace TeamsISO.App.ViewModels;
/// <summary>
/// Bindings for the global settings panel: framerate, resolution, aspect, audio,
/// NDI groups (discovery + output), and the participant-list hide-Local toggle.
/// </summary>
public sealed class GlobalSettingsViewModel : ObservableObject
{
private readonly IIsoController _controller;
private TargetFramerate _framerate;
private TargetResolution _resolution;
private AspectMode _aspect;
private AudioMode _audio;
private string _discoveryGroups;
private string _outputGroups;
private bool _hideLocalSelf = true;
public GlobalSettingsViewModel(IIsoController controller)
{
_controller = controller;
var current = controller.GlobalSettings;
_framerate = current.Framerate;
_resolution = current.Resolution;
_aspect = current.Aspect;
_audio = current.Audio;
var groups = controller.GroupSettings;
_discoveryGroups = groups.DiscoveryGroups ?? string.Empty;
_outputGroups = groups.OutputGroups ?? string.Empty;
ApplyCommand = new AsyncRelayCommand(ApplyAsync);
ApplyTranscoderTopologyCommand = new AsyncRelayCommand(ApplyTranscoderTopologyAsync);
}
public IEnumerable<TargetFramerate> AvailableFramerates => Enum.GetValues<TargetFramerate>();
public IEnumerable<TargetResolution> AvailableResolutions => Enum.GetValues<TargetResolution>();
public IEnumerable<AspectMode> AvailableAspectModes => Enum.GetValues<AspectMode>();
public IEnumerable<AudioMode> AvailableAudioModes => Enum.GetValues<AudioMode>();
public TargetFramerate Framerate { get => _framerate; set => SetField(ref _framerate, value); }
public TargetResolution Resolution { get => _resolution; set => SetField(ref _resolution, value); }
public AspectMode Aspect { get => _aspect; set => SetField(ref _aspect, value); }
public AudioMode Audio { get => _audio; set => SetField(ref _audio, value); }
/// <summary>NDI discovery group(s) — comma-separated. Empty = default (Public).</summary>
public string DiscoveryGroups { get => _discoveryGroups; set => SetField(ref _discoveryGroups, value); }
/// <summary>NDI output group(s) — comma-separated. Empty = default (Public).</summary>
public string OutputGroups { get => _outputGroups; set => SetField(ref _outputGroups, value); }
/// <summary>
/// Hide the user's own self-preview ("(Local)") from the participants list.
/// On by default — operators rarely want to ISO-route their own preview.
/// Read by <see cref="MainViewModel"/> when filtering the list it presents.
/// </summary>
public bool HideLocalSelf { get => _hideLocalSelf; set => SetField(ref _hideLocalSelf, value); }
public AsyncRelayCommand ApplyCommand { get; }
/// <summary>
/// One-click "set up the transcoder topology" — writes ndi-config.v1.json so all
/// local senders broadcast on a private group ("teamsiso-input") while local
/// receivers can see both that and "public", then sets the engine's discovery and
/// output groups to align (engine receives from the private group, emits on Public).
/// User has to restart Teams for the new ndi-config.v1.json to take effect there.
/// </summary>
public AsyncRelayCommand ApplyTranscoderTopologyCommand { get; }
private async Task ApplyAsync()
{
var settings = new FrameProcessingSettings(_framerate, _resolution, _aspect, _audio);
await _controller.SetGlobalSettingsAsync(settings, CancellationToken.None);
var groups = new NdiGroupSettings(
DiscoveryGroups: string.IsNullOrWhiteSpace(_discoveryGroups) ? null : _discoveryGroups.Trim(),
OutputGroups: string.IsNullOrWhiteSpace(_outputGroups) ? null : _outputGroups.Trim());
await _controller.SetGroupSettingsAsync(groups, CancellationToken.None);
}
private async Task ApplyTranscoderTopologyAsync()
{
// 1. Update the machine-wide NDI config so Teams' raw broadcasts go to the
// private group instead of polluting Public.
var result = NdiAccessManagerConfig.ApplyTranscoderTopology();
if (!result.Success)
{
MessageBox.Show(
$"Could not write NDI Access Manager config.\n\n{result.ErrorMessage}\n\nPath: {result.ConfigPath}",
"TeamsISO — Apply transcoder topology",
MessageBoxButton.OK,
MessageBoxImage.Warning);
return;
}
// 2. Update the engine: receive only from the private group, emit on Public.
var ourGroups = new NdiGroupSettings(
DiscoveryGroups: NdiAccessManagerConfig.TranscoderInputGroup,
OutputGroups: "public");
await _controller.SetGroupSettingsAsync(ourGroups, CancellationToken.None);
// 3. Reflect the new values in the bound text boxes.
DiscoveryGroups = NdiAccessManagerConfig.TranscoderInputGroup;
OutputGroups = "public";
var backupNote = result.BackupPath is null
? "No prior NDI config existed; a fresh one was created."
: $"A backup of your prior NDI config was saved to:\n{result.BackupPath}";
MessageBox.Show(
"Transcoder topology applied. ✓\n\n" +
"• Local senders (Teams, etc.) will broadcast on group 'teamsiso-input'.\n" +
"• Local receivers will see both 'public' and 'teamsiso-input'.\n" +
"• TeamsISO will discover from 'teamsiso-input' and re-emit on 'public'.\n\n" +
"RESTART Microsoft Teams for the new NDI config to take effect there.\n\n" +
backupNote,
"TeamsISO — Apply transcoder topology",
MessageBoxButton.OK,
MessageBoxImage.Information);
}
}