23 KiB
Plan Backlog
Completed phases
- Phase A — Engine Foundation (tag:
phase-a-complete) — domain model, parsers, participant tracker, frame processor, config, fakes, CI gate. - Phase B-1 — Pipeline Orchestration (tag:
phase-b-1-complete) — NdiReceiver, NdiSender, ExponentialBackoff, NdiRuntimeProbe, IsoPipeline supervisor, IsoController. - Phase B-2 — Real NDI Interop (tag:
phase-b-2-complete) —NdiInteropPInvokeagainst NDI 6 SDK, managed BGRA scaler,TeamsISO.Consoleheadless smoke runner,NdiVersionconstants. - Phase C — WPF UI (tag:
phase-c-complete) — MVVM helpers, ParticipantViewModel, GlobalSettingsViewModel, AlertBannerViewModel, MainViewModel, MainWindow XAML with participants DataGrid + settings sidebar + alert banner, App.xaml DI bootstrap. - Hardening + brand pass — May 2026 — see "Done since the May 2026 hand-off" below.
- Phase D — WiX Installer & Forgejo release — WiX v5 MSI scaffold, ARP icon wired, tag-push release workflow that builds + uploads MSI as a release asset.
Done since the May 2026 hand-off
Engine
- Forward-slash project paths in
TeamsISO.slnso.slnffilters work on Windows MSBuild. NdiNativeLibraryResolverresolvesProcessing.NDI.Lib.x64.dllviaNDI_RUNTIME_DIR_V6(with V5 / V4 fallbacks), so the engine starts on installs where the NDI dir isn't on PATH.NdiVersion.ExpectedRuntimeVersionPrefixupdated to match the shipping NDI 6 banner format (NDI SDK WIN64 …).NdiSourceParseraccepts current Teams desktop'sMS Teams - <name>brand format (plus legacyTeamsand defensiveMicrosoft Teams); reserved suffixes (Active Speaker,Audio,Audio Mix,Screen Share) are recognized in both legacy and dash-prefixed forms.- NDI groups end-to-end (discovery + output):
INdiInterop.CreateFinder(string?)andCreateSender(string, string?)populatep_groups;IsoControllerthreads them through fromEngineConfig.NdiGroups. ParticipantTrackersurfacesNdiSourceKind.ActiveSpeakeras a synthetic routable row named "Active Speaker" with a deterministic v5-GUID Id derived fromauto-mix:<machine>.IsoHealthStatswired end-to-end: live receiver/sender/processor refs published from the inner pipeline, frame counters / source resolution / running FPS (30-frame moving window) / drops + duplicates / pipeline state surfaced viaIsoController.GetStats.- Rolling daily file logging at
%LOCALAPPDATA%\TeamsISO\Logs\via Serilog.Sinks.File.
UI
- WPF rebuilt around Wild Dragon brand × Microsoft Teams flush layout — left rail with real dragon-mark logo (clickable → About dialog), chromeless title bar with custom min/max/close caption controls, cyan accent.
- Inter Variable + JetBrains Mono Variable bundled as
<Resource>so typography matches wilddragon.net regardless of system fonts. - App icon
teamsiso.ico(7 sizes) on taskbar / window / About / WiX MSI ARP. - Single-instance enforcement via per-user named Mutex with broadcast bring-to-front.
- Empty-state placeholder when no Teams sources are visible (faded dragon + checklist).
- Live frame counters in the Source / Live columns (in/out/drops, source resolution, running FPS).
- Per-pipeline state surfaced in the ISO toggle:
● LIVE(cyan),● ERROR(coral),● NO SIGNAL(amber),…(processing). - "Stop all ISOs" emergency button at the participants header.
- Hide-(Local) toggle so the user's own self-preview is filtered from the participants list.
- Window position / size / state persisted to
%LOCALAPPDATA%\TeamsISO\window.json, multi-monitor safe. - Tooltips on every interactive control in the settings panel + per-row textbox + ISO toggle.
- Toast feedback for settings actions (Apply / Apply Transcoder Topology / Stop All / Auto-disable).
- Auto-disable on participant departure (configurable, off by default): when a participant's NDI source disappears the engine tears down their pipeline; the toggle lives in
DISPLAYsettings. - Operator presets: chromeless
Presets…dialog from the participants header. Saves the current per-participantIsEnabled+CustomNameset keyed by display name to%LOCALAPPDATA%\TeamsISO\presets.json(atomic write, schema-versioned). Apply walks the live participants and reconciles viaEnableIsoAsync/DisableIsoAsync; participants in the preset who aren't in the current meeting are reported in the toast. - Auto-apply last preset on launch: opt-in checkbox in
DISPLAYsettings. After the operator's first manual Apply, every subsequent TeamsISO launch silently re-applies the same preset once participants populate (30-second grace window before applying with whoever's online). State lives inpresets.jsonnext to the preset list. - Refresh discovery affordance: header pill that rebuilds the underlying NDI finder on the next poll tick.
IIsoController.RefreshDiscoveryflips a flag the discovery loop honors before the next tick — old finder disposed, new finder created, seen-set cleared so all currently-visible sources re-fire as Added.ParticipantTracker.HandleAddedis idempotent: re-emitting the same FullName refreshes LastSeen rather than minting a duplicate row. - Settings tabs: the settings sidebar is now a TabControl with
OUTPUT/NETWORK/DISPLAYtabs and a single Apply Changes button below. Underline-on-active tab style lives inWildDragonTheme.xaml(Wd.TabControl+Wd.TabItem). - Crash diagnostics:
App.OnStartupwiresAppDomain.UnhandledException,Application.DispatcherUnhandledException, andTaskScheduler.UnobservedTaskExceptioninto a unified Serilog.Critical log line + user-facing dialog that points at the log directory. Dispatcher exceptions are markedHandled = trueso a single bad UI thunk doesn't take the app down; AppDomain crashes are terminal but at least the user gets the log path before exit. - First-launch onboarding: chromeless welcome dialog walks users through the once-per-machine setup (NDI runtime, Teams admin permission, transcoder topology, presets, log location). Suppressed after dismissal via marker file at
%LOCALAPPDATA%\TeamsISO\onboarding.flag. Re-openable from the About dialog via "Show welcome" button. - Reset output to defaults: ghost button at the bottom of the OUTPUT settings tab restores framerate / resolution / aspect / audio to
FrameProcessingSettings.Defaultafter confirmation. Doesn't touch NDI groups (sticky per-machine) or display toggles. - Per-output recording:
IRecorderSinkinterface +RawBgraRecorderSinkimplementation. When the operator enables "Record ISOs to disk" in the DISPLAY tab, each newly-enabled ISO writes its normalized output to<chosen-dir>/<participant>/video.bgraplus a sidecarmanifest.json(width / height / fps / frame counts) and aconvert.cmdone-liner that pipes the raw stream into FFmpeg to produce a final H.264output.mkv. Recorder runs on its own bounded queue (240-frameDropOldestbuffer) so disk pressure never blocks the live ISO; recorder failures are caught and ignored at the channel-write layer for the same reason. Already-running ISOs are not retroactively captured — operator disables + re-enables to start recording. Recording can be wired to a real-time H.264 encoder later via Vortice.MediaFoundation; theIRecorderSinkinterface is designed to swap implementations without touching the pipeline. - REST control surface:
ControlSurfaceServer—System.Net.HttpListeneron127.0.0.1:9755(configurable). Endpoints for participant ISO toggle (by Id or display name), refresh discovery, stop-all, recording on/off, preset apply, and Teams in-call commands (mute / camera / share / leave / raise-hand). Off by default; toggle in the DISPLAY tab. Bitfocus Companion / Stream Deck plugins / OSC bridges drive it. Documented atdocs/CONTROL-SURFACE.md. - PresetApplier: extracted from
PresetsDialog.OnApply. Single source of truth for "apply this preset to live participants" — used by the dialog, byMainViewModel.TryAutoApplyPendingPreset(auto-apply on launch), and by the RESTPOST /presets/{name}/applyendpoint. Marshals UI-bound writes (CustomName / IsEnabled) through an optional Dispatcher so off-thread callers don't crash WPF. - In-app preview thumbnails: 160×90 WriteableBitmap per participant, fed from the engine's most recent
ProcessedFrameat the existing 1Hz stats tick. Inline nearest-neighbor scaler inParticipantViewModel.UpdateThumbnailwrites directly into the bitmap's pinned BackBuffer (unsafe block,<AllowUnsafeBlocks>true</AllowUnsafeBlocks>in the .csproj) for ~10× perf vs. going through Span. Falls back to a—placeholder card when no pipeline is running. New "Preview" column in the participants DataGrid. - WebSocket live state push:
ws://127.0.0.1:9755/ws— clients connect, receive a participants snapshot immediately, and get fresh snapshots within 250ms whenever state changes. Snapshot diffing on JSON string keeps the wire quiet during steady-state. Used by Stream Deck / Companion buttons that want to light up when an ISO goes LIVE without polling. - OSC bridge over UDP:
OscBridgelistens on127.0.0.1:9000(TouchOSC's default). Same command vocabulary as the REST endpoints —/teamsiso/iso "Jane" 1,/teamsiso/preset "Friday Show",/teamsiso/teams/mute, etc. Minimal OSC 1.0 parser (int / float / string / T / F type tags; no bundles). TouchOSC layouts and Companion's Generic OSC surface can both drive it directly. - Manual update check: "Check for updates" button in the About dialog. Asks
forge.wilddragon.net/api/v1/repos/zgaetano/teamsiso/releases?limit=1, compares the newest tag's SemVer to the running version, prompts to open the releases page if newer. Manual only — no background polling for v1 so a long-running show doesn't get interrupted by a surprise installer. - Auto-update banner on launch: opt-in (default on) silent check throttled to once per 24h via
%LOCALAPPDATA%\TeamsISO\last-update-check.txt. When a newer release is found, a non-modal banner appears above the body with "Get update" / "Dismiss" buttons. Suppression via flag file atno-update-check.flagfor fleets that prefer central rollout. NewUpdateBannerViewModeldistinct from the engine alert banner. - Preset import / export: Export / Import buttons in the Presets dialog footer, backed by
OperatorPresetStore.ExportAllAsJson/ImportBundle. Bundle format isteamsiso-presets-bundle/v1JSON. On name collision the importer asks once (Overwrite/Keep/Cancel) rather than per-preset; deliberately doesn't include the operator'sLastAppliedName/AutoApplyOnStartupsince those are machine-local. - Recording markers:
IRecorderSink.AddMarker(label)plusIIsoController.AddRecordingMarker(label)fan-out to every active recorder. Surfaced via "Marker" button in the IN-CALL bar (auto-labels with timestamp),POST /recording/markerin the REST surface, and/teamsiso/recording/marker "Label"in OSC. Markers land inmanifest.jsonundermarkers[]withoffsetMs+labelfields for post-production chaptering. - Custom NDI output name template:
OutputNameTemplatestatic helper persisted tooutput-name-template.txtwith{name}/{guid}/{machine}/{timestamp}tokens. DefaultTEAMSISO_{guid}preserves the engine's hard-coded behavior; operator can switch toTEAMSISO_{name}for human-readable downstream switcher names. UI editor in the NETWORK settings tab. - Enriched footer status bar: rec badge (coral dot + count) when at least one ISO is being recorded; control-surface badge (cyan dot + "REST :9755 + OSC :9000") when those services are running. Computed at the existing 1Hz stats tick from
IIsoController.RecordingEnabled× running pipeline count andApp.ControlSurface.IsRunning/App.OscBridge.IsRunning. - Disk space watcher:
DiskSpaceWatcherpolls the recording drive every 5s while recording is on. Coral toast at <10GB free; auto-disables recording at <1GB so an unattended long show doesn't crash the host on disk-full. - Diagnostic bundle export: "Export diagnostics" button in About zips logs + config + presets + window state + version metadata into a
teamsiso-diagnostics-<ts>.zipin~/Downloads. Excludes screenshots / memory dumps; only files the user already wrote. - Per-participant recording opt-out: new
Reccolumn in the DataGrid lets the operator choose which ISOs get recorded when global recording is on.IIsoController.EnableIsoAsyncgained an optionalbool? recordOverrideparameter — null = follow global flag, true = force on, false = force off. - Window-scoped keyboard shortcuts: F1 (help), Ctrl+M (drop marker), Ctrl+Shift+S (stop all), Ctrl+R (refresh discovery). InputBindings on MainWindow → MainViewModel commands; F1 opens the new
HelpWindowcheat sheet. - Help cheat sheet: chromeless
HelpWindowlists keyboard shortcuts, file locations (%LOCALAPPDATA%\TeamsISO\Logs\,%APPDATA%\TeamsISO\config.json, etc.), and links to the public docs. Reduces support friction. - Bulk enable: header
Enable allbutton (green dot) enables ISOs for every online + non-enabled participant. Per-participant best-effort with a count toast. - Live participant filter: textbox above the DataGrid filters by display-name substring as you type. Backed by an
ICollectionViewFilter callback so the underlyingObservableCollectionisn't mutated (preserving identity-tracking). - Right-click context menu on participant rows: Toggle ISO, toggle Record-this-participant, Copy NDI source name to clipboard. Uses the existing per-row commands so the menu is just another binding surface.
- CLI:
--apply-preset NAME: launch-time flag that auto-applies the named preset once participants populate. Same code path as the persisted auto-apply preference. Useful forFriday Show.lnkdesktop shortcuts that drive recurring routings. - Dynamic status text: footer's center text now reads "3/5 ISOs live · 2 recording" once routing starts, instead of the static "Engine running at X fps target." Composed in
OnStatsTickfrom running participant + recording counts. - Embedded HTML control panel at
GET /ui: self-contained ~6KB page with WebSocket-driven live state and buttons for the common control actions. Open in a phone or second-monitor browser to drive TeamsISO without context-switching from the show. No external dependencies, no build step. - Session timer in footer: shows
MM:SS(orHH:MM:SSpast an hour) elapsed since the first ISO went live this session. Resets when all ISOs go offline. Green dot indicator for at-a-glance status. - Show notes service:
POST /notesand/teamsiso/notes "..."(OSC) append timestamped lines to%LOCALAPPDATA%\TeamsISO\Notes\<YYYY-MM-DD>.md. Operators wire a Stream Deck button to drop notes during a live show without leaving the production app. Markdown format renders cleanly in any editor. - NotesWindow inline viewer: chromeless dialog that displays today's notes file with 2s polling so REST/OSC-driven appends surface live. "Notes" button in the IN-CALL bar.
- Duplicate-preset action: "Duplicate" footer button in the Presets dialog. Custom inline prompt suggests
<original> (copy)/(copy 2)/ etc. names. - CHANGELOG.md: project-wide changelog following keep-a-changelog format. Captures the full May 2026 batch under
[Unreleased]. - README rewrite: top-level README now lists what TeamsISO does, build instructions, doc links, keyboard shortcuts table, file-locations table.
- Confirm-before-Stop-All: stop-all button now requires Yes confirmation, preventing accidental mid-show clicks. Default-No so Enter cancels.
Networking automation
- One-click transcoder topology button in Settings: writes
%APPDATA%\NDI\ndi-config.v1.jsonso all local senders broadcast onteamsiso-inputand local receivers see bothpublic+teamsiso-input. Engine settings auto-flip to receive-fromteamsiso-inputand emit-onpublic. Atomic write with timestamped backup of the prior config.
Phase E — embedded Teams orchestration
Spec at docs/superpowers/specs/2026-05-08-embedded-teams-orchestration.md. All three sub-phases shipped in May 2026:
- E.1 — Launcher. Rail "Launch / Stop Teams" toggle: launches via
ms-teams:URI →ms-teams.exe→ classicUpdate.exe --processStart, asks to confirmWM_CLOSEof all running Teams windows when toggled while Teams is up. - E.2 — Window orchestration. Rail eye-icon button hides every visible top-level Teams window via
EnumWindows+ShowWindow(SW_HIDE). Click again to restore + foreground. Lets the operator drive Teams from TeamsISO without ever seeing the Teams UI. - E.3 — In-call controls. UIAutomation-driven Mute / Camera / Share / Leave buttons in a new
IN-CALLcard at the top of the participants area.TeamsControlBridgewalks Teams' automation tree by candidate Name list (Mute,Unmute,Microphone,Toggle mute…) and tries Invoke or Toggle pattern. Tolerant lookup: when a Teams update renames a button we extend the candidate list, no crash. Toasts reflect the four outcomes (Invoked / TeamsNotRunning / ControlNotFound / InvokeFailed). Bridge also exposes (UI-not-yet-wired)ToggleRaiseHand,ToggleChat,OpenBackgroundEffects. Candidate name lists localized for English/German/Spanish/French/Portuguese/Japanese — all locales matched in a single pass; the first match wins. - PostMessage shortcut forwarding fallback.
TeamsLauncher.SendShortcut(modifiers, vk)posts WM_KEYDOWN/UP to the most-recently-used hidden Teams HWND. Best-effort — modern WebView2-hosted Teams sometimes ignores synthesized key messages at the app-shortcut layer; UIA is preferred when a button exists for the action.
Diagnostics
TeamsISO.Console --list-sourcesenumerates raw NDI source names visible to the local finder for ~5 seconds; debugging tool for setup issues.TeamsISO.Console --versionprints engine version + build SHA + .NET + OS + NDI runtime banner + exit-code legend, for support tickets.- About dialog inside the WPF host with the same info.
CI / Release / Docs
- Forgejo CI is green:
actions/upload-artifact@v3(Forgejo doesn't support v4 yet). .forgejo/workflows/release.yml: tag-push (v*.*.*) builds + tests + publishes + builds the MSI on a Windows runner and attaches it to the auto-created Forgejo release via the REST API.- Optional code-signing wired into
release.yml: whenSIGN_CERT_PFX_BASE64+SIGN_CERT_PASSWORDForgejo secrets are set, the workflow signs bothTeamsISO.exe(before MSI build) and the MSI (after) with SHA-256 + RFC 3161 timestamp. Skipped silently when the cert isn't configured.docs/RELEASING.mddocuments the OV vs EV trade-offs and the Azure Trusted Signing migration path.
Tests
- 78 unit tests passing; 9 NDI integration tests gated behind
--filter requires=ndi(runtime probe, finder + sender lifecycle on default and custom groups, loopback discovery, full pipeline frame round-trip asserting 1080p normalization).
Done since May 10 hand-off
Engine
- Audio peak metering wired end-to-end.
IsoHealthStats.PeakAudioLevelnow reports real values from a sibling NDI audio capture loop inNdiReceiver. NewINdiInterop.CaptureAudioPeakmethod (default- implemented for FakeNdiInterop, overridden in NdiInteropPInvoke).AudioPeakComputerhandles FLTP / FLT / PCM s16 with 14 unit tests covering edge cases. UI VU bars in the participants DataGrid now animate; the existing decay logic inParticipantViewModelwas already in place waiting for real values.
Control surface
- LAN-reachable mode. New checkbox in DISPLAY tab toggles whether the
REST/WebSocket surface and OSC bridge bind to
127.0.0.1only or to all interfaces (http://+:port/,IPAddress.Any). Settings panel surfaces the routable URL with a Copy button (picker prefers physical NICs and skips Tailscale / VPN tunnels / APIPA addresses). Use case: headless host PC + thin client on the same LAN — operator runs Teams + TeamsISO on a quiet machine, drives it from anywhere on the production network. No auth — documented as a trusted-LAN-only mode. First-time bind requires a one-shotnetsh http add urlacl; the diagnostic warning fires the exact remediation command if the bind fails.
UI polish
- Visible hover affordances on every themed button (Ghost / Caption / RailIcon / IsoToggle / Primary). Cyan accent borders + brighter fills so mouse-hover and tab-focus give an unmistakable affordance regardless of which dark surface the button sits on.
- Keyboard focus rings (
IsKeyboardFocusedtriggers) so tab-cycling through the UI gives visual feedback (was nothing —FocusVisualStylewasx:Nullwith no replacement). - ScrollBar restyled to slim transparent track + tinted thumb (Edge / VS Code pattern) in place of the chunky Win9x default.
- ContextMenu / MenuItem styled to match the dark canvas — right-click on a participant row no longer shows the cream-colored Notepad popup.
- ToolTip restyled: SurfaceElevated card with rounded corner + 320px text wrap, replacing the cream Win98 popup.
- Wd.Button.Primary disabled state distinct (was identical to enabled).
Next
-
Smoke-test on real Teams. Most of May's work hasn't run against a live meeting yet: the UIA in-call commands (mute / camera / share / leave) need their candidate-Name lists validated against the current Teams build, and the auto-apply-on-launch flow needs a real recurring meeting to confirm the 30-second grace window is right. Pin the AutomationIds for buttons we find — Name-based lookup is a starting point, AutomationId is what survives Teams UI updates. Now also includes: validate the audio peak metering against real Teams audio (check that FLTP decoding is correct for whatever sample rate Teams is broadcasting; the
--filter requires=ndiintegration tests don't exercise audio). -
Acquire a code-signing cert. Pipeline is wired (see "CI / Release / Docs" above); just needs
SIGN_CERT_PFX_BASE64+SIGN_CERT_PASSWORDset in Forgejo Secrets. OV cert ($200/yr) gets us signed but SmartScreen builds reputation slowly; EV cert ($300/yr, hardware token) is SmartScreen-trusted immediately. Azure Trusted Signing is the cloud-native path if a token-on-runner is fiddly. -
Activate
MediaFoundationRecorderSink. Scaffold + activation docs atdocs/REAL-TIME-RECORDING.mdship in this batch (gated behindMF_AVAILABLEbuild symbol). To enable:dotnet add Vortice.MediaFoundation, defineMF_AVAILABLE, swap one line inIsoController.EnableIsoAsync. Rough ~10× disk-pressure reduction. -
Forward Teams keyboard shortcuts via SendInput. Phase E.2 hides the Teams window but doesn't forward Ctrl+Shift+M / Ctrl+Shift+O / Ctrl+Shift+H to it. UIA covers mute/camera/share/leave/raise-hand/chat/background already; SendInput would let us pass arbitrary global hotkeys through to a hidden Teams for actions UIA can't reach. Lower priority now that UIA covers the core actions.