feat(ui): Launch Teams rail button + spec for embedded-Teams roadmap
All checks were successful
CI / build-and-test (push) Successful in 40s
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:
parent
16e0a483e2
commit
c08b90b0b2
5 changed files with 260 additions and 4 deletions
|
|
@ -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)".
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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}"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
90
src/TeamsISO.App/Services/TeamsLauncher.cs
Normal file
90
src/TeamsISO.App/Services/TeamsLauncher.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue