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:
@@ -21,7 +21,15 @@ public partial class MainViewModel : ViewModelBase
|
||||
if (IsLive || IsRecording) return;
|
||||
var streaming = OnAirPillOn;
|
||||
var recording = RecordPillOn;
|
||||
if (!streaming && !recording) return;
|
||||
if (!streaming && !recording)
|
||||
{
|
||||
// Nothing armed = "record something" (local recording needs no account,
|
||||
// 2026-09-01): a Start that silently no-ops is the dead end the design
|
||||
// principle forbids. The pill lights so reality and intent agree.
|
||||
RecordPillOn = true;
|
||||
BeginRecordOnly();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!streaming)
|
||||
{
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -211,6 +211,7 @@ public partial class MainViewModel : ViewModelBase
|
||||
OpenPremiumCommand = new RelayCommand(_ => OpenUrl(PremiumUrl), _ => IsPremiumAvailable);
|
||||
OpenLicensesCommand = new RelayCommand(_ => ShowLicenses());
|
||||
StartStreamCommand = new RelayCommand(_ => StartSession());
|
||||
SignInCommand = new RelayCommand(_ => _ = SignInAsync(), _ => !IsConnected);
|
||||
EndStreamCommand = new RelayCommand(_ => StopStream(), _ => IsLive || IsRecording);
|
||||
ChooseRecordFolderCommand = new RelayCommand(_ => ChooseRecordFolder());
|
||||
ResetRecordFolderCommand = new RelayCommand(_ => ResetRecordFolder());
|
||||
@@ -478,11 +479,24 @@ public partial class MainViewModel : ViewModelBase
|
||||
|
||||
private DateTime _lastWebcamNullLog = DateTime.MinValue;
|
||||
|
||||
/// <summary>Maps a config's identity GUID to the CameraManager session key (the
|
||||
/// DEVICE id). internal = test seam. Take-2 fix: looking up the identity GUID
|
||||
/// against a device-keyed dictionary returned null forever.</summary>
|
||||
internal string DeviceKeyForWebcam(string webcamId) =>
|
||||
_webcam is { } w && w.Id == webcamId && !string.IsNullOrWhiteSpace(w.DeviceId)
|
||||
? w.DeviceId
|
||||
: webcamId;
|
||||
|
||||
private VideoFrame? ResolveOutputFrameCore(SceneElement element)
|
||||
{
|
||||
return element switch
|
||||
{
|
||||
WebcamSceneConfig webcam => _cameraManager.GetLatestFrame(webcam.WebcamId),
|
||||
// CameraManager sessions are keyed by DEVICE id (AcquireAsync(_webcam.DeviceId));
|
||||
// configs carry the identity GUID (WebcamSceneConfig.WebcamId = _webcam.Id).
|
||||
// Looking up by the GUID returned null forever — the webcam was structurally
|
||||
// invisible on the output while the preview (bitmap path) looked fine
|
||||
// (caught by the resolver logging, 2026-09-01 take two).
|
||||
WebcamSceneConfig webcam => _cameraManager.GetLatestFrame(DeviceKeyForWebcam(webcam.WebcamId)),
|
||||
Source { IsLiveCapture: true, CaptureKey: not null } live => _screenCaptureManager.GetLatestFrame(live.CaptureKey),
|
||||
Source { Type: SourceType.ChatBox } chatBox => RenderChatBox(chatBox),
|
||||
Source { Type: SourceType.WebSource } web => _webView2Manager?.GetLatestFrame(web.Id),
|
||||
|
||||
Reference in New Issue
Block a user