fix(webcam): offer the Web Cam row only when a camera is attainable (live lock), not merely selected
Creator refinement: 'offered iff there's not one already configured & attainable'. CanAddWebcam now requires IsWebcamAttainable = identity present AND a RUNNING session (CameraManager.IsRunning) — an identity whose camera was unplugged or whose lock keeps failing leaves the row greyed with reason 'No webcam is currently available…', and it un-greys the moment a session is live. Gate re-raised at every attainability flip: staging, removal, startup lock success, first frame, camera failure, identity swap. Root cause the old test surfaced: the startup pass skipped acquiring when the loaded identity's configs already held the session, so there was no independent app base ref — removing the last placement dropped RefCount to 0 and killed the session. The single-camera branch now ALWAYS acquires (a running session just bumps), laying the app-wide base hold so the default outlives the scenes. Good Dog: WebcamMenuGateTests second fact — identity loaded, session can't start → row NOT offered + 'No webcam is currently available…' tooltip. Positive fact waits for WebcamStartupValidationTask to make the IsRunning read deterministic. Docs same commit (ai.md gate + base-lock, TASKS.md, HANDOFF.md incl. proven pre-existing audio flake). 305 tests (304 pass + known flake), 0 warnings.
This commit is contained in:
@@ -23,17 +23,22 @@ public partial class MainViewModel
|
||||
private DispatcherTimer? _webcamLockPollTimer;
|
||||
|
||||
/// <summary>
|
||||
/// Add → Webcam menu item: the webcam is an app-level resource (one camera —
|
||||
/// the app default). The row is offered in any scene that doesn't already
|
||||
/// host it, wherever a camera is selected; it is NOT blocked by other scenes
|
||||
/// holding their own placement (per-scene max, not app-wide).
|
||||
/// Add → Webcam menu item. The row is offered iff the SCENE has no webcam
|
||||
/// config yet AND a camera is attainable — an app default is selected and
|
||||
/// its session is RUNNING right now (identity alone is not enough: the
|
||||
/// device may have been unplugged, or the lock may still be failing, in
|
||||
/// which case the row stays grey and the persistent alert says why).
|
||||
/// Per-scene max is per-scene: another scene holding its own placement does
|
||||
/// NOT grey this one.
|
||||
/// </summary>
|
||||
public bool CanAddWebcam => StagedScene is { WebcamConfig: null } && _webcam != null;
|
||||
public bool CanAddWebcam => StagedScene is { WebcamConfig: null } && IsWebcamAttainable;
|
||||
|
||||
private bool IsWebcamAttainable => _webcam != null && _cameraManager.IsRunning(_webcam.DeviceId);
|
||||
|
||||
/// <summary>
|
||||
/// Why the Webcam row is greyed, when it is — never a silent bool. The max-1
|
||||
/// reason gets the gentle "graduated to OBS" line; no camera at all gets its
|
||||
/// own wording. Shown as the row's tooltip.
|
||||
/// reason gets the gentle "graduated to OBS" line; no attainable camera gets
|
||||
/// its own wording. Shown as the row's tooltip.
|
||||
/// </summary>
|
||||
public string WebcamAddToolTip
|
||||
{
|
||||
@@ -41,12 +46,20 @@ public partial class MainViewModel
|
||||
{
|
||||
if (StagedScene?.WebcamConfig != null)
|
||||
return "Already in this scene — one webcam per stream. A second camera means you've graduated to OBS.";
|
||||
if (_webcam == null)
|
||||
return "No webcam detected — plug one in, or allow camera access in Windows.";
|
||||
if (!IsWebcamAttainable)
|
||||
return "No webcam is currently available — plug one in, or allow camera access in Windows.";
|
||||
return "Adds the app default webcam to this scene.";
|
||||
}
|
||||
}
|
||||
|
||||
// Re-raise the rows that read live CameraManager session state: the offer
|
||||
// flips the moment a session starts (lock acquired, first frame) or dies.
|
||||
private void RaiseWebcamGateChanged()
|
||||
{
|
||||
OnPropertyChanged(nameof(CanAddWebcam));
|
||||
OnPropertyChanged(nameof(WebcamAddToolTip));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Right-click-on-preview → "Show Webcam": offered when the active scene has no
|
||||
/// webcam config (add one) or hides it (unhide — keeps the config row).
|
||||
@@ -193,6 +206,7 @@ public partial class MainViewModel
|
||||
if (_webcam?.DeviceId != deviceId) return;
|
||||
WebcamError = null;
|
||||
WebcamLockAlert = null; // the camera is provably alive — alert resolved.
|
||||
RaiseWebcamGateChanged();
|
||||
foreach (var scene in Scenes)
|
||||
foreach (var config in scene.Elements.OfType<WebcamSceneConfig>())
|
||||
config.VideoImageSource = bitmap;
|
||||
@@ -204,6 +218,7 @@ public partial class MainViewModel
|
||||
{
|
||||
if (_webcam?.DeviceId != deviceId) return;
|
||||
WebcamError = $"Webcam offline: {message}";
|
||||
RaiseWebcamGateChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -239,26 +254,23 @@ public partial class MainViewModel
|
||||
|
||||
var device = cameras![0];
|
||||
|
||||
// A session already started/starting for this device (loaded identity)
|
||||
// means we already hold the lock — nothing to do.
|
||||
if (_cameraManager.IsRunning(device.Id))
|
||||
{
|
||||
WebcamLockAlert = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// Lay the app-wide base lock. AcquireAsync on an already-running session
|
||||
// (loaded identity's configs) just bumps its refcount — that bump is the
|
||||
// app's own hold, so removing EVERY scene's placement keeps the session
|
||||
// alive and the Web Cam row offered (the app default outlives the scenes).
|
||||
var started = await _cameraManager.AcquireAsync(device.Id);
|
||||
if (started)
|
||||
{
|
||||
// The single camera becomes the app default so the Web Cam layer is
|
||||
// offered in the capabled scenes (Live/Chat) even on a clean layout.
|
||||
// The camera is now provably attainable: adopt the new identity when
|
||||
// there wasn't one, then re-raise the offer either way
|
||||
// (RaiseWebcamGateChanged) so an existing identity whose session was
|
||||
// down un-greys the row the moment the lock is real.
|
||||
if (_webcam == null)
|
||||
{
|
||||
_webcam = new Webcam { DeviceId = device.Id, Name = device.DisplayName };
|
||||
OnPropertyChanged(nameof(CanChangeWebcam));
|
||||
OnPropertyChanged(nameof(CanAddWebcam));
|
||||
OnPropertyChanged(nameof(WebcamAddToolTip));
|
||||
}
|
||||
RaiseWebcamGateChanged();
|
||||
WebcamLockAlert = null;
|
||||
return;
|
||||
}
|
||||
@@ -344,6 +356,8 @@ public partial class MainViewModel
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
RaiseWebcamGateChanged();
|
||||
}
|
||||
|
||||
// "Change Webcam…" from the webcam's context menu: picker, then swap the
|
||||
|
||||
Reference in New Issue
Block a user