feat(ui): Launch Teams rail button + spec for embedded-Teams roadmap
All checks were successful
CI / build-and-test (push) Successful in 40s

First step of Phase E.1 from the new spec at docs/superpowers/specs/2026-05-08-embedded-teams-orchestration.md: a third icon in the left rail launches the Microsoft Teams desktop client as a subprocess of TeamsISO so the operator doesn't have to leave the app to start a meeting.

Services/TeamsLauncher tries the ms-teams: URI first, falls back to %LOCALAPPDATA%\\Microsoft\\WindowsApps\\ms-teams.exe (new Teams), then the classic Update.exe handoff. On failure surfaces a friendly MessageBox with the install link.

The spec doc lays out the full three-phase roadmap (launcher -> window orchestration -> in-app meeting controls via Graph API or UIAutomation) and explicitly calls out what's out of scope (replacing Teams' media stack).

_NEXT.md updated to mark Phase D done and queue Phase E + remaining polish items (code-signing, Inter/JetBrains Mono font bundling, real Wild Dragon dragon-mark, drops counter, running-fps display).
This commit is contained in:
Zac Gaetano 2026-05-08 01:05:26 -04:00
parent 16e0a483e2
commit c08b90b0b2
5 changed files with 260 additions and 4 deletions

View file

@ -6,11 +6,38 @@
- **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`) — `NdiInteropPInvoke` against NDI 6 SDK, managed BGRA scaler, `TeamsISO.Console` headless smoke runner, `NdiVersion` constants.
- **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 — May 2026** (no tag) — see "Done in May 2026" below; covers the bug-fix triad that turned this from "won't even start" into "discovers real Teams participants in a real meeting", plus integration coverage and the brand/UX rebuild.
- **Phase D — WiX Installer** — WiX v5 MSI scaffold, Forgejo CI fix (artifact v3 pin), Forgejo release workflow on tag push.
## Next (Windows-only)
## Done in May 2026
1. **First end-to-end validation on Windows** — install NDI Runtime, clone the repo, build the full solution, run the integration tests against an NDI Test Pattern source, run the WPF app and validate against a real Teams meeting. Fix any issues found.
- Fixed `.sln` path-separator mismatch that broke `.slnf` filters on Windows.
- `NdiNativeLibraryResolver` resolves `Processing.NDI.Lib.x64.dll` via `NDI_RUNTIME_DIR_V6` so the engine starts on installs where the NDI dir isn't on PATH.
- `NdiVersion.ExpectedRuntimeVersionPrefix` updated to match the shipping NDI 6 banner format (`NDI SDK WIN64 ...`).
- `NdiSourceParser` accepts current Teams desktop's `MS Teams - <name>` brand format (plus legacy `Teams` and defensive `Microsoft Teams`).
- `--list-sources` diagnostic on `TeamsISO.Console`.
- NDI groups end-to-end (discovery + output) so the operator can confine Teams' raw broadcasts to a private group.
- Hide-(Local) toggle so the user's own self-preview doesn't pollute the participants list.
- Single-instance enforcement via per-user named Mutex with broadcast bring-to-front.
- WPF rebuilt around Wild Dragon brand × Microsoft Teams flush layout (left rail + chromeless title bar + caption controls + cyan accent + JetBrains Mono).
- `IsoHealthStats` wired end-to-end: live receiver/sender refs published from the inner pipeline, frame counters and source resolution displayed in a `Live` column on the participants DataGrid (1 Hz polled).
- Rolling daily file logging at `%LOCALAPPDATA%\TeamsISO\Logs\` via Serilog.Sinks.File.
- Real-NDI integration test tier (`requires=ndi`): runtime probe, finder/sender lifecycle on default + custom groups, loopback discovery, and a full pipeline frame round-trip that asserts 1920×1080 normalization.
- Forgejo CI is green (`actions/upload-artifact` pinned to v3 since Forgejo doesn't support v4).
- WiX v5 MSI scaffold + Forgejo release workflow on tag push (windows-latest runner; uploads MSI as both workflow artifact and release asset).
- Code-review pass + fixes (no static `Serilog.Log.Logger` mutation, frame-dimension snapshot instead of holding a `RawFrame` ref, ComponentDispatcher unsubscribe, Mutex-ownership flag).
- "Launch Teams" rail button (Phase E.1 starter, see `docs/superpowers/specs/2026-05-08-embedded-teams-orchestration.md`).
2. **Phase D — WiX Installer & Release** (Windows) — WiX v5 MSI installer detecting NDI Runtime, release pipeline triggering on tag push, code-signing decision implemented.
## Next
3. **Optional polish before v1.0** — system health indicators (CPU/GPU/network meters), per-stream framerate display, output thumbnail previews (deferred from v1.5 if useful), MaterialDesignThemes UI polish.
1. **Phase E — Embedded Teams orchestration** — see the spec at `docs/superpowers/specs/2026-05-08-embedded-teams-orchestration.md`. Three-phase rollout: launcher → window orchestration → in-app meeting controls. Phase E.1 partially shipped (launcher).
2. **Code-signing the MSI**`installer/TeamsISO.Installer.wixproj` has a `SignOutput` hook but no cert. For a v1.0 release, wire `signtool` invocation from a release-only CI secret. Until then SmartScreen will warn on first launch.
3. **Bundle Inter / JetBrains Mono fonts** so the typography doesn't depend on Windows fallbacks. Today the WPF UI uses Segoe UI Variable Display + Cascadia Mono as fallbacks.
4. **Wild Dragon dragon-mark** — the rail logo is a stylized "W" placeholder; swap in the real dragon SVG when available.
5. **Optional polish before v1.0** — running incoming-fps display (today the field on `IsoHealthStats` is 0), per-pipeline output thumbnail previews, MaterialDesignThemes-equivalent transitions, system health (CPU/network) meters in the rail.
6. **Drops counter on `IsoHealthStats`**`FrameProcessor` doesn't currently surface drops or duplicates; wire those through so the Live column can show "↓ N (dropped M)".

View file

@ -0,0 +1,106 @@
# Spec: Embedded Teams meeting orchestration
**Status:** Draft. Authored 2026-05-08.
## Problem
Operators currently run two apps side by side: Microsoft Teams (which broadcasts
NDI from its meetings) and TeamsISO (which consumes those NDI sources, normalizes
them, and re-emits clean ISOs). Two issues fall out:
1. **Two interfaces, one workflow.** Switching between Teams to drive the meeting
and TeamsISO to drive ISO routing is friction during a live show.
2. **Teams' raw NDI bleeds into the production network.** Even with
TeamsISO running, Teams broadcasts its at-source-resolution / at-source-framerate
feeds on the same `Public` NDI group that switchers and recorders subscribe to.
Operators see "garbage" NDI sources alongside the clean TeamsISO outputs unless
they manually configure NDI groups (which most don't).
The user's stated north star: **let me host the meeting from inside TeamsISO. Run
Teams in the background. Show me one interface; expose only the proper outputs.**
## Constraints
- Microsoft Teams' NDI broadcast feature is desktop-only — the web client does not
broadcast NDI. We cannot replace Teams with a WebView2 view of `teams.microsoft.com`.
- We do not have a native Teams SDK. The Microsoft Graph API exposes some meeting
control (create/join/end), but in-call operations (mute, share, react) are largely
out of scope or behind enterprise tenant configuration.
- Win32 window embedding (`SetParent`) of a foreign process's window is technically
possible but produces a fragile UX — Teams will break out, render incorrectly, or
fail to honor parent-window inputs.
- NDI group routing is the standard primitive for hiding noisy producers. We
shipped this in commit `909237f`. It works.
## Architecture
A three-phase rollout. Each phase is shippable on its own.
### Phase E.1 — Teams launcher (launches Teams as a subprocess)
The minimum viable embed. TeamsISO grows a "Launch Teams" affordance on the rail.
Clicking it:
1. Reads the global `NdiGroupSettings.DiscoveryGroups` from `EngineConfig`. If
empty, defaults to `teamsiso-input`.
2. Opens **NDI Access Manager** (or programmatically writes its config) so Teams
broadcasts on `teamsiso-input` rather than `Public`.
3. Launches `ms-teams:` URI (or the `MSTeams.exe` directly for the new client) in
the background.
4. Marks Teams as "owned by TeamsISO" — the rail icon flips to "Stop Teams"; on
click, sends WM_CLOSE to the Teams main window.
5. Surfaces meeting health in the existing engine-status pill (e.g. "Teams running
• 2 participants").
Implementation effort: **a few hours.** Pure WPF + ProcessStartInfo + a small
NdiAccessManagerHelper that reads/writes Teams' config.
### Phase E.2 — Window orchestration
Teams' main window is repositioned + minimized when launched, so the user's
foreground experience is the TeamsISO window. Optional:
- Pin Teams to a hidden virtual desktop with `IVirtualDesktopManager`.
- Forward keyboard shortcuts (mute, camera, share) from TeamsISO into Teams via
`SendInput` while Teams' window is hidden.
Implementation effort: **a day.** Mostly Win32 plumbing.
### Phase E.3 — Meeting controls in TeamsISO's UI
A "Meeting" panel in the left rail that shows the active call's participant list
(with their mute / video state) and exposes Join/Leave/Mute/Share controls. Two
ways to plumb this:
- **Microsoft Graph API for the chrome.** Auth as the user via OAuth (interactive
device-code flow), poll `/me/onlineMeetings` for active meetings, render in
TeamsISO's UI. In-call mute/cam state is not exposed via Graph as of writing —
Phase E.3 would surface participant *presence* but not mic/cam controls.
- **Teams' UI Automation tree.** Walk Teams' window with `UIAutomation` to read
call state. Brittle but usable; what other "Teams remote" tools do.
Implementation effort: **a week per route.** Recommend Graph for read paths,
UIAutomation for write paths.
## Out of scope (for now)
- Hosting the actual meeting media stack (audio/video render, mixer, network).
Teams owns this and we don't want to.
- Replacing Teams entirely with our own SIP/WebRTC stack. That's a different
product.
## Decision required
User to confirm:
1. Phase E.1 first (just launcher + group routing). Yes/no.
2. Whether to use `ms-teams:` URI launch or the new MSTeams.exe binary path
(`%LOCALAPPDATA%\Microsoft\WindowsApps\ms-teams.exe`).
3. Whether to ship NDI Access Manager config writes, or just document the manual
steps and trust the user to set them once.
## Implementation log
- 2026-05-08: First version of this spec drafted while user is asleep.
- 2026-05-08: Phase E.1 partial — "Launch Teams" rail button shipped (commit
pending). Group-routing automation deferred until user confirms approach.

View file

@ -103,6 +103,21 @@
</Grid>
</Button>
<!-- Nav: Launch Teams. Subprocess-launches the MS Teams desktop client. -->
<Button DockPanel.Dock="Top"
Style="{StaticResource Wd.Button.RailIcon}"
Click="OnLaunchTeamsClick"
ToolTip="Launch Microsoft Teams">
<!-- Stylized 'video meeting' camera icon -->
<Path Data="M 4,8 L 16,8 L 16,16 L 4,16 Z M 16,11 L 22,8 L 22,16 L 16,13 Z"
Stroke="{DynamicResource Wd.Text.Secondary}"
StrokeThickness="1.6"
Fill="Transparent"
StrokeLineJoin="Round"
Width="22" Height="20"
Stretch="Uniform"/>
</Button>
<!-- Nav: Settings (placeholder — opens panel on right; not toggled in this build) -->
<Button DockPanel.Dock="Top"
Style="{StaticResource Wd.Button.RailIcon}"

View file

@ -1,5 +1,6 @@
using System.Windows;
using System.Windows.Shapes;
using TeamsISO.App.Services;
using TeamsISO.App.ViewModels;
namespace TeamsISO.App;
@ -28,6 +29,23 @@ public partial class MainWindow : Window
/// <summary>Custom close button.</summary>
private void OnClose(object sender, RoutedEventArgs e) => Close();
/// <summary>
/// First step toward the Embedded-Teams roadmap (Phase E.1) — launches the MS
/// Teams desktop client as a subprocess so the operator doesn't have to switch
/// apps to start a meeting.
/// </summary>
private void OnLaunchTeamsClick(object sender, RoutedEventArgs e)
{
if (!TeamsLauncher.TryLaunch(out var error))
{
MessageBox.Show(
$"Could not launch Microsoft Teams.\n\n{error}",
"TeamsISO — Launch Teams",
MessageBoxButton.OK,
MessageBoxImage.Warning);
}
}
/// <summary>
/// Swap the maximize-button glyph between the "single rectangle" (when normal) and the
/// "two-overlapping-rectangles" (when maximized) variants, matching the Windows 11

View file

@ -0,0 +1,90 @@
using System.Diagnostics;
using System.IO;
namespace TeamsISO.App.Services;
/// <summary>
/// Small Win32 wrapper that launches the Microsoft Teams desktop client as a
/// subprocess of TeamsISO. First step toward Phase E.1 of the embedded-Teams
/// roadmap (see docs/superpowers/specs/2026-05-08-embedded-teams-orchestration.md):
/// the operator can launch Teams from within TeamsISO so they don't have to
/// switch apps to start a meeting.
///
/// The launcher tries (in order):
/// 1. ms-teams: URI (works for both classic and new Teams)
/// 2. MSTeams.exe in %LOCALAPPDATA%\Microsoft\WindowsApps\
/// 3. Teams.exe in %LOCALAPPDATA%\Microsoft\Teams\Update.exe (legacy)
///
/// Group-routing automation (writing NDI Access Manager config so Teams
/// broadcasts on a private group) is deferred to a follow-up — for v1.0 we
/// document the manual steps in RELEASING.md and trust the operator to set
/// them once per machine.
/// </summary>
public static class TeamsLauncher
{
/// <summary>
/// Launches Teams. Returns true if a launch was started successfully (the
/// process may take a few seconds to actually appear). False if every
/// fallback path failed.
/// </summary>
public static bool TryLaunch(out string? errorMessage)
{
errorMessage = null;
// Path 1: URI scheme. The shell handler picks whichever Teams client
// is registered (new MSTeams.exe takes priority on modern Windows).
if (TryStart("ms-teams:", useShell: true)) return true;
// Path 2: new Teams' WindowsApps shim.
var newTeams = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"Microsoft", "WindowsApps", "ms-teams.exe");
if (File.Exists(newTeams) && TryStart(newTeams, useShell: false)) return true;
// Path 3: classic Teams Update.exe with --processStart hands off to
// the actual Teams.exe via Squirrel.
var classicUpdater = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"Microsoft", "Teams", "Update.exe");
if (File.Exists(classicUpdater))
{
try
{
Process.Start(new ProcessStartInfo
{
FileName = classicUpdater,
Arguments = "--processStart \"Teams.exe\"",
UseShellExecute = false,
CreateNoWindow = true,
});
return true;
}
catch (Exception ex)
{
errorMessage = ex.Message;
}
}
errorMessage ??= "No Microsoft Teams installation was found. Install Teams from https://www.microsoft.com/microsoft-teams and try again.";
return false;
}
private static bool TryStart(string target, bool useShell)
{
try
{
var info = new ProcessStartInfo
{
FileName = target,
UseShellExecute = useShell,
CreateNoWindow = true,
};
Process.Start(info);
return true;
}
catch
{
return false;
}
}
}