Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 30 additions & 8 deletions docs/beta-engineering-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ What exists vs. what the plans assumed:

| Area | Reality |
|---|---|
| Packaging | `scripts/package-native.ps1` (loose folder) and `package-native-msix.ps1` (unsigned MSIX with a robust MakeAppx/layout fallback chain) both stage native core + Zoom runtime + FFmpeg. Neither registers the vcam DLL, sets up crash dumps, nor creates the recording folder |
| Packaging | **The beta path is NSIS, not MSIX (T2.6 / #438).** `package-alpha.ps1` + `package-alpha-installer.ps1` build the shipped installer from `scripts/alpha/installer.nsi`, gated by `Test-AlphaInstaller.ps1`. The installer registers the virtual camera and creates the recording folder as of T2.1/#433, and its uninstall removes the first-run FFmpeg runtime (T2.7/#474). It does NOT enable WER crash dumps: that key is HKLM-only and this installer is per-user — measured 2026-09-12, an HKCU entry produced zero dumps. `package-native.ps1` (loose folder) and `package-native-msix.ps1` (unsigned MSIX) still exist; nothing on the beta path calls them |
| Signing | **D2 shipped 2026-07-18:** `sign-native-msix.ps1 -Mode production` signs via Azure Trusted Signing (dlib) or PFX/thumbprint from env, requires an RFC3161 timestamp, runs `signtool verify /pa`, enforces the manifest-Publisher/cert-subject match, and **hard-fails on any gap** (distinct exit codes). `-Mode dev` (default) keeps the self-signed flow and still exits 0 without signtool, but now prints a LOUD "ARTIFACT LEFT UNSIGNED" warning. Decision logic covered by `scripts/tests/test-sign-native-msix.ps1` (10 dry-run cases). Cert identity itself still unprovisioned (D0.1) |
| Versioning | ~~THREE unsynced version sources~~ **Synced via D1 (2026-07-18):** `package.json` is the source of truth; `scripts/stamp-version.mjs` stamps `Package.appxmanifest` (`Identity Version`, still `Publisher="CN=CoreVideo Pro Dev"` — D2 owns that) and the csproj; CI `version-sync` job enforces it. Release tag validation still checks only `package.json` (fine — everything else must now match it) |
| CI release | `release.yml` builds the **loose folder** (not MSIX), never signs, never creates a GitHub Release; the artifact upload is gated on `COREVIDEO_PUBLISH == 'never'` (inverted/dead). `bump:version` + `release:notes` exist but are unwired |
Expand All @@ -34,13 +34,35 @@ What exists vs. what the plans assumed:
trusted, signtool-integrated, days not weeks, no cert file to protect in CI)
with a classic OV Authenticode purchase started in parallel as fallback —
Trusted Signing still requires org identity validation, so start BOTH now.
2. **Package format: signed MSIX + App Installer.** `package-native-msix.ps1`
already produces the MSIX and the manifest already declares `runFullTrust` +
the OAuth protocol. App Installer (`.appinstaller` at a stable HTTPS URL)
gives install + **auto-update on launch** with near-zero app code — it
collapses the B1 "auto-update channel" item into hosting a file. Fallback if
MSIX fights us on a reference config: signed EXE installer (Inno/WiX) + the
D4 in-app version check.
2. **Package format — SUPERSEDED 2026-09-12 (T2.6 / #438). What actually ships
is an NSIS EXE installer, not MSIX.** This section recommended signed MSIX +
App Installer, and named "signed EXE installer (Inno/WiX)" only as the
fallback if MSIX fought us on a reference config. The fallback is what was
built, in #417, and every beta since has shipped that way. Reading this
section as current sends you to a packaging route nothing uses.

**The route in use, and where it actually lives:**
`scripts/package-alpha.ps1` stages the app, then
`scripts/package-alpha-installer.ps1` builds an NSIS installer from
`scripts/alpha/installer.nsi`, and `scripts/alpha/Test-AlphaInstaller.ps1` is
the gate — it performs a REAL silent install, a runtime probe, duplicate and
invalid-marker rejection, a silent uninstall and a user-file preservation
check. The whole runbook is in CLAUDE.md under "Cutting a Windows beta".
`package-native-msix.ps1` and `sign-native-msix.ps1` still exist and still
work; nothing on the beta path calls them.

**What the change cost, which is the part worth recording.** MSIX would have
given install + auto-update from one hosted `.appinstaller` file — that is
why it was preferred. NSIS gives neither, so:
- **auto-update is still unbuilt** and is no longer "collapsed into hosting a
file"; it is the D4 in-app version check or nothing;
- the installer had to grow, by hand, the things MSIX does for free —
one stable entry point, a manifest-driven uninstall that removes what first
run downloaded, and a refusal that says why (T2.7 / #474, 2026-09-12).

**Signing is unaffected either way.** T0.1 (a certificate) is still the
blocker, and `release.yml` already refuses to ship unsigned. Whichever
package format carries the bytes, nothing external installs until that lands.
3. **Zoom SDK redistribution** (`docs/zoom-windows-sdk-packaging.md`, still
unresolved): ask Zoom now. Fork: (a) licensed to bundle → runtime rides the
MSIX payload (already implemented — `Assert-MsixPayloadReady` requires
Expand Down
21 changes: 19 additions & 2 deletions native-shell/CoreVideoPro.WinUI/ViewModels/OverlaysViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,19 @@ public OverlaysViewModel(StudioViewModel studio)

public SolidColorBrush CaptionTextBrush => CaptionStyleHelper.CaptionTextBrush(CaptionTextColor);

// #476 / T3.5. A caption composites into PROGRAM, so its colour is a real
// on-air decision and deserves the same picker the brand rows have had all
// along. The hex string stays the source of truth: the picker reads it and
// writes it back, so there is no second copy to drift and no re-entrancy
// latch (the brand rows need one because their hex values are observable
// properties written from two directions; this is a projection).
public Color CaptionTextPickerColor
{
get => CoreVideoPro.WinUI.Services.HexColor.ParseOrDefault(
CaptionTextColor, Color.FromArgb(255, 0xF7, 0xFB, 0xF8));
set => CaptionTextColor = CoreVideoPro.WinUI.Services.HexColor.ToHex(value);
}

public SolidColorBrush CaptionBackgroundBrush =>
CaptionStyleHelper.CaptionBackgroundBrush(CaptionBackgroundOpacity);

Expand All @@ -143,7 +156,11 @@ public OverlaysViewModel(StudioViewModel studio)

partial void OnCaptionFontSizeChanged(string value) => NotifyCaptionPresentationChanged();

partial void OnCaptionTextColorChanged(string value) => NotifyCaptionPresentationChanged();
partial void OnCaptionTextColorChanged(string value)
{
OnPropertyChanged(nameof(CaptionTextPickerColor));
NotifyCaptionPresentationChanged();
}

partial void OnCaptionBackgroundOpacityChanged(int value) => NotifyCaptionPresentationChanged();

Expand Down Expand Up @@ -336,7 +353,7 @@ private static void SetPickerColorFromText(ref bool updating, Action update)
}

private static Color ParseHexColorOrDefault(string value, Color fallback) =>
TryParseHexColor(value, out var color) ? color : fallback;
CoreVideoPro.WinUI.Services.HexColor.ParseOrDefault(value, fallback);

private static bool TryParseHexColor(string? value, out Color color)
{
Expand Down
29 changes: 27 additions & 2 deletions native-shell/CoreVideoPro.WinUI/Views/OverlaysPage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -459,8 +459,33 @@
</StackPanel>
<StackPanel Grid.Row="1" Grid.Column="2" Spacing="4">
<TextBlock Text="Text color" FontSize="10" Foreground="{StaticResource StudioMutedBrush}" />
<TextBox Text="{x:Bind ViewModel.Overlays.CaptionTextColor, Mode=TwoWay}"
PlaceholderText="#F7FBF8" />
<!-- #476: the hex box stays, with a picker beside it. -->
<Grid ColumnSpacing="6">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="24" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBox Text="{x:Bind ViewModel.Overlays.CaptionTextColor, Mode=TwoWay}"
PlaceholderText="#F7FBF8" />
<Border Grid.Column="1" Width="22" Height="22" CornerRadius="4"
VerticalAlignment="Center"
Background="{x:Bind ViewModel.Overlays.CaptionTextBrush, Mode=OneWay}" />
<Button Grid.Column="2" Content="Pick"
Style="{StaticResource OperatorGhostButton}"
AutomationProperties.Name="Pick caption text color">
<Button.Flyout>
<Flyout>
<ColorPicker Color="{x:Bind ViewModel.Overlays.CaptionTextPickerColor, Mode=TwoWay}"
IsAlphaEnabled="False"
IsAlphaSliderVisible="False"
IsAlphaTextInputVisible="False"
IsColorChannelTextInputVisible="False"
IsHexInputVisible="False" />
</Flyout>
</Button.Flyout>
</Button>
</Grid>
</StackPanel>
<StackPanel Grid.Row="1" Grid.Column="3" Spacing="4">
<TextBlock FontSize="10" Foreground="{StaticResource StudioMutedBrush}">
Expand Down
1 change: 1 addition & 0 deletions native/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -604,6 +604,7 @@ if(BUILD_TESTING)
tests/OutputDestinationSupervisorTest.cpp
tests/AudioDspTest.cpp
tests/MediaCueHandoffTest.cpp
tests/BrowserHostExitMessageTest.cpp
tests/IsoFrameConformTest.cpp
tests/BrowserSourceTest.cpp
tests/AudioMasteringTest.cpp
Expand Down
33 changes: 33 additions & 0 deletions native/src/modules/BrowserHostExitMessage.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#pragma once

// #440 / T3.2 — what a browser source says when its host process dies.
//
// The adapter reported "browser host exited (code 3)". Exit 3 has exactly one
// common cause on a machine we have never seen: the Evergreen WebView2 Runtime
// is not installed. The operator saw a stalled source and a number.
//
// Same rule as the Zoom join reasons (modules/ZoomJoinFailureMessage.h): an
// exit code is WIRE vocabulary, this is the ONE place it becomes English, and
// an UNKNOWN code passes through with its number rather than being flattened
// into a generic sentence that says nothing.

#include <string>

namespace corevideo::modules {

// The browser host's own exit contract (native/browser-host): 3 is raised both
// when the runtime probe finds nothing AND when creating the WebView2
// environment fails, because on a tester's machine those are the same
// actionable fact.
inline constexpr int kBrowserHostExitWebView2Missing = 3;

[[nodiscard]] inline std::string browserHostExitMessage(int exitCode) {
if (exitCode == kBrowserHostExitWebView2Missing) {
return "This browser source needs the Microsoft Edge WebView2 Runtime, which is not "
"installed (or could not start). Install the Evergreen WebView2 Runtime from "
"https://developer.microsoft.com/microsoft-edge/webview2/ and reload the source.";
}
return "Browser host exited (code " + std::to_string(exitCode) + ").";
}

} // namespace corevideo::modules
7 changes: 6 additions & 1 deletion native/src/modules/BrowserSourceHostAdapter.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#include "core/BoundedAsyncLog.h"
#include "modules/BrowserSourceHostAdapter.h"

#include "modules/BrowserHostExitMessage.h"

#include "modules/BrowserSourceShm.h"

#include <algorithm>
Expand Down Expand Up @@ -377,7 +379,10 @@ void BrowserSourceHostAdapter::supervisorLoop() {
closeProcessLocked(source);
closeMappingLocked(source);
source.policy.onFailure(now);
source.lastError = "browser host exited (code " + std::to_string(exitCode) + ")";
// #440 / T3.2: an exit code is wire vocabulary. Exit 3 means the
// WebView2 Runtime is missing, which is the one thing a tester on a
// machine we cannot see can actually fix.
source.lastError = browserHostExitMessage(static_cast<int>(exitCode));
::corevideo::core::nativeLogf("[browser] WARNING %s host DIED (exit=%lu, failure %d/%d) — %s\n",
id.c_str(), static_cast<unsigned long>(exitCode),
source.policy.consecutiveFailures(),
Expand Down
31 changes: 31 additions & 0 deletions native/tests/BrowserHostExitMessageTest.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// #440 / T3.2. A browser source whose host died reported "browser host exited
// (code 3)" and showed a stalled tile. Exit 3 has one common cause on a machine
// we have never seen — no WebView2 Runtime — and that is the one thing a tester
// can actually act on.

#include "modules/BrowserHostExitMessage.h"

#include <gtest/gtest.h>

using corevideo::modules::browserHostExitMessage;

TEST(BrowserHostExitMessage, TheMissingRuntimeSaysWhatToInstall) {
const auto message = browserHostExitMessage(3);
EXPECT_NE(message.find("WebView2"), std::string::npos) << message;
// It must carry the action, not just the diagnosis.
EXPECT_NE(message.find("Install"), std::string::npos) << message;
EXPECT_NE(message.find("developer.microsoft.com"), std::string::npos) << message;
// And must not leak the raw code, which is what it replaces.
EXPECT_EQ(message.find("code 3"), std::string::npos) << message;
}

// An unknown code keeps its number. Flattening every other death into a generic
// sentence would recreate the silence this replaces — the same rule the Zoom
// join reasons follow.
TEST(BrowserHostExitMessage, AnUnknownExitKeepsItsNumber) {
for (const int code : {0, 1, 70, -1073741819}) {
const auto message = browserHostExitMessage(code);
EXPECT_NE(message.find(std::to_string(code)), std::string::npos) << message;
EXPECT_EQ(message.find("WebView2"), std::string::npos) << message;
}
}
Loading