2026-05-08 00:16:26 -04:00
|
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
|
|
|
<!--
|
|
|
|
|
TeamsISO — MSI installer (WiX v5)
|
|
|
|
|
|
|
|
|
|
Produces: TeamsISO-Setup-<Version>.msi (per-machine install).
|
|
|
|
|
|
|
|
|
|
Build:
|
|
|
|
|
dotnet publish src/TeamsISO.App/TeamsISO.App.csproj -c Release -r win-x64 (no self contained) -o publish/TeamsISO
|
|
|
|
|
dotnet build installer/TeamsISO.Installer.wixproj -c Release
|
|
|
|
|
|
|
|
|
|
Runtime expectations:
|
|
|
|
|
- .NET 8 Desktop runtime present on target (framework-dependent build)
|
|
|
|
|
- NDI 6 Runtime present — checked in CheckNdiRuntime; absence WARNS
|
|
|
|
|
but does not block install (operators can install NDI after the app)
|
|
|
|
|
-->
|
|
|
|
|
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs"
|
|
|
|
|
xmlns:ui="http://wixtoolset.org/schemas/v4/wxs/ui">
|
|
|
|
|
|
|
|
|
|
<Package Name="TeamsISO"
|
|
|
|
|
Manufacturer="Wild Dragon LLC"
|
|
|
|
|
Version="1.0.0.0"
|
|
|
|
|
UpgradeCode="9F4A8B2C-1D3E-4A5B-9C6D-8E7F0A1B2C3D"
|
|
|
|
|
Scope="perMachine"
|
|
|
|
|
Compressed="yes"
|
|
|
|
|
InstallerVersion="500">
|
|
|
|
|
|
|
|
|
|
<SummaryInformation Description="TeamsISO — Per-Participant NDI ISO Controller for Microsoft Teams"
|
|
|
|
|
Manufacturer="Wild Dragon LLC" />
|
|
|
|
|
|
|
|
|
|
<!--
|
|
|
|
|
MajorUpgrade: a newer install replaces an older one in-place.
|
|
|
|
|
Disallow downgrades; users should uninstall the newer first.
|
|
|
|
|
-->
|
|
|
|
|
<MajorUpgrade DowngradeErrorMessage="A newer version of TeamsISO is already installed. Uninstall it before installing this older version."
|
|
|
|
|
Schedule="afterInstallInitialize" />
|
|
|
|
|
|
|
|
|
|
<!--
|
|
|
|
|
Single MSI feature; users see only the install/uninstall screens.
|
|
|
|
|
-->
|
|
|
|
|
<Feature Id="Main" Title="TeamsISO" Level="1">
|
|
|
|
|
<ComponentGroupRef Id="ApplicationFiles" />
|
|
|
|
|
<ComponentGroupRef Id="Shortcuts" />
|
fix: cold-start discovery + installer shortcuts + single-instance hardening
Three independent fixes bundled because all were chasing the same operator
report: 'I just installed, launched from the shortcut, no participants.'
1) NdiDiscoveryService: poll immediately, then ramp from 200ms to the
configured interval over the first 3 seconds. PeriodicTimer.WaitForNext-
TickAsync waits the full interval before its first tick, so for a 500ms
discovery interval the operator stared at 'no ndi sources yet' for half
a second on every cold start. Force-poll up front (catches the runtime
cache), then run a fast inner loop for ~3s while mDNS replies trickle
in. Both loops share a try/finally so the NDI finder is always disposed.
2) MainViewModel.IsDiscovering: new boolean, true for 8s after engine start
AS LONG AS no participants have arrived. MainWindow.xaml swaps the
empty-state copy on this binding:
IsDiscovering=true -> 'scanning for ndi sources...' (cyan dot)
IsDiscovering=false -> 'no ndi sources visible -- is teams in a
meeting?' + Refresh CTA
The old copy ('no ndi sources yet -- open teams and start a meeting')
was being shown immediately at launch even when discovery just hadn't
run yet, making the app look broken.
3) App.xaml.cs: single-instance mutex moved from Local\ to Global\. On
admin-user boxes with UAC disabled, launches from different parents
(elevated File Explorer, non-elevated shell, etc.) can land in slightly
different security contexts and a Local\ name can be invisible to the
sibling. Global\ namespace closes that hole — both processes see the
same mutex regardless of integrity. Belt-and-braces against future
dual-instance file/port contention.
4) installer/Package.wxs: add a Desktop shortcut component (per-machine
feature, HKCU keypath per ICE38/ICE43). Operators who can't find the
Start Menu entry get the Desktop icon. Both shortcuts target the
installed exe, NOT a stale path under publish/.
2026-05-16 11:23:19 -04:00
|
|
|
<ComponentGroupRef Id="DesktopShortcut" />
|
2026-05-08 00:16:26 -04:00
|
|
|
<ComponentGroupRef Id="ArpEntry" />
|
|
|
|
|
</Feature>
|
|
|
|
|
|
|
|
|
|
<!--
|
|
|
|
|
Friendly install UI. WixToolset.UI.wixext provides several flavors;
|
|
|
|
|
WixUI_InstallDir lets the user pick the directory.
|
|
|
|
|
-->
|
|
|
|
|
<ui:WixUI Id="WixUI_InstallDir" />
|
|
|
|
|
<Property Id="WIXUI_INSTALLDIR" Value="INSTALLFOLDER" />
|
|
|
|
|
|
|
|
|
|
<!--
|
|
|
|
|
ARP icon + about-box link.
|
|
|
|
|
-->
|
|
|
|
|
<Property Id="ARPHELPLINK" Value="https://wilddragon.net" />
|
|
|
|
|
<Property Id="ARPURLINFOABOUT" Value="https://wilddragon.net" />
|
|
|
|
|
<!-- ARPNOMODIFY is set by WixUI_InstallDir; don't redeclare. -->
|
|
|
|
|
<Property Id="ARPNOREPAIR" Value="1" />
|
|
|
|
|
|
feat: app icon, FPS, drops counter, --version, About dialog, Stop Teams toggle
Six related polish items, all building on tonight's groundwork.
1. App icon: teamsiso.ico generated from dragon-mark.png at 7 sizes (16-256), wired as ApplicationIcon in the WPF csproj, MainWindow.Icon, AboutWindow.Icon, and ARPPRODUCTICON in the WiX MSI. Taskbar / window / Add-Remove-Programs all show the dragon mark now.
2. Running incoming FPS: ring buffer of last 30 frame timestamps in IsoPipeline; ComputeFps() returns moving-average rate. Surfaced on IsoHealthStats.IncomingFps and shown in the Source column of the participants DataGrid as 'WxH · 59.94 fps'. Resets cleanly on every supervisor restart.
3. Drops counter: FrameProcessor.Stats already aggregated FramesDropped (closest-frame strategy when the receiver outpaces the processor) and FramesDuplicated; just plumbed _liveProcessor through IsoPipeline so GetStats() can read them. Exposed in the Live column under the in/out counters as a coral-tinted 'drop N'.
4. Console --version flag: prints engine version (with embedded git SHA), .NET version, OS, NDI runtime banner, expected prefix, exit-code legend, plus a wilddragon.net link. Useful for support tickets.
5. About dialog: chromeless modal with the dragon mark + version / .NET / OS / NDI runtime fields and a link to wilddragon.net. Triggered by clicking the rail logo.
6. Teams launcher Stop toggle: TeamsLauncher gains IsRunning() and StopAll(). The rail's Teams button now toggles — if Teams is up, ask to close all Teams windows via WM_CLOSE; otherwise launch as before. Confirms before stopping so we don't kill the user's call mid-transition.
Tests: 74/74 unit + 9/9 NDI integration green throughout. MSI builds clean and now embeds the dragon icon for ARP.
2026-05-08 13:50:19 -04:00
|
|
|
<!--
|
|
|
|
|
ARP icon — references the same .ico the WPF host uses. WiX requires the
|
|
|
|
|
icon resource to live next to the wxs OR be reachable at build time;
|
|
|
|
|
we point at the published copy under src/TeamsISO.App/Assets so the icon
|
|
|
|
|
embedded in the MSI matches the icon in the running exe.
|
|
|
|
|
-->
|
|
|
|
|
<Icon Id="TeamsISOIcon" SourceFile="$(var.AssetsDir)teamsiso.ico" />
|
|
|
|
|
<Property Id="ARPPRODUCTICON" Value="TeamsISOIcon" />
|
|
|
|
|
|
2026-05-08 00:16:26 -04:00
|
|
|
<!--
|
|
|
|
|
NDI Runtime detection. We check for NDI_RUNTIME_DIR_V6 in the system
|
|
|
|
|
environment block. Missing → warn during install, don't block. The
|
|
|
|
|
engine surfaces a clear MessageBox with an install-NDI link at first
|
|
|
|
|
launch if the runtime really isn't there.
|
|
|
|
|
-->
|
|
|
|
|
<Property Id="NDIRUNTIMEDIR" Value="0">
|
|
|
|
|
<RegistrySearch Id="NdiRuntimeDirV6Search"
|
|
|
|
|
Root="HKLM"
|
|
|
|
|
Key="SYSTEM\CurrentControlSet\Control\Session Manager\Environment"
|
|
|
|
|
Name="NDI_RUNTIME_DIR_V6"
|
|
|
|
|
Type="raw" />
|
|
|
|
|
</Property>
|
|
|
|
|
|
|
|
|
|
<!--
|
|
|
|
|
NDI runtime detection is surfaced at first app launch (App.xaml.cs pops a
|
|
|
|
|
MessageBox with an install link). We deliberately don't block install on
|
|
|
|
|
a missing runtime so admins can stage the app before NDI is rolled out.
|
|
|
|
|
VBScript-based install-time prompts are deprecated in WiX v5 / Windows
|
|
|
|
|
and rewriting in C++ is overkill for a soft warning.
|
|
|
|
|
-->
|
|
|
|
|
|
|
|
|
|
<!--
|
|
|
|
|
Install layout under Program Files\Wild Dragon\TeamsISO.
|
|
|
|
|
-->
|
|
|
|
|
<StandardDirectory Id="ProgramFiles64Folder">
|
|
|
|
|
<Directory Id="ManufacturerFolder" Name="Wild Dragon">
|
|
|
|
|
<Directory Id="INSTALLFOLDER" Name="TeamsISO" />
|
|
|
|
|
</Directory>
|
|
|
|
|
</StandardDirectory>
|
|
|
|
|
|
|
|
|
|
<StandardDirectory Id="ProgramMenuFolder">
|
|
|
|
|
<Directory Id="WildDragonStartMenuFolder" Name="Wild Dragon" />
|
|
|
|
|
</StandardDirectory>
|
|
|
|
|
|
|
|
|
|
<!--
|
|
|
|
|
Files: harvested from the publish output dir at build time.
|
|
|
|
|
WiX v5 understands <Files Include="..."> with glob patterns and
|
|
|
|
|
synthesizes one Component per file with stable GUIDs.
|
|
|
|
|
-->
|
|
|
|
|
<ComponentGroup Id="ApplicationFiles" Directory="INSTALLFOLDER">
|
|
|
|
|
<Files Include="$(var.PublishDir)**" />
|
|
|
|
|
</ComponentGroup>
|
|
|
|
|
|
|
|
|
|
<!--
|
2026-05-16 11:43:54 -04:00
|
|
|
Start Menu and Desktop shortcuts. The Target is intentionally
|
|
|
|
|
runas.exe (NOT TeamsISO.exe directly) with the /trustlevel:0x20000
|
|
|
|
|
argument. This drops the spawned TeamsISO to MEDIUM integrity even
|
|
|
|
|
when the shortcut is invoked from an elevated File Explorer.
|
|
|
|
|
|
|
|
|
|
Why: on admin-user boxes with UAC effectively disabled, double-
|
|
|
|
|
clicking the .lnk has explorer.exe as the spawner — and when the
|
|
|
|
|
spawned TeamsISO inherits explorer's elevated token, NDI Find
|
|
|
|
|
returns zero discovered sources (suspected window-station / desktop
|
|
|
|
|
handle inheritance quirk in NDI's mDNS layer; not reproducible from
|
|
|
|
|
any other parent). Wrapping with /trustlevel:0x20000 forces the
|
|
|
|
|
child to a restricted medium-integrity token regardless of the
|
|
|
|
|
invoking shell's level. Empirically verified to make discovery
|
|
|
|
|
succeed on the dev box where the bug first surfaced.
|
|
|
|
|
|
|
|
|
|
The /trustlevel:0x20000 magic number is the Windows "Basic User"
|
|
|
|
|
SAFER trust level — documented at
|
|
|
|
|
https://learn.microsoft.com/windows/security/identity-protection/windows-credential-theft-mitigation-guide-appendix#runas
|
|
|
|
|
|
|
|
|
|
Icon points at TeamsISO.exe (not runas) so the shortcut still shows
|
|
|
|
|
the app's icon, and WindowStyle=Minimized hides the brief runas
|
|
|
|
|
console flash.
|
2026-05-08 00:16:26 -04:00
|
|
|
-->
|
|
|
|
|
<ComponentGroup Id="Shortcuts" Directory="WildDragonStartMenuFolder">
|
|
|
|
|
<Component Id="StartMenuShortcut" Guid="*">
|
|
|
|
|
<Shortcut Id="StartMenuTeamsISO"
|
|
|
|
|
Name="TeamsISO"
|
|
|
|
|
Description="Per-Participant NDI ISO Controller for Microsoft Teams"
|
2026-05-16 11:43:54 -04:00
|
|
|
Target="[SystemFolder]runas.exe"
|
|
|
|
|
Arguments="/trustlevel:0x20000 "[INSTALLFOLDER]TeamsISO.exe""
|
|
|
|
|
WorkingDirectory="INSTALLFOLDER"
|
|
|
|
|
Icon="TeamsISOIcon"
|
|
|
|
|
Show="minimized" />
|
2026-05-08 00:16:26 -04:00
|
|
|
<!-- Required by ICE64: Start Menu folder must be cleaned on uninstall. -->
|
|
|
|
|
<RemoveFolder Id="RemoveWildDragonStartMenuFolder"
|
|
|
|
|
Directory="WildDragonStartMenuFolder"
|
|
|
|
|
On="uninstall" />
|
|
|
|
|
<RegistryValue Root="HKCU"
|
|
|
|
|
Key="Software\Wild Dragon\TeamsISO"
|
|
|
|
|
Name="StartMenuShortcut"
|
|
|
|
|
Type="integer"
|
|
|
|
|
Value="1"
|
|
|
|
|
KeyPath="yes" />
|
|
|
|
|
</Component>
|
|
|
|
|
</ComponentGroup>
|
|
|
|
|
|
fix: cold-start discovery + installer shortcuts + single-instance hardening
Three independent fixes bundled because all were chasing the same operator
report: 'I just installed, launched from the shortcut, no participants.'
1) NdiDiscoveryService: poll immediately, then ramp from 200ms to the
configured interval over the first 3 seconds. PeriodicTimer.WaitForNext-
TickAsync waits the full interval before its first tick, so for a 500ms
discovery interval the operator stared at 'no ndi sources yet' for half
a second on every cold start. Force-poll up front (catches the runtime
cache), then run a fast inner loop for ~3s while mDNS replies trickle
in. Both loops share a try/finally so the NDI finder is always disposed.
2) MainViewModel.IsDiscovering: new boolean, true for 8s after engine start
AS LONG AS no participants have arrived. MainWindow.xaml swaps the
empty-state copy on this binding:
IsDiscovering=true -> 'scanning for ndi sources...' (cyan dot)
IsDiscovering=false -> 'no ndi sources visible -- is teams in a
meeting?' + Refresh CTA
The old copy ('no ndi sources yet -- open teams and start a meeting')
was being shown immediately at launch even when discovery just hadn't
run yet, making the app look broken.
3) App.xaml.cs: single-instance mutex moved from Local\ to Global\. On
admin-user boxes with UAC disabled, launches from different parents
(elevated File Explorer, non-elevated shell, etc.) can land in slightly
different security contexts and a Local\ name can be invisible to the
sibling. Global\ namespace closes that hole — both processes see the
same mutex regardless of integrity. Belt-and-braces against future
dual-instance file/port contention.
4) installer/Package.wxs: add a Desktop shortcut component (per-machine
feature, HKCU keypath per ICE38/ICE43). Operators who can't find the
Start Menu entry get the Desktop icon. Both shortcuts target the
installed exe, NOT a stale path under publish/.
2026-05-16 11:23:19 -04:00
|
|
|
<StandardDirectory Id="DesktopFolder" />
|
|
|
|
|
<ComponentGroup Id="DesktopShortcut" Directory="DesktopFolder">
|
|
|
|
|
<Component Id="DesktopShortcutComponent" Guid="*">
|
|
|
|
|
<Shortcut Id="DesktopTeamsISO"
|
|
|
|
|
Name="TeamsISO"
|
|
|
|
|
Description="Per-Participant NDI ISO Controller for Microsoft Teams"
|
2026-05-16 11:43:54 -04:00
|
|
|
Target="[SystemFolder]runas.exe"
|
|
|
|
|
Arguments="/trustlevel:0x20000 "[INSTALLFOLDER]TeamsISO.exe""
|
fix: cold-start discovery + installer shortcuts + single-instance hardening
Three independent fixes bundled because all were chasing the same operator
report: 'I just installed, launched from the shortcut, no participants.'
1) NdiDiscoveryService: poll immediately, then ramp from 200ms to the
configured interval over the first 3 seconds. PeriodicTimer.WaitForNext-
TickAsync waits the full interval before its first tick, so for a 500ms
discovery interval the operator stared at 'no ndi sources yet' for half
a second on every cold start. Force-poll up front (catches the runtime
cache), then run a fast inner loop for ~3s while mDNS replies trickle
in. Both loops share a try/finally so the NDI finder is always disposed.
2) MainViewModel.IsDiscovering: new boolean, true for 8s after engine start
AS LONG AS no participants have arrived. MainWindow.xaml swaps the
empty-state copy on this binding:
IsDiscovering=true -> 'scanning for ndi sources...' (cyan dot)
IsDiscovering=false -> 'no ndi sources visible -- is teams in a
meeting?' + Refresh CTA
The old copy ('no ndi sources yet -- open teams and start a meeting')
was being shown immediately at launch even when discovery just hadn't
run yet, making the app look broken.
3) App.xaml.cs: single-instance mutex moved from Local\ to Global\. On
admin-user boxes with UAC disabled, launches from different parents
(elevated File Explorer, non-elevated shell, etc.) can land in slightly
different security contexts and a Local\ name can be invisible to the
sibling. Global\ namespace closes that hole — both processes see the
same mutex regardless of integrity. Belt-and-braces against future
dual-instance file/port contention.
4) installer/Package.wxs: add a Desktop shortcut component (per-machine
feature, HKCU keypath per ICE38/ICE43). Operators who can't find the
Start Menu entry get the Desktop icon. Both shortcuts target the
installed exe, NOT a stale path under publish/.
2026-05-16 11:23:19 -04:00
|
|
|
WorkingDirectory="INSTALLFOLDER"
|
2026-05-16 11:43:54 -04:00
|
|
|
Icon="TeamsISOIcon"
|
|
|
|
|
Show="minimized" />
|
fix: cold-start discovery + installer shortcuts + single-instance hardening
Three independent fixes bundled because all were chasing the same operator
report: 'I just installed, launched from the shortcut, no participants.'
1) NdiDiscoveryService: poll immediately, then ramp from 200ms to the
configured interval over the first 3 seconds. PeriodicTimer.WaitForNext-
TickAsync waits the full interval before its first tick, so for a 500ms
discovery interval the operator stared at 'no ndi sources yet' for half
a second on every cold start. Force-poll up front (catches the runtime
cache), then run a fast inner loop for ~3s while mDNS replies trickle
in. Both loops share a try/finally so the NDI finder is always disposed.
2) MainViewModel.IsDiscovering: new boolean, true for 8s after engine start
AS LONG AS no participants have arrived. MainWindow.xaml swaps the
empty-state copy on this binding:
IsDiscovering=true -> 'scanning for ndi sources...' (cyan dot)
IsDiscovering=false -> 'no ndi sources visible -- is teams in a
meeting?' + Refresh CTA
The old copy ('no ndi sources yet -- open teams and start a meeting')
was being shown immediately at launch even when discovery just hadn't
run yet, making the app look broken.
3) App.xaml.cs: single-instance mutex moved from Local\ to Global\. On
admin-user boxes with UAC disabled, launches from different parents
(elevated File Explorer, non-elevated shell, etc.) can land in slightly
different security contexts and a Local\ name can be invisible to the
sibling. Global\ namespace closes that hole — both processes see the
same mutex regardless of integrity. Belt-and-braces against future
dual-instance file/port contention.
4) installer/Package.wxs: add a Desktop shortcut component (per-machine
feature, HKCU keypath per ICE38/ICE43). Operators who can't find the
Start Menu entry get the Desktop icon. Both shortcuts target the
installed exe, NOT a stale path under publish/.
2026-05-16 11:23:19 -04:00
|
|
|
<RegistryValue Root="HKCU"
|
|
|
|
|
Key="Software\Wild Dragon\TeamsISO"
|
|
|
|
|
Name="DesktopShortcut"
|
|
|
|
|
Type="integer"
|
|
|
|
|
Value="1"
|
|
|
|
|
KeyPath="yes" />
|
|
|
|
|
</Component>
|
|
|
|
|
</ComponentGroup>
|
|
|
|
|
|
2026-05-08 00:16:26 -04:00
|
|
|
<!--
|
|
|
|
|
ARP icon registry entry. Optional — the MSI auto-fills most ARP
|
|
|
|
|
fields from the Package element. We only need to point at the
|
|
|
|
|
executable for the ARP icon.
|
|
|
|
|
-->
|
|
|
|
|
<ComponentGroup Id="ArpEntry" Directory="INSTALLFOLDER">
|
|
|
|
|
<Component Id="ArpIconRegistry" Guid="*">
|
|
|
|
|
<RegistryValue Root="HKLM"
|
|
|
|
|
Key="Software\Wild Dragon\TeamsISO"
|
|
|
|
|
Name="InstallPath"
|
|
|
|
|
Type="string"
|
|
|
|
|
Value="[INSTALLFOLDER]"
|
|
|
|
|
KeyPath="yes" />
|
|
|
|
|
</Component>
|
|
|
|
|
</ComponentGroup>
|
|
|
|
|
|
|
|
|
|
</Package>
|
|
|
|
|
</Wix>
|