feat(webcam): resource lifecycle startup slice — poll-on-start, single-cam lock, persistent Layers alert
The creator couldn't add a webcam to Live (grayed app-wide) and nothing in the app explained why. Ground truth from the live DB: one Webcam identity AND one WebcamSceneConfig in the Chat scene — so the gray was the single-identity rule working, but the reason was unobservable. This slice makes the webcam a resource the app validates and locks, mirroring how OBS reserves its devices. At startup we enumerate the OS once (ValidateWebcamResourceStartupAsync, fired after LoadLayout, stored as WebcamStartupValidationTask for tests to await): - 0 webcams -> app runs on, layer inactive, no alarm - exactly 1 -> attempt CameraManager.AcquireAsync as an app-wide lock; on failure show a persistent red alert at the bottom of the Layers panel (WebcamLockAlert + Retry) that re-polls every 5s and clears itself the moment the camera locks, or on any first real frame - >=2 -> deliberately no auto-lock; camera selection belongs to the App Settings dialog (gear) — next slice CameraManager.IsRunning(deviceId) tells the pass a session already exists (started OR still starting) so loaded identity configs count as the lock and the pass never double-acquires. Test seams mirror LayoutPathOverride: CameraEnumeratorOverride / CameraFrameSourceFactoryOverride so the startup probe never touches real hardware under test. Good Dog test: WebcamStartupResourceTests x3 — single-cam locked + app runs on, zero-cams no-alarm/no-lock, lock-fails -> red alert -> Retry -> clears. Full suite 304/304, build 0 warnings, scope-check passed. Docs in-commit: TASKS Open items + ai.md Webcam section + HANDOFF rewrite. Also corrects the record: 'NVIDIA Broadcast opens the webcam exclusively' was a suspect-list claim (CameraConflictProbe reads process names only, no handles) — not restated as fact.
This commit is contained in:
@@ -423,6 +423,24 @@ This replaces the old five-seeder cluster (`Seed{Starting,Brb,Ending,Chat}Backgr
|
||||
TASK 26; the old per-staged-scene gate let a second picker run from a scene that lacked the config).
|
||||
**Removing the last webcam config anywhere clears the identity** (`_webcam = null`, raising
|
||||
`CanAddWebcam`/`CanChangeWebcam`), which also drops "Change Webcam…" and re-enables Add.
|
||||
- **Startup resource lifecycle (2026-09-17):** the webcam is a resource the app *allocates and
|
||||
locks*, validated at startup — this answers "why is the WebCam row greyed" from inside the app
|
||||
instead of a DB spelunk. `MainViewModel.ValidateWebcamResourceStartupAsync` (fired fire-and-forget
|
||||
right after `LoadLayout`, stored as `WebcamStartupValidationTask` so tests await it) enumerates the
|
||||
OS once and tri-states on the count: **0** → app runs on, layer inactive, no alarm; **1** → attempt
|
||||
`CameraManager.AcquireAsync(device)` as an app-wide lock — on failure a **persistent red alert**
|
||||
(`WebcamLockAlert`, bottom of the Layers panel with a Retry button) re-polls every 5 s
|
||||
(`_webcamLockPollTimer`) until a lock succeeds, and any first real frame also clears it; **≥2** →
|
||||
deliberately no auto-lock — selection belongs to the App Settings dialog (gear) **next slice**.
|
||||
A session already running for the device (loaded identity's configs) counts as the lock, so the
|
||||
pass never double-acquires: `CameraManager.IsRunning(deviceId)` = a session exists (started OR
|
||||
still starting — a mid-start session is not "locked yet", it's in flight), and a rolled-back
|
||||
(failed) session leaves the dictionary so the pass can retry. Test seams mirror `LayoutPathOverride`:
|
||||
`CameraEnumeratorOverride`/`CameraFrameSourceFactoryOverride` let `WebcamStartupResourceTests`
|
||||
drive the probe without real hardware (0-cam no-alarm, 1-cam locked, lock-fail → alert → clears on
|
||||
retry). **Attribution correction:** the "NVIDIA Broadcast opens the webcam exclusively" failure
|
||||
below is a *suspect-list* claim — `CameraConflictProbe` reads only running process names, no device
|
||||
handles; contention symptoms fit shared-mode/bandwidth just as well (see `MyMistakes.md`).
|
||||
The **YouTube Chat layer follows the same one-per-layout rule** (TASK 27):
|
||||
`CanAddYouTubeChat` greys out the (+) item while any scene carries a ChatBox source (raised on
|
||||
staging + elements change; `AddSource` refuses a second as defense in depth). Chat layers created
|
||||
|
||||
Reference in New Issue
Block a user