feat(webcam): app-default gate slice — per-scene offer, Add places default directly, identity survives removal
Chat's camera no longer greys Web Cam in Live (per-scene max, not app-wide); TASK 26 superseded by creator directive (app-level resource model). Add Webcam now places the existing app default without the picker; the picker runs only for the initial selection. Removing the last placement keeps the identity (_webcam never nulled) so Add stays offered. Startup pass adopts a solo camera as the app default, so a clean layout offers the layer in Live/Chat at once. Dynamic WebcamAddToolTip names the why (incl. the 'graduated to OBS' line). Good Dog: WebcamMenuGateTests rewritten (real app + temp DB + camera seams) — identity in Chat does not gray Live; Add in Live places same wc-1 no picker; scene-with-placement stays gray; identity survives both removals (DB row 1). WebcamStartupResourceTests single-lock fact asserts CanAddWebcam after adoption. Docs same commit (ai.md supersession, TASKS.md, HANDOFF.md). 304/304, 0 warnings.
This commit is contained in:
@@ -408,21 +408,22 @@ This replaces the old five-seeder cluster (`Seed{Starting,Brb,Ending,Chat}Backgr
|
||||
- **TFM:** `net8.0-windows10.0.19041.0` (app + tests) pulls the WinRT projection from the SDK reference
|
||||
packs — no NuGet package, no capability manifest (unpackaged desktop app works; the Windows privacy
|
||||
camera toggle still applies). `EnableWindowsTargeting` keeps WSL builds working.
|
||||
- **One webcam, many scenes (schema v3):** a singleton `Webcam` row holds the identity
|
||||
(`Id`/`DeviceId`/`Name`); each scene gets its own `WebcamSceneConfig` (position/size/clip/mirror/
|
||||
border/`IsVisible`). `Scene.Elements` holds images (`Source`) and, at most once, the webcam
|
||||
(`WebcamSceneConfig`); `Scene.WebcamConfig` is the accessor. `CameraManager` refcounts capture
|
||||
sessions by `DeviceId` (a session starts at `RefCount = 1`; repeat acquire bumps it; the last
|
||||
release stops + disposes). **"Add Webcam" ALWAYS opens the Windows camera picker** — the creator is
|
||||
never silently handed the previous camera (which used to happen after deleting one scene's webcam
|
||||
while another scene still used it; that identity survived, so re-adding bypassed the choice).
|
||||
Picking a different camera than the current app-wide one swaps it everywhere via
|
||||
`SwapWebcamIdentityAsync` (the same path "Change Webcam…" uses), keeping the singleton honest;
|
||||
picking the same one just places the config. The Add Webcam menu item greys out whenever a webcam
|
||||
exists **anywhere** — one camera identity app-wide (`CanAddWebcam` = `StagedScene != null && _webcam == null`,
|
||||
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.
|
||||
- **One webcam, many scenes (schema v3) — app-level default (2026-09-17):** a singleton `Webcam`
|
||||
row holds the identity (`Id`/`DeviceId`/`Name`); each scene gets its own `WebcamSceneConfig`
|
||||
(position/size/clip/mirror/border/`IsVisible`). `Scene.Elements` holds images (`Source`) and, at
|
||||
most once, the webcam (`WebcamSceneConfig`); `Scene.WebcamConfig` is the accessor. `CameraManager`
|
||||
refcounts capture sessions by `DeviceId` (a session starts at `RefCount = 1`; repeat acquire bumps
|
||||
it; the last release stops + disposes). **The webcam is an app-level resource — ONE selection (the
|
||||
app default), usable in every scene that can host it.** "Add Webcam" places the existing default
|
||||
DIRECTLY (no picker); the Windows camera picker runs only for the initial selection
|
||||
(`_webcam == null`). Choosing a different camera in the picker swaps it app-wide via
|
||||
`SwapWebcamIdentityAsync` (same path "Change Webcam…" uses). **Per-scene gate, not app-wide**
|
||||
(TASK 26 superseded by creator directive 2026-09-17): `CanAddWebcam` = `StagedScene is { WebcamConfig: null } && _webcam != null`
|
||||
— a scene holding its own placement greys the row (one webcam per stream), but ANOTHER scene
|
||||
holding one does not, so Chat's camera never greys Live; the greyed-row tooltip carries the reason
|
||||
(`WebcamAddToolTip`, incl. the gentle "a second camera means you've graduated to OBS" line).
|
||||
Removing a scene's placement **keeps the identity** (it's the app default; `_webcam` is never
|
||||
nulled by removal) — Add stays offered for the same camera.
|
||||
- **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
|
||||
@@ -435,10 +436,14 @@ This replaces the old five-seeder cluster (`Seed{Starting,Brb,Ending,Chat}Backgr
|
||||
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`:
|
||||
(failed) session leaves the dictionary so the pass can retry. After a successful single-cam lock,
|
||||
the pass **adopts the camera as the app default** if no identity exists yet (fresh layout) — the
|
||||
Web Cam layer is then offered in Live/Chat immediately (`CanAddWebcam` true); `IsRunning` skip
|
||||
means a loaded identity whose session is already up never overwrites it. 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
|
||||
drive the probe without real hardware (0-cam no-alarm, 1-cam locked → identity adopted, 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):
|
||||
|
||||
Reference in New Issue
Block a user