fix: webcam identity->device key for output frames; top bar always has a Start; light-pill grouping

Take two (2026-09-01) three confirmed defects, all from creator feedback + the
new resolver logging:

1. NO WEBCAM IN OUTPUT: WebcamSceneConfig.WebcamId carries the identity GUID;
   CameraManager keys sessions by DEVICE id — GetLatestFrame(guid) returned null
   forever, the compositor silently dropped the layer while the preview (bitmap
   path) looked fine. The resolver logging added in 8dcaee0 caught it red-handed.
   DeviceKeyForWebcam maps identity->device through the _webcam singleton (identity
   mismatch / unknown ids pass through). Test: WebcamOutputKeyTests.

2. START BUTTON VANISHED AFTER STOP: my pills-clear ruling met the old
   ShowPrimaryStartButton gate (needed IsConnected or a lit REC pill) — signed-out,
   pills-off = blank top bar, no session reachable. Start is now ALWAYS the idle
   face; unarmed Start records (local needs no account — no dead-end no-op); the
   Sign In button folds into Start's context menu ('Sign in to YouTube', shown while
   disconnected) with a dedicated SignInCommand. SessionTeardownTests extended:
   stopped session must leave a reachable Start.

3. TOP BAR ORDER (creator spec): [sign light] REC [pill]  [sign light] ON-AIR [pill]
   — each reality lamp now sits in front of its own intent switch (was: two pills,
   then two orphaned dots). Sign labels keep the shared StatusSignText style.

Tests: WebcamOutputKey, SessionTeardown, FramePump, WebcamMenuGate, GlobalHotkey,
BroadcastPullOut — 14/14 across the six classes, clean build 0 warnings.
This commit is contained in:
2026-09-01 22:25:20 -07:00
parent 97ffc426fe
commit 89fee6ca4a
6 changed files with 148 additions and 32 deletions
+7 -9
View File
@@ -60,7 +60,6 @@ public partial class MainViewModel : ViewModelBase
OnPropertyChanged(nameof(OnAirBrush));
OnPropertyChanged(nameof(IsLivePrivate));
OnPropertyChanged(nameof(ShowPrimaryStartButton));
OnPropertyChanged(nameof(ShowLogInButton));
OnPropertyChanged(nameof(ShowEndStreamButton));
OnPropertyChanged(nameof(EndButtonText));
OnPropertyChanged(nameof(AccountStatusLightToolTip));
@@ -127,12 +126,12 @@ public partial class MainViewModel : ViewModelBase
}
}
/// <summary>The primary button shows when there's something to start, but never
/// while a session is running — once recording/live the ONLY control is the End
/// button.</summary>
public bool ShowPrimaryStartButton =>
IsOffline && !IsRecording && !IsLive
&& (IsConnected || (RecordPillOn && !OnAirPillOn));
/// <summary>The primary button is ALWAYS the idle face of the top bar (2026-09-01):
/// stopping a session used to hide every start affordance (pills clear on stop,
/// and the old gate needed a sign-in or a lit pill — signed-out creators found a
/// blank bar). No dead ends: an unarmed Start records (local needs no account);
/// sign-in lives in the Start context menu.</summary>
public bool ShowPrimaryStartButton => IsOffline && !IsRecording;
/// <summary>Either output selected makes the button actionable.</summary>
public bool CanStartSession => RecordPillOn || OnAirPillOn;
@@ -144,8 +143,6 @@ public partial class MainViewModel : ViewModelBase
/// "Stop Recording", while live "End Stream".</summary>
public string EndButtonText => IsRecording && !IsLive ? "Stop Recording" : "End Stream";
public bool ShowLogInButton => !IsConnected && IsOffline && !IsRecording;
public StreamHealth CurrentHealth
{
get => _currentHealth;
@@ -320,6 +317,7 @@ public partial class MainViewModel : ViewModelBase
public ICommand StartStreamCommand { get; }
public ICommand EndStreamCommand { get; }
public ICommand SignInCommand { get; }
public ICommand ChooseRecordFolderCommand { get; }
public ICommand ResetRecordFolderCommand { get; }
}