Commit graph

9 commits

Author SHA1 Message Date
c53c7a7768 Wire engine audio peak metering — UI VU bars now animate
Some checks failed
CI / build-and-test (push) Failing after 29s
The DataGrid's per-row audio level bar (in the Live column) was inert because IsoHealthStats.PeakAudioLevel always returned 0.0. Engine work needed: capture NDI audio frames, compute peak amplitude, publish through the existing stats path.

Engine:

- AudioPeakComputer (new): max-abs computation across NDI's FLTP / FLT / PCM s16 sample formats. Pure managed code, fully unit-tested (14 cases — clamping behaviour, edge cases like short.MinValue overflow, totalSamples-vs-buffer mismatch defenses).

- INdiInterop.CaptureAudioPeak (new, default-implemented): polls one audio frame, returns peak in [0,1] or null on timeout. FakeNdiInterop inherits the no-op default; production NdiInteropPInvoke overrides with real FLTP decode through a sibling RecvCaptureV3Audio import + RecvFreeAudioV3.

- NdiNative: AudioFrameV3 struct + audio-only RecvCaptureV3 binding + FreeAudioV3.

- NdiReceiver: spins up a sibling audio-capture loop alongside the existing video loop on the same lifetime. Audio failures are caught + logged but never re-thrown (a misbehaving audio path must never tear down the live video pipeline). Latest peak published via Volatile<long> (BitConverter int64 bits) so UI reads are torn-free across threads.

- IsoPipeline.GetStats: surfaces NdiReceiver.LatestAudioPeak as IsoHealthStats.PeakAudioLevel.

UI:

- ParticipantViewModel.OnStatsTick already had the decay logic (max-of-new-or-decayed-old, 0.7 multiplier) waiting for real values. No UI changes needed.

Tests: 14 new + 141 existing = 155/155 passing. 0 warnings, 0 errors.
2026-05-10 13:28:26 -04:00
16e0a483e2 fix: address review findings on tonight's commits
All checks were successful
CI / build-and-test (push) Successful in 35s
Code review on d14a33a..bab29b0 turned up three real issues, fixed here.

1. EngineLogging.CreateDefault no longer mutates Serilog.Log.Logger. The static set was a belt-and-suspenders attempt to catch any code path that reaches for the singleton, but it doesn't matter (engine code uses ILogger<T>, never Serilog.Log.*) and it raced under xUnit's parallel test execution.

2. IsoPipeline stops holding a RawFrame reference for stats. The receiver-side TappedChannelWriter callback now snapshots only Width/Height into volatile ints — frame's pixel buffer is allowed to GC on its normal schedule and a late stats poll can never resurrect a dropped frame. (Today the buffer is fully managed so a use-after-free wasn't actually possible, but the snapshot pattern is the right ownership shape.)

3. App.xaml.cs's ComponentDispatcher.ThreadFilterMessage subscription now lives in a field and is unsubscribed in OnExit. Mutex release is gated on a new _ownsSingleInstanceMutex flag so the 'lost the race; shut down silently' path doesn't accidentally try to release a handle it never owned.

Plus a load-bearing comment in NdiInteropPInvoke.CreateFinder explaining why we free the UTF-8 group buffers right after the native call returns — same lifetime contract Phase B-2's CreateReceiver / CreateSender have always relied on; if it's wrong, those would fail too. The loopback discovery integration test would catch a regression.

Tests: 74/74 unit + 9/9 NDI integration green.
2026-05-08 01:01:00 -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
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
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
14cbc25b99 feat(interop): scaffold TeamsISO.Engine.NdiInterop project 2026-05-07 15:08:21 +00:00