21 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).
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.
-
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. -
Wire engine audio capture. The UI's audio level VU bar (in the Live column) is in place but inert —
IsoHealthStats.PeakAudioLevelalways reads 0.0. Engine work needed: extendINdiInterop.CaptureFrameto also surface audio frames, parse based on FourCC (FLTp, PCMs16, etc), compute peak per pipeline tick, publish throughIsoPipeline.GetStats. Once that's done the UI bar starts animating with no further changes. -
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.