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
+17 -21
View File
@@ -24,29 +24,26 @@
</TextBlock>
</Button>
<!-- REC / ON-AIR pills (intent) + status signs (reality) -->
<!-- Session controls (creator order 2026-09-01): [sign light] LABEL [pill] per
output — each reality lamp sits in front of its own intent switch. -->
<StackPanel Grid.Column="1" Orientation="Horizontal"
HorizontalAlignment="Center" VerticalAlignment="Center">
<!-- REC pill: local recording needs no sign-in -->
<TextBlock Text="REC" Foreground="#e94560" FontWeight="Bold" FontSize="12"
VerticalAlignment="Center" Margin="0,0,6,0"/>
<!-- REC group: local recording needs no sign-in -->
<Ellipse Width="12" Height="12" VerticalAlignment="Center"
Fill="{Binding RecDotBrush}" Opacity="{Binding RecDotOpacity}"/>
<TextBlock Text="REC" Style="{StaticResource StatusSignText}"/>
<ToggleButton Style="{StaticResource PillToggle}" VerticalAlignment="Center"
Margin="6,0,16,0"
IsChecked="{Binding RecordPillOn}"/>
<!-- ON-AIR pill: greyed out until signed in -->
<TextBlock Text="ON-AIR" Foreground="#e94560" FontWeight="Bold" FontSize="12"
VerticalAlignment="Center" Margin="16,0,6,0"/>
<!-- ON-AIR group: greyed out until signed in -->
<Ellipse Width="12" Height="12" VerticalAlignment="Center"
Fill="{Binding OnAirBrush}"/>
<TextBlock Text="ON-AIR" Style="{StaticResource StatusSignText}"/>
<ToggleButton Style="{StaticResource PillToggle}" VerticalAlignment="Center"
Margin="6,0,0,0"
IsChecked="{Binding OnAirPillOn}"
IsEnabled="{Binding CanToggleOnAir}"
ToolTip="{Binding AccountStatusLightToolTip}"/>
<!-- Status signs: REC dot + ON-AIR dot -->
<Ellipse Width="12" Height="12" VerticalAlignment="Center" Margin="16,0,0,0"
Fill="{Binding RecDotBrush}" Opacity="{Binding RecDotOpacity}"/>
<TextBlock Text="REC" Style="{StaticResource StatusSignText}"/>
<!-- ON-AIR sign: gray offline, red public, green private -->
<Ellipse Width="12" Height="12" VerticalAlignment="Center" Margin="16,0,0,0"
Fill="{Binding OnAirBrush}"/>
<TextBlock Text="ON-AIR" Style="{StaticResource StatusSignText}"/>
<Border Background="#8f1f1f" CornerRadius="3" Padding="6,1" Margin="8,0,0,0"
VerticalAlignment="Center"
Visibility="{Binding IsLivePrivate, Converter={StaticResource BoolToVis}}">
@@ -89,17 +86,16 @@
Visibility="Collapsed"/>
</Grid>
</Border>
<!-- Sign In button (disconnected + offline) -->
<Button Content="Sign In" Style="{StaticResource YtButton}"
Margin="0,0,12,0"
Command="{Binding StartStreamCommand}"
Visibility="{Binding ShowLogInButton, Converter={StaticResource BoolToVis}}"/>
<!-- Primary Start button (offline): dynamic label from the pills -->
<!-- Primary Start button: the always-present idle action (sign-in moved
into its context menu — a bare bar after stopping was a dead end) -->
<Button Content="Start" Style="{StaticResource YtButton}"
Command="{Binding StartStreamCommand}"
Visibility="{Binding ShowPrimaryStartButton, Converter={StaticResource BoolToVis}}">
<Button.ContextMenu>
<ContextMenu>
<MenuItem Header="Sign in to YouTube" Command="{Binding SignInCommand}"
Visibility="{Binding IsConnected, Converter={StaticResource InverseBoolToVis}}"/>
<Separator/>
<MenuItem Header="Choose Record Folder" Command="{Binding ChooseRecordFolderCommand}"/>
<Separator/>
<MenuItem Header="Change Account" Command="{Binding ChangeAccountCommand}"/>