Commit graph

63 commits

Author SHA1 Message Date
bab29b02ab feat(ui): chromeless title bar with custom caption controls
All checks were successful
CI / build-and-test (push) Successful in 37s
MainWindow drops the standard Windows title bar (WindowStyle=None + WindowChrome with CaptionHeight=44, ResizeBorderThickness=6, UseAeroCaptionButtons=False) and draws its own minimize / maximize-restore / close buttons inline in the existing header strip. The custom buttons opt into shell:WindowChrome.IsHitTestVisibleInChrome=True so clicks fire on them rather than starting a window drag.

Result: the entire top of the window is now ours, matching the Microsoft Teams desktop client's flush header look. The 'TeamsISO + by Wild Dragon' branding sits at the same baseline as the engine-status pill and the caption controls, and dragging anywhere not occupied by an interactive widget moves the window.

Caption-button styles in the theme: 46x32 hover-tinted, with the close button turning the Windows 11 #C42B1C red on hover. Maximize-button glyph swaps between the single-rectangle and overlapping-rectangles variants on StateChanged.

Drive-by: ParticipantViewModel.{FramesIn,FramesOut,IncomingResolution} setters dropped from private to public so {Run Text=...} bindings (which default to TwoWay on Run) can attach without WPF throwing 'cannot work on read-only property'.
2026-05-08 00:55:57 -04:00
9c231118de feat(stats): wire IsoHealthStats end-to-end and surface live counters in UI
IsoPipeline now publishes refs to its currently-live NdiReceiver and NdiSender (set by RunInnerPipelineAsync, cleared on exit) so a stats poll from any thread can read FramesCaptured / FramesSent without entangling the pipeline's lifetime with its observer. The receiver's raw-frame channel is wrapped with a TappedChannelWriter so the most recent RawFrame is captured for source-resolution display, again without changing the receiver's contract.

IsoController.GetStats() drops the stub return-Empty and instead reads the live pipeline.GetStats() outside the gate so a slow stats read can't serialize the controller's other operations.

WPF: MainViewModel runs a 1 Hz DispatcherTimer that pulls stats for every participant view-model and pushes them via UpdateStats(). ParticipantViewModel grows three displayable properties — FramesIn, FramesOut, IncomingResolution — bound into the participants DataGrid as a new 'Live' column showing the down/up frame counts and the source resolution underneath the machine name.

Tests: 74/74 unit + 9/9 NDI integration green; the existing round-trip integration test exercises the new wiring at runtime (live receiver/sender refs are set, frames flow, channels close cleanly).
2026-05-08 00:52:44 -04:00
f07aad1c6a ci(forgejo): release workflow on tag push -> MSI artifact + release asset
.forgejo/workflows/release.yml triggers on annotated tag pushes matching v*.*.*. The workflow runs on a Windows runner (required for WiX MSI), restores and builds the Windows solution filter, runs unit tests (skipping the requires=ndi tier — CI runners don't have NDI), publishes TeamsISO.App + TeamsISO.Console for win-x64 framework-dependent, builds the WiX MSI scaffold, and uploads the MSI both as a workflow artifact (downloadable from the run page) and as an asset on the auto-created Release for the tag.

Tag version is parsed from refs/tags/vX.Y.Z and threaded into /p:Version on every dotnet build invocation so the publish output, the assembly metadata, and the MSI ProductVersion all agree.

Release-asset upload uses the Forgejo REST API directly via curl + Invoke-RestMethod rather than depending on a third-party action; if the auto-create-release-on-tag-push setting is off, the workflow creates the release itself. Pre-release flag is set when the tag contains -alpha/-beta/-rc.

docs/RELEASING.md walks through cutting a release and flags the code-signing TODO (SignOutput property is wired but no cert; SmartScreen will warn on first launch until that lands).
2026-05-08 00:48:57 -04:00
1d85396a90 feat(logging): rolling file sink under %LOCALAPPDATA%\\TeamsISO\\Logs
Adds Serilog.Sinks.File to TeamsISO.Engine and a new EngineLogging.CreateDefault() factory that writes to BOTH the existing console sink and a rolling daily file at %LOCALAPPDATA%\\TeamsISO\\Logs\\teamsiso<date>.log. The WPF host (TeamsISO.exe is a WinExe with no console attached at runtime) now uses CreateDefault so support has something to ask for when users file an issue. The Console build keeps using CreateConsole — stdout is the right surface there and shell redirection beats a competing on-disk sink.

Files roll daily, cap at 10 MB before mid-day rollover, and only the most recent 14 are retained. Disk flush interval is 250 ms so a tail -f from another tool sees lines promptly. Path is announced via the first log line on every startup.

Two unit tests gate the wiring: AllLoggers_WriteToFile (verifies both typed and named CreateLogger() reach the file) and LogsAtBelowMinimumLevel_AreSuppressed (regression guard for level filtering). 74/74 unit tests pass (was 72).

Also adds a startup breadcrumb log line in App.OnStartup carrying the build version + PID so we can correlate a user's log file with a specific commit.
2026-05-08 00:47:25 -04:00
9891f2444d test(ndi): end-to-end pipeline round-trip with framerate normalization
Synthesizes a 640x360 cyan BGRA source named like a Teams participant, runs the production IsoPipeline against it (NdiReceiver -> FrameProcessor -> ManagedNearestNeighborFrameScaler -> NdiSender, all backed by NdiInteropPInvoke), connects a receiver to the resulting TEAMSISO_* output, and asserts that captured frames come back at the configured 1920x1080 target. Closes the loop on the receive/scale/emit chain that was previously only unit-tested in isolation against fakes.

If this test ever goes red we have a regression in something that actually matters: the runtime resolver, the parser-free pipeline construction path, the frame channel back-pressure, the scaler's pillarbox math, the sender clock, or the receiver-from-loopback path. Pinned at requires=ndi so default CI skips it; runs locally in ~780ms.
2026-05-08 00:39:23 -04:00
dae8f35db9 ci(forgejo): pin actions/upload-artifact to v3
Forgejo Actions runs in GHES compat mode and the bundled @actions/artifact toolkit only supports the v3 protocol. v4 fails with:

    GHESNotSupportedError: @actions/artifact v2.0.0+, upload-artifact@v4+ and download-artifact@v4+ are not currently supported on GHES.

Build, tests (72/72), and the 80% coverage gate (currently 86.7%) all pass; only the trailing artifact-upload steps were red, which fails the whole run. Pinning to v3 restores green.
2026-05-08 00:36:58 -04:00
d2c0c2159f feat(installer): WiX v5 MSI scaffold for Wild Dragon TeamsISO
Some checks failed
CI / build-and-test (push) Failing after 34s
Adds installer/TeamsISO.Installer.wixproj (WixToolset.Sdk 5.0.2 + WixToolset.UI.wixext) plus Package.wxs that produces a per-machine x64 MSI bundling the Release publish output of TeamsISO.App.

Layout: Program Files\\Wild Dragon\\TeamsISO\\, Start Menu shortcut under Programs\\Wild Dragon\\TeamsISO, ARP entry pointing at https://wilddragon.net for both help and about, NoRepair set so users uninstall+install for upgrades. MajorUpgrade is wired so upgrade-in-place from older versions works; downgrade is blocked with a friendly message.

NDI runtime presence is searched (HKLM environment NDI_RUNTIME_DIR_V6) and surfaced as the NDIRUNTIMEDIR property — the install no longer prompts via the deprecated VBScript custom action; instead the WPF app's existing first-run NDI check pops the install link dialog if the runtime is missing. Operators can stage the app before NDI rolls out.

Build:

    dotnet publish src/TeamsISO.App/TeamsISO.App.csproj -c Release -r win-x64 -o publish/TeamsISO

    dotnet build installer/TeamsISO.Installer.wixproj -c Release

Verified locally: MSI builds clean (0 warnings, 0 errors), produces TeamsISO-Setup-1.0.0-alpha.0.msi (336 KB), summary info reads correctly via WindowsInstaller COM API. Property table contains ARPHELPLINK/ARPURLINFOABOUT/Manufacturer/ProductName/UpgradeCode as expected.
2026-05-08 00:16:26 -04:00
0b24fbb529 test(ndi): seed requires=ndi integration tests against real NDI runtime
Replaces the previously-skipped placeholder with 8 integration tests that exercise the production P/Invoke shim against the installed NDI 6 runtime: runtime version probe + prefix assertion (catches future SDK rebrandings), finder lifecycle on default + custom groups (incl. whitespace tolerance + multi-group), sender lifecycle on default + custom groups, and a loopback-discovery test that creates a uniquely-named sender and asserts a same-process finder sees it within 5 s.

All marked [Trait('requires', 'ndi')] so the existing CI filter (Category!=ndi&requires!=ndi) excludes them. Run locally with: dotnet test --filter requires=ndi. Today: 8/8 pass against NDI 6.2 on Windows 11.
2026-05-08 00:11:01 -04:00
6cac486fbe feat(ui): rebrand to Wild Dragon + Microsoft Teams layout
Some checks failed
CI / build-and-test (push) Failing after 33s
Replaces the Stone theme with Wild Dragon branding (canvas #0A0A0A, accent cyan #97EDF0, secondary #9AE0FD, coral alert #FB819C — sourced from wilddragon.net) and reorganizes MainWindow into a Microsoft Teams-style three-column layout: a 72px left rail (logo + Participants/Settings nav + engine-status indicator), a center content area (header + participants card), and a right settings panel.

Adds InitialsConverter so participant avatars render real initials (Brendon Power -> 'BP', '(Local)' -> 'L') instead of a generic glyph. Drops the obsolete StoneTheme.xaml; the project now ships exactly one theme dictionary.

Typography: Inter (with Segoe UI Variable Display fallback) for the sans stack, JetBrains Mono (Cascadia Mono fallback) for machine names and timecodes — matching the wilddragon.net site.

Verified live against the running Teams meeting: app launches, participant 'Brendon Power' displays with avatar, settings panel surfaces NDI groups + Hide-Local toggle, engine status pill shows green/live.
2026-05-08 00:08:39 -04:00
53c06a9af9 feat(ui): single-instance enforcement via per-user named mutex
Two simultaneous TeamsISO processes contend over the NDI runtime, the same default sender names, and %APPDATA%\\TeamsISO\\config.json — observed during testing when launchers / shortcuts produced duplicate windows. Add a Local namespace per-user-keyed mutex (Local\\WildDragon.TeamsISO.SingleInstance.<username>) at startup; if a second instance can't claim it, broadcast a registered window message ('WildDragon.TeamsISO.BringToFront') and Shutdown(0). The running instance subscribes to that message via ComponentDispatcher.ThreadFilterMessage and surfaces its main window when received.

Per-user keying lets two different Windows users on the same machine each run their own TeamsISO. Mutex is released and disposed on OnExit.

Verified: Start-Process the exe twice in a row -> only one process remains, with the original window surfaced.
2026-05-07 23:59:47 -04:00
b542d01835 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
909237f454 feat(ndi): plumb NDI groups (discovery + output) through the engine
Adds an NdiGroupSettings record carrying optional comma-separated NDI group lists for the finder and the senders. Extends INdiInterop.CreateFinder / CreateSender with optional groups arguments and populates NDIlib_find_create_t.p_groups and NDIlib_send_create_t.p_groups via P/Invoke. IsoController reads the settings on construction, threads DiscoveryGroups into NdiDiscoveryService and OutputGroups into IsoPipelineConfig, and exposes SetGroupSettingsAsync for runtime updates (group changes apply on next process restart so live pipelines aren't orphaned).

This unblocks the 'transcoder' topology where Teams broadcasts NDI on a private group (e.g. teamsiso-input) and TeamsISO re-emits clean normalized streams on Public — keeping raw, wrong-framerate Teams sources off the production network.

EngineConfig schema is JSON-back-compat: existing config.json files (no NdiGroups field) deserialize with NdiGroups=null and load as NdiGroupSettings.Default. UI surface for these settings comes in a follow-up.

Tests: 72/72 passing (was 69) — added IsoController coverage that group settings are read from ConfigStore on startup, passed to the finder, threaded into per-pipeline config, and round-trip through SetGroupSettingsAsync/Save/Load.
2026-05-07 23:48:49 -04:00
fa8d2a8fad feat(console): add --list-sources diagnostic flag
Some checks failed
CI / build-and-test (push) Failing after 27s
Enumerates every raw NDI source string visible to the local NDI finder for ~5 seconds and prints unique ones, then exits. Bypasses NdiSourceParser so it surfaces sources that the parser would otherwise reject — useful for confirming the on-the-wire format Teams (or any other producer) is actually emitting on a given setup.

Used directly to diagnose the Teams 'MS Teams' vs. 'Teams' brand-prefix mismatch fixed in the previous commit; left in as a permanent debug tool for future setup issues.
2026-05-07 23:33:44 -04:00
ca124540a7 fix(parser): accept 'MS Teams' brand prefix from current Teams NDI broadcasts
The new Microsoft Teams desktop client (observed against a live meeting on Teams 26106.1906.4665.7308) emits NDI source strings of the form

    WOOGLIN (MS Teams - Brendon Power)

    WOOGLIN (MS Teams - (Local))

    WOOGLIN (MS Teams - Active Speaker)

rather than the legacy 'MACHINE (Teams - ...)' shape NdiSourceParser was written to. As a result every Teams source was rejected and TeamsISO showed zero participants in real meetings.

Refactor the parser to recognize 'Teams', 'MS Teams', and (defensively) 'Microsoft Teams' as brand prefixes — longest first so 'MS Teams' isn't shadowed. Also recognize reserved suffix tokens after a dash ('Active Speaker' / 'Audio' / 'Audio Mix' / 'Screen Share') so the new active-speaker output is correctly classified as ActiveSpeaker rather than misread as a participant named 'Active Speaker'.

Tests: kept all legacy cases, added MS Teams + Microsoft Teams variants and the new dash-prefixed reserved-suffix cases. 69/69 unit tests passing; verified end-to-end against a live Teams meeting where TeamsISO.exe now shows '(Local)' and 'Brendon Power' in the Participants DataGrid.
2026-05-07 23:33:43 -04:00
d90ebb826f fix(ndi): match the NDI 6 WIN64 runtime banner in version probe
Some checks failed
CI / build-and-test (push) Failing after 26s
Shipping NDI 6 reports its version as a build banner of the form

    NDI SDK WIN64 13:07:00 Jun  2 2025 6.2.0.3

not the 'NDI SDK for Windows v6.x.x.x' format the prefix constant assumed. As a result NdiRuntimeProbe raised a spurious mismatch alert on every supported install. Update ExpectedRuntimeVersionPrefix to 'NDI SDK WIN64' which is the stable architecture token in the new banner; the trailing four-part version remains available for a stricter major-version check if that becomes useful.
2026-05-07 15:15:03 -04:00
d14a33a0a3 fix(ndi): resolve Processing.NDI.Lib.x64 via NDI_RUNTIME_DIR_V6 env var
The NDI 6 installer sets NDI_RUNTIME_DIR_V6 (and V5/V4 for back-compat) but does not always add the runtime directory to PATH, so a default DllImport(Processing.NDI.Lib.x64) failed with 0x8007007E (DllNotFoundException) on otherwise correctly installed machines, killing both TeamsISO.exe and TeamsISO.Console at preflight.

Add NdiNativeLibraryResolver, registered from NdiNative's static ctor, that resolves the DLL by trying NDI_RUNTIME_DIR_V6 / V5 / V4 in order, NativeLibrary.Load-ing the file from disk before the runtime falls back to its default search algorithm. Static-ctor registration (rather than [ModuleInitializer]) sidesteps CA2255 under TreatWarningsAsErrors and still guarantees the resolver is in place before the first P/Invoke fires.
2026-05-07 15:14:54 -04:00
0f03c272ad fix(build): use forward-slash paths in TeamsISO.sln so Windows .slnf resolves
MSBuild on Windows compares solution-filter project paths against .sln entries as raw strings, so the forward-slash entries in TeamsISO.Windows.slnf / TeamsISO.Linux.slnf were being rejected (MSB5028) against the .sln's backslash entries. Linux dotnet normalizes separators so CI happened to be green. Switch the .sln to forward slashes so both platforms agree; Visual Studio accepts either form.
2026-05-07 15:14:42 -04:00
e3321ff279 feat(ui): wire DI bootstrap in App.xaml.cs and add Windows solution filter
Some checks failed
CI / build-and-test (push) Failing after 36s
2026-05-07 15:41:58 +00:00
d64b110550 feat(ui): add MainWindow XAML with participants DataGrid, settings sidebar, alert banner
Some checks failed
CI / build-and-test (push) Failing after 34s
2026-05-07 15:40:49 +00:00
8c441318d8 feat(ui): add MainViewModel with live participants collection and dispatcher marshalling
Some checks failed
CI / build-and-test (push) Failing after 34s
2026-05-07 15:40:06 +00:00
fbb73bcf04 feat(ui): add MVVM helpers and per-component view models (Participant, GlobalSettings, AlertBanner)
Some checks failed
CI / build-and-test (push) Has been cancelled
2026-05-07 15:39:46 +00:00
2c6fbdf861 docs: add Phase C WPF UI plan
Some checks failed
CI / build-and-test (push) Failing after 32s
2026-05-07 15:38:57 +00:00
c6f23e1885 docs: update plan backlog after Phase B-2 completion
Some checks failed
CI / build-and-test (push) Failing after 32s
2026-05-07 15:38:24 +00:00
368920734d fix(console): disambiguate System.Console from TeamsISO.Console namespace
Some checks failed
CI / build-and-test (push) Has been cancelled
2026-05-07 15:38:02 +00:00
b2dafb7aed feat(console): add TeamsISO.Console headless smoke runner
Some checks failed
CI / build-and-test (push) Has been cancelled
2026-05-07 15:37:44 +00:00
88841780af feat(pipeline): add managed BGRA nearest-neighbor scaler with aspect modes
Some checks failed
CI / build-and-test (push) Failing after 27s
2026-05-07 15:37:07 +00:00
af37b4d9e1 refactor(interop): NdiRuntimeProbe now matches by prefix to handle NDI runtime version strings
Some checks failed
CI / build-and-test (push) Failing after 24s
2026-05-07 15:36:26 +00:00
da5818b690 feat(interop): add NdiInteropPInvoke production INdiInterop implementation
Some checks failed
CI / build-and-test (push) Failing after 26s
2026-05-07 15:35:59 +00:00
90b0951a42 fix(interop): rename create-settings structs to avoid name collision with P/Invoke methods
Some checks failed
CI / build-and-test (push) Failing after 26s
2026-05-07 15:35:15 +00:00
6f09ca35ef feat(interop): add NDI 6 native bindings, handle types, and version constants
Some checks failed
CI / build-and-test (push) Has been cancelled
2026-05-07 15:34:54 +00:00
60b12eb637 docs: add Phase B-2 NDI interop plan
Some checks failed
CI / build-and-test (push) Failing after 27s
2026-05-07 15:34:11 +00:00
54cb4ac3d4 docs: update plan backlog after Phase B-1 completion
Some checks failed
CI / build-and-test (push) Failing after 30s
2026-05-07 15:28:52 +00:00
cd5e852a30 feat(controller): add IIsoController and IsoController implementation
Some checks failed
CI / build-and-test (push) Has been cancelled
2026-05-07 15:28:27 +00:00
49b6dfb9ed feat(pipeline): add IsoPipeline with lifecycle and restart supervisor
Some checks failed
CI / build-and-test (push) Failing after 29s
2026-05-07 15:26:54 +00:00
e318514202 feat(interop): add NdiRuntimeProbe with version-mismatch result
Some checks failed
CI / build-and-test (push) Failing after 27s
2026-05-07 15:24:31 +00:00
798a5abd64 feat(pipeline): add ExponentialBackoff policy
Some checks failed
CI / build-and-test (push) Has been cancelled
2026-05-07 15:24:13 +00:00
aecbda674d feat(pipeline): add NdiSender with channel-based input
Some checks failed
CI / build-and-test (push) Has been cancelled
2026-05-07 15:23:51 +00:00
ead5e79935 feat(pipeline): add NdiReceiver with channel-based output
Some checks failed
CI / build-and-test (push) Has been cancelled
2026-05-07 15:23:26 +00:00
f1513ddaf5 docs: add Phase B-1 pipeline-orchestration plan
Some checks failed
CI / build-and-test (push) Failing after 25s
2026-05-07 15:22:56 +00:00
38f7db888e feat(domain): default global framerate to 59.94p (user's primary production target)
Some checks failed
CI / build-and-test (push) Failing after 25s
2026-05-07 15:21:58 +00:00
381cac66e6 docs: add Phase A test playbook stub and plan backlog
Some checks failed
CI / build-and-test (push) Failing after 26s
2026-05-07 15:16:37 +00:00
27dc0f90c7 feat(logging): add EngineLogging.CreateConsole helper
Some checks failed
CI / build-and-test (push) Has been cancelled
2026-05-07 15:16:17 +00:00
f2b4e881e4 ci: enforce 80% line coverage gate on TeamsISO.Engine
Some checks failed
CI / build-and-test (push) Has been cancelled
2026-05-07 15:16:11 +00:00
5c039025fd feat(pipeline): add FrameProcessor with closest-frame timing and slate fallback
Some checks failed
CI / build-and-test (push) Failing after 24s
2026-05-07 15:15:19 +00:00
970f04861d feat(pipeline): add SolidFrameRenderer slate and IFrameScaler/PassthroughFrameScaler
Some checks failed
CI / build-and-test (push) Failing after 25s
2026-05-07 15:14:37 +00:00
1b280e3e77 feat(discovery): add NdiDiscoveryService with diff-based event emission
Some checks failed
CI / build-and-test (push) Failing after 22s
2026-05-07 15:14:15 +00:00
cef9018b6d feat(discovery): add ParticipantTracker with rename heuristic
Some checks failed
CI / build-and-test (push) Failing after 22s
2026-05-07 15:13:42 +00:00
c07a668672 test(fakes): add FakeNdiInterop and FakeFrameClock; feat(discovery): add DiscoveryEvent
Some checks failed
CI / build-and-test (push) Failing after 24s
2026-05-07 15:13:00 +00:00
f562303b47 feat(pipeline,interop): add RawFrame, ProcessedFrame, IFrameClock and INdiInterop test seam
Some checks failed
CI / build-and-test (push) Has been cancelled
2026-05-07 15:12:36 +00:00
3f8b5f1a7b feat(persistence): add ConfigStore with atomic JSON writes and corruption-safe load
Some checks failed
CI / build-and-test (push) Failing after 22s
2026-05-07 15:12:01 +00:00