using System.Reactive.Linq; using Microsoft.Extensions.Logging; using TeamsISO.Engine.Controller; using TeamsISO.Engine.Domain; using TeamsISO.Engine.Interop; using TeamsISO.Engine.Logging; using TeamsISO.Engine.NdiInterop; using TeamsISO.Engine.Persistence; using TeamsISO.Engine.Pipeline; namespace TeamsISO.Console; using SysConsole = System.Console; /// /// Headless smoke runner. Wires up the engine end-to-end against the production NDI P/Invoke /// interop and prints participant updates, alerts, and ISO state. Useful for first-validation /// against a real Teams meeting before the WPF UI is alive. /// /// Requires Windows + the NDI Runtime installed. /// /// Usage: /// teamsiso-console # discover only — print participants as they appear/leave /// teamsiso-console --enable-all # auto-enable an ISO for every discovered participant /// public static class Program { public static async Task Main(string[] args) { var enableAll = args.Contains("--enable-all", StringComparer.OrdinalIgnoreCase); using var loggerFactory = EngineLogging.CreateConsole(LogLevel.Information); var logger = loggerFactory.CreateLogger("TeamsISO.Console"); if (!OperatingSystem.IsWindows()) { logger.LogError("TeamsISO.Console requires Windows + the NDI Runtime. Current OS is not Windows."); return 1; } NdiInteropPInvoke interop; try { interop = new NdiInteropPInvoke(loggerFactory.CreateLogger()); } catch (Exception ex) { logger.LogError(ex, "Could not initialize NDI runtime. Install the NDI Runtime from https://ndi.video/tools/ and try again."); return 2; } var configPath = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "TeamsISO", "config.json"); var configStore = new ConfigStore(configPath, loggerFactory.CreateLogger()); var probe = new NdiRuntimeProbe(interop, NdiVersion.ExpectedRuntimeVersionPrefix); var scaler = new ManagedNearestNeighborFrameScaler(); // Pipeline factory: wires up real NDI receiver/processor/sender against the production interop. IsoPipeline PipelineFactory(IsoPipelineConfig config) { var clock = new PeriodicTimerFrameClock(config.Settings.FramerateHz); return new IsoPipeline( config, interop, scaler, clock, ExponentialBackoff.Default, (delay, ct) => Task.Delay(delay, ct), loggerFactory); } await using var controller = new IsoController( interop, PipelineFactory, configStore, probe, loggerFactory); // Wire up event printing var sub1 = controller.Participants .DistinctUntilChanged() .Subscribe(plist => { logger.LogInformation("Participants ({Count}):", plist.Count); foreach (var p in plist) logger.LogInformation(" - {Name} source={Source}", p.DisplayName, p.CurrentSource?.FullName ?? ""); }); var sub2 = controller.Alerts.Subscribe(a => logger.LogWarning("ALERT: {Message}", a.Message)); using var cts = new CancellationTokenSource(); SysConsole.CancelKeyPress += (_, e) => { e.Cancel = true; cts.Cancel(); }; await controller.StartAsync(cts.Token); logger.LogInformation("Engine started. Default framerate: {Fps:F2} fps. Press Ctrl+C or 'q' Enter to quit.", controller.GlobalSettings.FramerateHz); if (enableAll) { // Auto-enable any participant we see, until cancellation. controller.Participants.Subscribe(async plist => { foreach (var p in plist.Where(p => p.CurrentSource is not null)) { try { await controller.EnableIsoAsync(p.Id, customName: null, cts.Token); } catch (Exception ex) { logger.LogWarning(ex, "Could not enable ISO for {Name}.", p.DisplayName); } } }); } // Read until 'q' or cancellation. var input = Task.Run(() => { while (!cts.IsCancellationRequested) { var line = SysConsole.ReadLine(); if (line is null) return; if (line.Trim().Equals("q", StringComparison.OrdinalIgnoreCase)) { cts.Cancel(); return; } } }); try { await Task.Delay(Timeout.Infinite, cts.Token); } catch (OperationCanceledException) { /* expected on quit */ } sub1.Dispose(); sub2.Dispose(); interop.Dispose(); logger.LogInformation("Engine stopped."); return 0; } }