2026-05-10 09:41:28 -04:00
|
|
|
using System.IO;
|
2026-05-08 07:19:31 -04:00
|
|
|
using System.Windows;
|
|
|
|
|
using TeamsISO.App.Services;
|
2026-05-07 11:39:46 -04:00
|
|
|
using TeamsISO.Engine.Controller;
|
|
|
|
|
using TeamsISO.Engine.Domain;
|
|
|
|
|
|
|
|
|
|
namespace TeamsISO.App.ViewModels;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
feat(ui): rebuild MainWindow with Stone-theme design system
Adopts the design language from Dammyjay93/interface-design: warm Stone neutrals, accent orange (#EA580C), borders-only depth (no shadows), 8px spacing grid, all-caps section labels, mono typography for machine names and timecodes.
Themes/StoneTheme.xaml is the single source of truth for design tokens (color brushes, typography styles, spacing) plus restyled control templates (Button, TextBox, ComboBox, ComboBoxItem, CheckBox, DataGrid + DataGridColumnHeader / DataGridRow / DataGridCell, ScrollBar). MainWindow consumes the tokens and is laid out with a header (title + status pill), section-headed Settings sidebar (Output Format / NDI Network / Display), card-wrapped participant list, and a mono status footer.
Settings sidebar surfaces the new NDI group configuration (discovery + output) and a Hide-(Local) checkbox. The latter filters the user's own self-preview from the participants list at the MainViewModel layer (HideLocalSelf=true by default) so operators don't accidentally route their own preview as an ISO. Apply Changes round-trips both FrameProcessingSettings and NdiGroupSettings through the controller in one go.
2026-05-07 23:58:02 -04:00
|
|
|
/// Bindings for the global settings panel: framerate, resolution, aspect, audio,
|
|
|
|
|
/// NDI groups (discovery + output), and the participant-list hide-Local toggle.
|
2026-05-07 11:39:46 -04:00
|
|
|
/// </summary>
|
|
|
|
|
public sealed class GlobalSettingsViewModel : ObservableObject
|
|
|
|
|
{
|
|
|
|
|
private readonly IIsoController _controller;
|
feat(ui): toast feedback for settings actions; refresh _NEXT.md
Adds a small auto-dismissing pill notification at the bottom-center of the participants area: 'Settings saved' on Apply Changes, 'Transcoder topology applied — restart Teams to take effect' after the one-click NDI groups setup. ToastViewModel owns its own DispatcherTimer and resets the dismissal countdown on successive calls, so the most recent message is always the one visible. Hooked into MainViewModel and threaded into GlobalSettingsViewModel via constructor injection.
_NEXT.md rewritten to reflect the May 2026 hardening pass: separates engine / UI / networking / Phase E.1 / diagnostics / CI / tests sections, lists every shipped item, and re-prioritizes the remaining work (Phase E.2-E.3 embedded Teams, code-signing the MSI, refresh-discovery affordance, output thumbnail previews, settings panel UX, auto-disable on departure, operator presets).
2026-05-09 09:30:04 -04:00
|
|
|
private readonly ToastViewModel? _toast;
|
2026-05-07 11:39:46 -04:00
|
|
|
private TargetFramerate _framerate;
|
|
|
|
|
private TargetResolution _resolution;
|
|
|
|
|
private AspectMode _aspect;
|
|
|
|
|
private AudioMode _audio;
|
feat(ui): rebuild MainWindow with Stone-theme design system
Adopts the design language from Dammyjay93/interface-design: warm Stone neutrals, accent orange (#EA580C), borders-only depth (no shadows), 8px spacing grid, all-caps section labels, mono typography for machine names and timecodes.
Themes/StoneTheme.xaml is the single source of truth for design tokens (color brushes, typography styles, spacing) plus restyled control templates (Button, TextBox, ComboBox, ComboBoxItem, CheckBox, DataGrid + DataGridColumnHeader / DataGridRow / DataGridCell, ScrollBar). MainWindow consumes the tokens and is laid out with a header (title + status pill), section-headed Settings sidebar (Output Format / NDI Network / Display), card-wrapped participant list, and a mono status footer.
Settings sidebar surfaces the new NDI group configuration (discovery + output) and a Hide-(Local) checkbox. The latter filters the user's own self-preview from the participants list at the MainViewModel layer (HideLocalSelf=true by default) so operators don't accidentally route their own preview as an ISO. Apply Changes round-trips both FrameProcessingSettings and NdiGroupSettings through the controller in one go.
2026-05-07 23:58:02 -04:00
|
|
|
private string _discoveryGroups;
|
|
|
|
|
private string _outputGroups;
|
|
|
|
|
private bool _hideLocalSelf = true;
|
2026-05-10 09:41:28 -04:00
|
|
|
private bool _autoDisableOnDeparture = false;
|
|
|
|
|
private bool _autoApplyLastPreset;
|
|
|
|
|
private bool _recordIsosToDisk;
|
|
|
|
|
private string _recordingDirectory = string.Empty;
|
|
|
|
|
private bool _controlSurfaceEnabled;
|
|
|
|
|
private int _controlSurfacePort = ControlSurfaceServer.DefaultPort;
|
|
|
|
|
private bool _oscBridgeEnabled;
|
|
|
|
|
private int _oscBridgePort = OscBridge.DefaultPort;
|
|
|
|
|
private bool _updateCheckOnLaunch = UpdateChecker.LaunchCheckEnabled;
|
|
|
|
|
private string _outputNameTemplate = TeamsISO.App.Services.OutputNameTemplate.Get();
|
|
|
|
|
private UIPreferences.SortMode _participantSort = UIPreferences.SortMode.JoinOrder;
|
|
|
|
|
private bool _minimizeToTray;
|
2026-05-10 10:01:32 -04:00
|
|
|
private bool _controlSurfaceLanReachable;
|
2026-05-07 11:39:46 -04:00
|
|
|
|
feat(ui): toast feedback for settings actions; refresh _NEXT.md
Adds a small auto-dismissing pill notification at the bottom-center of the participants area: 'Settings saved' on Apply Changes, 'Transcoder topology applied — restart Teams to take effect' after the one-click NDI groups setup. ToastViewModel owns its own DispatcherTimer and resets the dismissal countdown on successive calls, so the most recent message is always the one visible. Hooked into MainViewModel and threaded into GlobalSettingsViewModel via constructor injection.
_NEXT.md rewritten to reflect the May 2026 hardening pass: separates engine / UI / networking / Phase E.1 / diagnostics / CI / tests sections, lists every shipped item, and re-prioritizes the remaining work (Phase E.2-E.3 embedded Teams, code-signing the MSI, refresh-discovery affordance, output thumbnail previews, settings panel UX, auto-disable on departure, operator presets).
2026-05-09 09:30:04 -04:00
|
|
|
public GlobalSettingsViewModel(IIsoController controller, ToastViewModel? toast = null)
|
2026-05-07 11:39:46 -04:00
|
|
|
{
|
|
|
|
|
_controller = controller;
|
feat(ui): toast feedback for settings actions; refresh _NEXT.md
Adds a small auto-dismissing pill notification at the bottom-center of the participants area: 'Settings saved' on Apply Changes, 'Transcoder topology applied — restart Teams to take effect' after the one-click NDI groups setup. ToastViewModel owns its own DispatcherTimer and resets the dismissal countdown on successive calls, so the most recent message is always the one visible. Hooked into MainViewModel and threaded into GlobalSettingsViewModel via constructor injection.
_NEXT.md rewritten to reflect the May 2026 hardening pass: separates engine / UI / networking / Phase E.1 / diagnostics / CI / tests sections, lists every shipped item, and re-prioritizes the remaining work (Phase E.2-E.3 embedded Teams, code-signing the MSI, refresh-discovery affordance, output thumbnail previews, settings panel UX, auto-disable on departure, operator presets).
2026-05-09 09:30:04 -04:00
|
|
|
_toast = toast;
|
2026-05-07 11:39:46 -04:00
|
|
|
var current = controller.GlobalSettings;
|
|
|
|
|
_framerate = current.Framerate;
|
|
|
|
|
_resolution = current.Resolution;
|
|
|
|
|
_aspect = current.Aspect;
|
|
|
|
|
_audio = current.Audio;
|
feat(ui): rebuild MainWindow with Stone-theme design system
Adopts the design language from Dammyjay93/interface-design: warm Stone neutrals, accent orange (#EA580C), borders-only depth (no shadows), 8px spacing grid, all-caps section labels, mono typography for machine names and timecodes.
Themes/StoneTheme.xaml is the single source of truth for design tokens (color brushes, typography styles, spacing) plus restyled control templates (Button, TextBox, ComboBox, ComboBoxItem, CheckBox, DataGrid + DataGridColumnHeader / DataGridRow / DataGridCell, ScrollBar). MainWindow consumes the tokens and is laid out with a header (title + status pill), section-headed Settings sidebar (Output Format / NDI Network / Display), card-wrapped participant list, and a mono status footer.
Settings sidebar surfaces the new NDI group configuration (discovery + output) and a Hide-(Local) checkbox. The latter filters the user's own self-preview from the participants list at the MainViewModel layer (HideLocalSelf=true by default) so operators don't accidentally route their own preview as an ISO. Apply Changes round-trips both FrameProcessingSettings and NdiGroupSettings through the controller in one go.
2026-05-07 23:58:02 -04:00
|
|
|
|
|
|
|
|
var groups = controller.GroupSettings;
|
|
|
|
|
_discoveryGroups = groups.DiscoveryGroups ?? string.Empty;
|
|
|
|
|
_outputGroups = groups.OutputGroups ?? string.Empty;
|
|
|
|
|
|
2026-05-10 09:41:28 -04:00
|
|
|
// Restore persisted UI toggles so the operator's preference survives
|
|
|
|
|
// process restarts. UIPreferences keeps a tiny JSON file under
|
|
|
|
|
// %LOCALAPPDATA%\TeamsISO\ui-prefs.json — defaults match the original
|
|
|
|
|
// in-memory init (HideLocalSelf=true, AutoDisableOnDeparture=false).
|
|
|
|
|
var uiPrefs = UIPreferences.Load();
|
|
|
|
|
_hideLocalSelf = uiPrefs.HideLocalSelf;
|
|
|
|
|
_autoDisableOnDeparture = uiPrefs.AutoDisableOnDeparture;
|
|
|
|
|
_participantSort = uiPrefs.ParticipantSort;
|
|
|
|
|
_minimizeToTray = uiPrefs.MinimizeToTray;
|
2026-05-10 10:01:32 -04:00
|
|
|
_controlSurfaceLanReachable = uiPrefs.ControlSurfaceLanReachable;
|
2026-05-10 09:41:28 -04:00
|
|
|
|
|
|
|
|
// Bring the auto-apply flag in from the presets store so the checkbox
|
|
|
|
|
// reflects the user's prior choice when the settings panel opens.
|
|
|
|
|
try { _autoApplyLastPreset = OperatorPresetStore.GetStartupPreference().AutoApplyOnStartup; }
|
|
|
|
|
catch { /* best-effort — disk read failures shouldn't block UI startup */ }
|
|
|
|
|
|
|
|
|
|
// Default recording directory: %USERPROFILE%\Videos\TeamsISO\<today's date>.
|
|
|
|
|
// Operator can override via the textbox. Date in the path keeps recordings
|
|
|
|
|
// from a long-running show day organized without us having to scan + rotate.
|
|
|
|
|
_recordingDirectory = Path.Combine(
|
|
|
|
|
Environment.GetFolderPath(Environment.SpecialFolder.MyVideos),
|
|
|
|
|
"TeamsISO",
|
|
|
|
|
DateTimeOffset.Now.ToString("yyyy-MM-dd"));
|
|
|
|
|
_recordIsosToDisk = controller.RecordingEnabled;
|
|
|
|
|
if (!string.IsNullOrEmpty(controller.RecordingDirectory))
|
|
|
|
|
_recordingDirectory = controller.RecordingDirectory;
|
|
|
|
|
|
2026-05-07 11:39:46 -04:00
|
|
|
ApplyCommand = new AsyncRelayCommand(ApplyAsync);
|
2026-05-08 07:19:31 -04:00
|
|
|
ApplyTranscoderTopologyCommand = new AsyncRelayCommand(ApplyTranscoderTopologyAsync);
|
2026-05-10 09:41:28 -04:00
|
|
|
ResetOutputDefaultsCommand = new RelayCommand(ResetOutputDefaults);
|
2026-05-10 10:01:32 -04:00
|
|
|
CopyControlSurfaceUrlCommand = new RelayCommand(() =>
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
System.Windows.Clipboard.SetText(ControlSurfaceUrl);
|
|
|
|
|
_toast?.Show($"Copied: {ControlSurfaceUrl}");
|
|
|
|
|
}
|
|
|
|
|
catch
|
|
|
|
|
{
|
|
|
|
|
// Clipboard occasionally errors when something else has it locked.
|
|
|
|
|
}
|
|
|
|
|
});
|
2026-05-10 09:41:28 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void ResetOutputDefaults()
|
|
|
|
|
{
|
|
|
|
|
var confirm = MessageBox.Show(
|
|
|
|
|
"Reset framerate, resolution, aspect and audio to TeamsISO defaults?\n\n" +
|
|
|
|
|
"This won't touch your NDI group configuration or display toggles.",
|
|
|
|
|
"TeamsISO — Reset output defaults",
|
|
|
|
|
MessageBoxButton.YesNo,
|
|
|
|
|
MessageBoxImage.Question,
|
|
|
|
|
MessageBoxResult.No);
|
|
|
|
|
if (confirm != MessageBoxResult.Yes) return;
|
|
|
|
|
|
|
|
|
|
var defaults = FrameProcessingSettings.Default;
|
|
|
|
|
Framerate = defaults.Framerate;
|
|
|
|
|
Resolution = defaults.Resolution;
|
|
|
|
|
Aspect = defaults.Aspect;
|
|
|
|
|
Audio = defaults.Audio;
|
|
|
|
|
_toast?.Show("Output settings reset to defaults — click Apply Changes to commit");
|
2026-05-07 11:39:46 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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); }
|
|
|
|
|
|
feat(ui): rebuild MainWindow with Stone-theme design system
Adopts the design language from Dammyjay93/interface-design: warm Stone neutrals, accent orange (#EA580C), borders-only depth (no shadows), 8px spacing grid, all-caps section labels, mono typography for machine names and timecodes.
Themes/StoneTheme.xaml is the single source of truth for design tokens (color brushes, typography styles, spacing) plus restyled control templates (Button, TextBox, ComboBox, ComboBoxItem, CheckBox, DataGrid + DataGridColumnHeader / DataGridRow / DataGridCell, ScrollBar). MainWindow consumes the tokens and is laid out with a header (title + status pill), section-headed Settings sidebar (Output Format / NDI Network / Display), card-wrapped participant list, and a mono status footer.
Settings sidebar surfaces the new NDI group configuration (discovery + output) and a Hide-(Local) checkbox. The latter filters the user's own self-preview from the participants list at the MainViewModel layer (HideLocalSelf=true by default) so operators don't accidentally route their own preview as an ISO. Apply Changes round-trips both FrameProcessingSettings and NdiGroupSettings through the controller in one go.
2026-05-07 23:58:02 -04:00
|
|
|
/// <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.
|
2026-05-10 09:41:28 -04:00
|
|
|
/// Persisted to <c>ui-prefs.json</c> via <see cref="UIPreferences"/>.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public bool HideLocalSelf
|
|
|
|
|
{
|
|
|
|
|
get => _hideLocalSelf;
|
|
|
|
|
set
|
|
|
|
|
{
|
|
|
|
|
if (SetField(ref _hideLocalSelf, value)) PersistUiPrefs();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// When a participant leaves the meeting (their NDI source disappears),
|
|
|
|
|
/// automatically tear down their ISO pipeline. Off by default so transient
|
|
|
|
|
/// drops don't lose the operator's routing — but useful for clean
|
|
|
|
|
/// show-end behavior. Read by MainViewModel when reconciling departures.
|
|
|
|
|
/// Persisted to <c>ui-prefs.json</c>.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public bool AutoDisableOnDeparture
|
|
|
|
|
{
|
|
|
|
|
get => _autoDisableOnDeparture;
|
|
|
|
|
set
|
|
|
|
|
{
|
|
|
|
|
if (SetField(ref _autoDisableOnDeparture, value)) PersistUiPrefs();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Available sort modes for the dropdown in DISPLAY settings.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public IEnumerable<UIPreferences.SortMode> AvailableSortModes => Enum.GetValues<UIPreferences.SortMode>();
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// How the participants DataGrid is sorted. Persisted across launches via
|
|
|
|
|
/// <see cref="UIPreferences"/>. Reaches into <see cref="MainViewModel.SetSortMode"/>
|
|
|
|
|
/// on Application.Current to actually apply the sort to the live view —
|
|
|
|
|
/// the settings VM doesn't directly know about the main VM but App holds
|
|
|
|
|
/// both and exposes the main window via its DataContext.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public UIPreferences.SortMode ParticipantSort
|
|
|
|
|
{
|
|
|
|
|
get => _participantSort;
|
|
|
|
|
set
|
|
|
|
|
{
|
|
|
|
|
if (!SetField(ref _participantSort, value)) return;
|
|
|
|
|
PersistUiPrefs();
|
|
|
|
|
// Apply to the live view immediately. App.MainWindow.DataContext
|
|
|
|
|
// is the MainViewModel; cast and call.
|
|
|
|
|
var main = (Application.Current?.MainWindow?.DataContext) as MainViewModel;
|
|
|
|
|
main?.SetSortMode(value);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Minimize-to-tray behavior. When on, minimizing the main window hides
|
|
|
|
|
/// it from the taskbar and shows a tray icon (double-click to restore).
|
|
|
|
|
/// Right-click menu on the tray icon offers "Show", "Stop all ISOs", "Exit".
|
|
|
|
|
/// Useful for long unattended shows where the operator wants TeamsISO
|
|
|
|
|
/// running but invisible.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public bool MinimizeToTray
|
|
|
|
|
{
|
|
|
|
|
get => _minimizeToTray;
|
|
|
|
|
set
|
|
|
|
|
{
|
|
|
|
|
if (!SetField(ref _minimizeToTray, value)) return;
|
|
|
|
|
PersistUiPrefs();
|
|
|
|
|
// Reach into the App-owned tray host. App constructs it after the
|
|
|
|
|
// main window exists, so the cast is safe at any time the settings
|
|
|
|
|
// panel is interactable.
|
|
|
|
|
var tray = (Application.Current as App)?.TrayIcon;
|
|
|
|
|
if (tray is not null) tray.Enabled = value;
|
|
|
|
|
_toast?.Show(value
|
|
|
|
|
? "Minimize-to-tray enabled — minimizing now hides the window"
|
|
|
|
|
: "Minimize-to-tray disabled");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Snapshot the current persistable UI state to disk. Called from any
|
|
|
|
|
/// <see cref="UIPreferences.Prefs"/>-backed setter. Best-effort — disk
|
|
|
|
|
/// failures don't surface to the operator (the in-memory state still
|
|
|
|
|
/// reflects their click for this session).
|
|
|
|
|
/// </summary>
|
|
|
|
|
private void PersistUiPrefs() =>
|
|
|
|
|
UIPreferences.Save(new UIPreferences.Prefs(
|
|
|
|
|
HideLocalSelf: _hideLocalSelf,
|
|
|
|
|
AutoDisableOnDeparture: _autoDisableOnDeparture,
|
|
|
|
|
ParticipantSort: _participantSort,
|
2026-05-10 10:01:32 -04:00
|
|
|
MinimizeToTray: _minimizeToTray,
|
|
|
|
|
ControlSurfaceLanReachable: _controlSurfaceLanReachable));
|
2026-05-10 09:41:28 -04:00
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Record each newly-enabled ISO's normalized output to disk under
|
|
|
|
|
/// <see cref="RecordingDirectory"/>. Already-running ISOs are not retroactively
|
|
|
|
|
/// recorded — the operator should disable + re-enable them. Outputs raw BGRA
|
|
|
|
|
/// + manifest.json + convert.cmd; running convert.cmd produces a final
|
|
|
|
|
/// H.264 .mkv via FFmpeg.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public bool RecordIsosToDisk
|
|
|
|
|
{
|
|
|
|
|
get => _recordIsosToDisk;
|
|
|
|
|
set
|
|
|
|
|
{
|
|
|
|
|
if (SetField(ref _recordIsosToDisk, value))
|
|
|
|
|
{
|
|
|
|
|
_controller.SetRecording(value, _recordingDirectory);
|
|
|
|
|
_toast?.Show(value
|
|
|
|
|
? "Recording on — newly-enabled ISOs will write to disk"
|
|
|
|
|
: "Recording off");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Output root for recorder files. Each ISO writes a subdirectory keyed by
|
|
|
|
|
/// participant display name. Default: <c>%USERPROFILE%\Videos\TeamsISO\<date></c>.
|
feat(ui): rebuild MainWindow with Stone-theme design system
Adopts the design language from Dammyjay93/interface-design: warm Stone neutrals, accent orange (#EA580C), borders-only depth (no shadows), 8px spacing grid, all-caps section labels, mono typography for machine names and timecodes.
Themes/StoneTheme.xaml is the single source of truth for design tokens (color brushes, typography styles, spacing) plus restyled control templates (Button, TextBox, ComboBox, ComboBoxItem, CheckBox, DataGrid + DataGridColumnHeader / DataGridRow / DataGridCell, ScrollBar). MainWindow consumes the tokens and is laid out with a header (title + status pill), section-headed Settings sidebar (Output Format / NDI Network / Display), card-wrapped participant list, and a mono status footer.
Settings sidebar surfaces the new NDI group configuration (discovery + output) and a Hide-(Local) checkbox. The latter filters the user's own self-preview from the participants list at the MainViewModel layer (HideLocalSelf=true by default) so operators don't accidentally route their own preview as an ISO. Apply Changes round-trips both FrameProcessingSettings and NdiGroupSettings through the controller in one go.
2026-05-07 23:58:02 -04:00
|
|
|
/// </summary>
|
2026-05-10 09:41:28 -04:00
|
|
|
public string RecordingDirectory
|
|
|
|
|
{
|
|
|
|
|
get => _recordingDirectory;
|
|
|
|
|
set
|
|
|
|
|
{
|
|
|
|
|
if (SetField(ref _recordingDirectory, value) && _recordIsosToDisk)
|
|
|
|
|
_controller.SetRecording(true, value);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2026-05-10 10:01:32 -04:00
|
|
|
/// REST control surface — Stream Deck / Companion / thin-client controllers.
|
|
|
|
|
/// Off by default. Bind address depends on <see cref="ControlSurfaceLanReachable"/>:
|
|
|
|
|
/// loopback (127.0.0.1) by default, or all-interfaces when LAN-reachable.
|
2026-05-10 09:41:28 -04:00
|
|
|
/// </summary>
|
|
|
|
|
public bool ControlSurfaceEnabled
|
|
|
|
|
{
|
|
|
|
|
get => _controlSurfaceEnabled;
|
|
|
|
|
set
|
|
|
|
|
{
|
|
|
|
|
if (!SetField(ref _controlSurfaceEnabled, value)) return;
|
|
|
|
|
var srv = (Application.Current as App)?.ControlSurface;
|
|
|
|
|
if (srv is null) return;
|
2026-05-10 10:01:32 -04:00
|
|
|
if (value) srv.Start(_controlSurfacePort, _controlSurfaceLanReachable);
|
2026-05-10 09:41:28 -04:00
|
|
|
else srv.Stop();
|
|
|
|
|
_toast?.Show(value
|
2026-05-10 10:01:32 -04:00
|
|
|
? $"Control surface listening on {ControlSurfaceUrl}"
|
2026-05-10 09:41:28 -04:00
|
|
|
: "Control surface stopped");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Port the control surface binds to. Editable while the surface is off; while on,
|
|
|
|
|
/// changing the port stops + restarts the listener on the new port.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public int ControlSurfacePort
|
|
|
|
|
{
|
|
|
|
|
get => _controlSurfacePort;
|
|
|
|
|
set
|
|
|
|
|
{
|
|
|
|
|
if (!SetField(ref _controlSurfacePort, value)) return;
|
2026-05-10 10:01:32 -04:00
|
|
|
OnPropertyChanged(nameof(ControlSurfaceUrl));
|
|
|
|
|
if (!_controlSurfaceEnabled) return;
|
|
|
|
|
var srv = (Application.Current as App)?.ControlSurface;
|
|
|
|
|
srv?.Start(value, _controlSurfaceLanReachable);
|
|
|
|
|
_toast?.Show($"Control surface restarted on {ControlSurfaceUrl}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// LAN-reachable mode. When false (default), control surface binds to
|
|
|
|
|
/// 127.0.0.1 — only this machine. When true, binds to all interfaces so
|
|
|
|
|
/// a thin-client controller on a phone or another laptop can drive
|
|
|
|
|
/// TeamsISO. The OSC bridge follows suit if it's running.
|
|
|
|
|
///
|
|
|
|
|
/// Important: HttpListener requires either Administrator privilege OR a
|
|
|
|
|
/// one-time URL ACL reservation for non-loopback prefixes:
|
|
|
|
|
/// <code>netsh http add urlacl url=http://+:9755/ user=Everyone</code>
|
|
|
|
|
/// (run from an elevated PowerShell). Without that the listener throws
|
|
|
|
|
/// AccessDeniedException on Start; the failure surfaces as a logger
|
|
|
|
|
/// warning with the exact netsh command.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public bool ControlSurfaceLanReachable
|
|
|
|
|
{
|
|
|
|
|
get => _controlSurfaceLanReachable;
|
|
|
|
|
set
|
|
|
|
|
{
|
|
|
|
|
if (!SetField(ref _controlSurfaceLanReachable, value)) return;
|
|
|
|
|
PersistUiPrefs();
|
|
|
|
|
OnPropertyChanged(nameof(ControlSurfaceUrl));
|
2026-05-10 09:41:28 -04:00
|
|
|
if (!_controlSurfaceEnabled) return;
|
|
|
|
|
var srv = (Application.Current as App)?.ControlSurface;
|
2026-05-10 10:01:32 -04:00
|
|
|
srv?.Start(_controlSurfacePort, value);
|
|
|
|
|
var osc = (Application.Current as App)?.OscBridge;
|
|
|
|
|
if (osc?.IsRunning == true) osc.Start(_oscBridgePort, value);
|
|
|
|
|
_toast?.Show(value
|
|
|
|
|
? $"Control surface now LAN-reachable: {ControlSurfaceUrl}"
|
|
|
|
|
: "Control surface now loopback-only");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Friendly URL of the running surface, for the settings panel + status
|
|
|
|
|
/// bar tooltip. Resolves to the first non-loopback IPv4 address when
|
|
|
|
|
/// LAN-reachable; loopback otherwise. Computed on demand because the
|
|
|
|
|
/// LAN IP may change between settings opens (Wi-Fi swap, VPN connect).
|
|
|
|
|
/// </summary>
|
|
|
|
|
public string ControlSurfaceUrl
|
|
|
|
|
{
|
|
|
|
|
get
|
|
|
|
|
{
|
|
|
|
|
var host = _controlSurfaceLanReachable
|
|
|
|
|
? GetLanIPv4() ?? "127.0.0.1"
|
|
|
|
|
: "127.0.0.1";
|
|
|
|
|
return $"http://{host}:{_controlSurfacePort}/ui";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2026-05-10 13:11:11 -04:00
|
|
|
/// Best-effort routable IPv4 address suitable for showing the operator a
|
|
|
|
|
/// "paste me into the thin client" URL. Skips:
|
|
|
|
|
/// • loopback interfaces (127.x)
|
|
|
|
|
/// • tunnel/virtual interfaces (NetworkInterfaceType.Tunnel — e.g. WSL,
|
|
|
|
|
/// Hyper-V, Tailscale, OpenVPN-style virtuals)
|
|
|
|
|
/// • APIPA/link-local addresses (169.254.x — assigned when DHCP fails;
|
|
|
|
|
/// a host with one of these AND a real DHCP lease should pick the lease)
|
|
|
|
|
/// Prefers Ethernet/Wi-Fi over everything else, then falls back to the
|
|
|
|
|
/// first non-link-local non-loopback IPv4. Returns null only if no
|
|
|
|
|
/// usable address exists at all.
|
2026-05-10 10:01:32 -04:00
|
|
|
/// </summary>
|
|
|
|
|
private static string? GetLanIPv4()
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
2026-05-10 13:11:11 -04:00
|
|
|
string? linkLocalFallback = null;
|
|
|
|
|
string? otherFallback = null;
|
2026-05-10 10:01:32 -04:00
|
|
|
foreach (var ni in System.Net.NetworkInformation.NetworkInterface.GetAllNetworkInterfaces())
|
|
|
|
|
{
|
|
|
|
|
if (ni.OperationalStatus != System.Net.NetworkInformation.OperationalStatus.Up) continue;
|
|
|
|
|
if (ni.NetworkInterfaceType == System.Net.NetworkInformation.NetworkInterfaceType.Loopback) continue;
|
2026-05-10 13:11:11 -04:00
|
|
|
if (ni.NetworkInterfaceType == System.Net.NetworkInformation.NetworkInterfaceType.Tunnel) continue;
|
|
|
|
|
var isPhysical =
|
|
|
|
|
ni.NetworkInterfaceType == System.Net.NetworkInformation.NetworkInterfaceType.Ethernet ||
|
|
|
|
|
ni.NetworkInterfaceType == System.Net.NetworkInformation.NetworkInterfaceType.GigabitEthernet ||
|
|
|
|
|
ni.NetworkInterfaceType == System.Net.NetworkInformation.NetworkInterfaceType.Wireless80211;
|
|
|
|
|
|
2026-05-10 10:01:32 -04:00
|
|
|
foreach (var ua in ni.GetIPProperties().UnicastAddresses)
|
|
|
|
|
{
|
2026-05-10 13:11:11 -04:00
|
|
|
if (ua.Address.AddressFamily != System.Net.Sockets.AddressFamily.InterNetwork) continue;
|
|
|
|
|
if (System.Net.IPAddress.IsLoopback(ua.Address)) continue;
|
|
|
|
|
var addr = ua.Address.ToString();
|
|
|
|
|
var isLinkLocal = addr.StartsWith("169.254.", StringComparison.Ordinal);
|
|
|
|
|
|
|
|
|
|
if (isPhysical && !isLinkLocal) return addr; // best
|
|
|
|
|
if (!isLinkLocal) otherFallback ??= addr; // routable but virtual NIC
|
|
|
|
|
if (isLinkLocal) linkLocalFallback ??= addr; // worst
|
2026-05-10 10:01:32 -04:00
|
|
|
}
|
|
|
|
|
}
|
2026-05-10 13:11:11 -04:00
|
|
|
return otherFallback ?? linkLocalFallback;
|
2026-05-10 09:41:28 -04:00
|
|
|
}
|
2026-05-10 10:01:32 -04:00
|
|
|
catch { /* best-effort */ }
|
|
|
|
|
return null;
|
2026-05-10 09:41:28 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// OSC bridge over UDP — same command surface as the REST endpoints,
|
|
|
|
|
/// reachable from Companion / TouchOSC / lighting consoles. Off by default;
|
|
|
|
|
/// bound to 127.0.0.1 only.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public bool OscBridgeEnabled
|
|
|
|
|
{
|
|
|
|
|
get => _oscBridgeEnabled;
|
|
|
|
|
set
|
|
|
|
|
{
|
|
|
|
|
if (!SetField(ref _oscBridgeEnabled, value)) return;
|
|
|
|
|
var bridge = (Application.Current as App)?.OscBridge;
|
|
|
|
|
if (bridge is null) return;
|
2026-05-10 10:01:32 -04:00
|
|
|
if (value) bridge.Start(_oscBridgePort, _controlSurfaceLanReachable);
|
2026-05-10 09:41:28 -04:00
|
|
|
else bridge.Stop();
|
2026-05-10 10:01:32 -04:00
|
|
|
var host = _controlSurfaceLanReachable ? "0.0.0.0" : "127.0.0.1";
|
2026-05-10 09:41:28 -04:00
|
|
|
_toast?.Show(value
|
2026-05-10 10:01:32 -04:00
|
|
|
? $"OSC bridge listening on udp://{host}:{_oscBridgePort}/"
|
2026-05-10 09:41:28 -04:00
|
|
|
: "OSC bridge stopped");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>OSC bridge UDP port. Default 9000 (TouchOSC's default).</summary>
|
|
|
|
|
public int OscBridgePort
|
|
|
|
|
{
|
|
|
|
|
get => _oscBridgePort;
|
|
|
|
|
set
|
|
|
|
|
{
|
|
|
|
|
if (!SetField(ref _oscBridgePort, value)) return;
|
|
|
|
|
if (!_oscBridgeEnabled) return;
|
|
|
|
|
var bridge = (Application.Current as App)?.OscBridge;
|
2026-05-10 10:01:32 -04:00
|
|
|
bridge?.Start(value, _controlSurfaceLanReachable);
|
|
|
|
|
var host = _controlSurfaceLanReachable ? "0.0.0.0" : "127.0.0.1";
|
|
|
|
|
_toast?.Show($"OSC bridge restarted on udp://{host}:{value}/");
|
2026-05-10 09:41:28 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Output-name template applied when the operator enables an ISO without
|
|
|
|
|
/// a per-participant CustomName. Default <c>"TEAMSISO_{guid}"</c> matches
|
|
|
|
|
/// the engine's hard-coded behavior; switch to <c>"TEAMSISO_{name}"</c>
|
|
|
|
|
/// for human-readable NDI source names. See <see cref="OutputNameTemplate"/>
|
|
|
|
|
/// for the supported tokens.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public string OutputNameTemplate
|
|
|
|
|
{
|
|
|
|
|
get => _outputNameTemplate;
|
|
|
|
|
set
|
|
|
|
|
{
|
|
|
|
|
if (SetField(ref _outputNameTemplate, value))
|
|
|
|
|
{
|
|
|
|
|
TeamsISO.App.Services.OutputNameTemplate.Set(value ?? string.Empty);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Background update check on launch. Throttled to once per 24h via a
|
|
|
|
|
/// timestamp file. When a newer release is found, surfaces a non-modal
|
|
|
|
|
/// banner with a "Get update" button. Off-by-default would be friendlier
|
|
|
|
|
/// for paranoid setups; on-by-default is friendlier for adoption.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public bool UpdateCheckOnLaunch
|
|
|
|
|
{
|
|
|
|
|
get => _updateCheckOnLaunch;
|
|
|
|
|
set
|
|
|
|
|
{
|
|
|
|
|
if (SetField(ref _updateCheckOnLaunch, value))
|
|
|
|
|
{
|
|
|
|
|
UpdateChecker.LaunchCheckEnabled = value;
|
|
|
|
|
_toast?.Show(value
|
|
|
|
|
? "Update checks enabled — runs once per 24h on launch"
|
|
|
|
|
: "Update checks disabled");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// On launch, automatically re-apply the most recently applied operator preset.
|
|
|
|
|
/// Closes the loop on the recurring-show workflow: the operator clicks Apply
|
|
|
|
|
/// once, and from that point on TeamsISO restores the same routing on every
|
|
|
|
|
/// subsequent launch as soon as the matching participants come online.
|
|
|
|
|
/// Persisted to <c>presets.json</c>'s <c>AutoApplyOnStartup</c> field via
|
|
|
|
|
/// <see cref="OperatorPresetStore"/>.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public bool AutoApplyLastPreset
|
|
|
|
|
{
|
|
|
|
|
get => _autoApplyLastPreset;
|
|
|
|
|
set
|
|
|
|
|
{
|
|
|
|
|
if (SetField(ref _autoApplyLastPreset, value))
|
|
|
|
|
{
|
|
|
|
|
try { OperatorPresetStore.SetAutoApplyOnStartup(value); }
|
|
|
|
|
catch { /* best-effort */ }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
feat(ui): rebuild MainWindow with Stone-theme design system
Adopts the design language from Dammyjay93/interface-design: warm Stone neutrals, accent orange (#EA580C), borders-only depth (no shadows), 8px spacing grid, all-caps section labels, mono typography for machine names and timecodes.
Themes/StoneTheme.xaml is the single source of truth for design tokens (color brushes, typography styles, spacing) plus restyled control templates (Button, TextBox, ComboBox, ComboBoxItem, CheckBox, DataGrid + DataGridColumnHeader / DataGridRow / DataGridCell, ScrollBar). MainWindow consumes the tokens and is laid out with a header (title + status pill), section-headed Settings sidebar (Output Format / NDI Network / Display), card-wrapped participant list, and a mono status footer.
Settings sidebar surfaces the new NDI group configuration (discovery + output) and a Hide-(Local) checkbox. The latter filters the user's own self-preview from the participants list at the MainViewModel layer (HideLocalSelf=true by default) so operators don't accidentally route their own preview as an ISO. Apply Changes round-trips both FrameProcessingSettings and NdiGroupSettings through the controller in one go.
2026-05-07 23:58:02 -04:00
|
|
|
|
2026-05-07 11:39:46 -04:00
|
|
|
public AsyncRelayCommand ApplyCommand { get; }
|
|
|
|
|
|
2026-05-10 10:01:32 -04:00
|
|
|
/// <summary>
|
|
|
|
|
/// Copy the current control-surface URL to the clipboard. Operators on a
|
|
|
|
|
/// thin-client setup tap this, then paste into a phone browser. Bound to
|
|
|
|
|
/// a small button next to the LAN-reachable toggle.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public RelayCommand CopyControlSurfaceUrlCommand { get; }
|
|
|
|
|
|
2026-05-10 09:41:28 -04:00
|
|
|
/// <summary>
|
|
|
|
|
/// Restore Output settings (framerate, resolution, aspect, audio) to the engine's
|
|
|
|
|
/// shipping defaults. Doesn't touch NDI groups (those are intentionally sticky —
|
|
|
|
|
/// the operator's transcoder topology is a per-machine setting that survives
|
|
|
|
|
/// preferences resets) and doesn't touch Display toggles. Confirms first.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public RelayCommand ResetOutputDefaultsCommand { get; }
|
|
|
|
|
|
2026-05-08 07:19:31 -04:00
|
|
|
/// <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; }
|
|
|
|
|
|
feat(ui): rebuild MainWindow with Stone-theme design system
Adopts the design language from Dammyjay93/interface-design: warm Stone neutrals, accent orange (#EA580C), borders-only depth (no shadows), 8px spacing grid, all-caps section labels, mono typography for machine names and timecodes.
Themes/StoneTheme.xaml is the single source of truth for design tokens (color brushes, typography styles, spacing) plus restyled control templates (Button, TextBox, ComboBox, ComboBoxItem, CheckBox, DataGrid + DataGridColumnHeader / DataGridRow / DataGridCell, ScrollBar). MainWindow consumes the tokens and is laid out with a header (title + status pill), section-headed Settings sidebar (Output Format / NDI Network / Display), card-wrapped participant list, and a mono status footer.
Settings sidebar surfaces the new NDI group configuration (discovery + output) and a Hide-(Local) checkbox. The latter filters the user's own self-preview from the participants list at the MainViewModel layer (HideLocalSelf=true by default) so operators don't accidentally route their own preview as an ISO. Apply Changes round-trips both FrameProcessingSettings and NdiGroupSettings through the controller in one go.
2026-05-07 23:58:02 -04:00
|
|
|
private async Task ApplyAsync()
|
2026-05-07 11:39:46 -04:00
|
|
|
{
|
|
|
|
|
var settings = new FrameProcessingSettings(_framerate, _resolution, _aspect, _audio);
|
feat(ui): rebuild MainWindow with Stone-theme design system
Adopts the design language from Dammyjay93/interface-design: warm Stone neutrals, accent orange (#EA580C), borders-only depth (no shadows), 8px spacing grid, all-caps section labels, mono typography for machine names and timecodes.
Themes/StoneTheme.xaml is the single source of truth for design tokens (color brushes, typography styles, spacing) plus restyled control templates (Button, TextBox, ComboBox, ComboBoxItem, CheckBox, DataGrid + DataGridColumnHeader / DataGridRow / DataGridCell, ScrollBar). MainWindow consumes the tokens and is laid out with a header (title + status pill), section-headed Settings sidebar (Output Format / NDI Network / Display), card-wrapped participant list, and a mono status footer.
Settings sidebar surfaces the new NDI group configuration (discovery + output) and a Hide-(Local) checkbox. The latter filters the user's own self-preview from the participants list at the MainViewModel layer (HideLocalSelf=true by default) so operators don't accidentally route their own preview as an ISO. Apply Changes round-trips both FrameProcessingSettings and NdiGroupSettings through the controller in one go.
2026-05-07 23:58:02 -04:00
|
|
|
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);
|
feat(ui): toast feedback for settings actions; refresh _NEXT.md
Adds a small auto-dismissing pill notification at the bottom-center of the participants area: 'Settings saved' on Apply Changes, 'Transcoder topology applied — restart Teams to take effect' after the one-click NDI groups setup. ToastViewModel owns its own DispatcherTimer and resets the dismissal countdown on successive calls, so the most recent message is always the one visible. Hooked into MainViewModel and threaded into GlobalSettingsViewModel via constructor injection.
_NEXT.md rewritten to reflect the May 2026 hardening pass: separates engine / UI / networking / Phase E.1 / diagnostics / CI / tests sections, lists every shipped item, and re-prioritizes the remaining work (Phase E.2-E.3 embedded Teams, code-signing the MSI, refresh-discovery affordance, output thumbnail previews, settings panel UX, auto-disable on departure, operator presets).
2026-05-09 09:30:04 -04:00
|
|
|
|
|
|
|
|
_toast?.Show("Settings saved");
|
2026-05-07 11:39:46 -04:00
|
|
|
}
|
2026-05-08 07:19:31 -04:00
|
|
|
|
|
|
|
|
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);
|
feat(ui): toast feedback for settings actions; refresh _NEXT.md
Adds a small auto-dismissing pill notification at the bottom-center of the participants area: 'Settings saved' on Apply Changes, 'Transcoder topology applied — restart Teams to take effect' after the one-click NDI groups setup. ToastViewModel owns its own DispatcherTimer and resets the dismissal countdown on successive calls, so the most recent message is always the one visible. Hooked into MainViewModel and threaded into GlobalSettingsViewModel via constructor injection.
_NEXT.md rewritten to reflect the May 2026 hardening pass: separates engine / UI / networking / Phase E.1 / diagnostics / CI / tests sections, lists every shipped item, and re-prioritizes the remaining work (Phase E.2-E.3 embedded Teams, code-signing the MSI, refresh-discovery affordance, output thumbnail previews, settings panel UX, auto-disable on departure, operator presets).
2026-05-09 09:30:04 -04:00
|
|
|
|
|
|
|
|
_toast?.Show("Transcoder topology applied — restart Teams to take effect");
|
2026-05-08 07:19:31 -04:00
|
|
|
}
|
2026-05-07 11:39:46 -04:00
|
|
|
}
|