Files
LlamaCasty/HANDOFF.md
T
gramps 1e4017df03 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.
2026-09-17 08:03:58 -07:00

7.7 KiB
Raw Blame History

HANDOFF — 2026-09-17 (webcam resource lifecycle, startup slice; committed locally — no push)

Branch / Commit State

main HEAD = 94a934f webcam reader-ladder fix (local). This work unit sits un-pushed on top as a NEW commit (same bundle of web/A/V work — stays commit-local until greenlight). Below: slice-18 C4 cache (64a5a6d), slice-17, slice-16, slice-15, c01206f, b22d08e (signed audio-sync, pushed). No push after this commit either — still awaiting the device take + user greenlight.

⚠️ Branding (2026-09-14, creator-corrected): product = llamacasty, internals = ytLive

Product is llamacasty; repo path, csproj AssemblyName/RootNamespace, DB/log paths (%APPDATA%\ytLlive\...), most code names are legacy ytLive/ytLlive. User-facing text: "llamacasty" / UI labels use the catalog names ("Web Cam", "YouTube Chat", "Countdown Timer"…).

✅ Committed locally — webcam resource lifecycle (startup slice)

Incident review (the reason this exists): the creator couldn't add a webcam to the Live scene (WebCam row grayed app-wide). Ground truth from %APPDATA%\ytLlive\ytLlive.db (NOT layout.db, which is a 0-byte legacy file): one Webcam identity (C920 \?\USB#VID_046D&PID_082D…GLOBAL) AND one WebcamSceneConfig — in the Chat scene. Under the single-identity rule that means Live's "+" menu is gray by design, but the app couldn't say why. Combined with the earlier crash/failure chain, the fix is a proper resource lifecycle:

  • Startup poll + tri-state (MainViewModel.ValidateWebcamResourceStartupAsync, fired fire-and-forget right after LoadLayout): 0 webcams → app runs on, layer inactive, no alarm; 1 → attempt CameraManager.AcquireAsync as an app-wide LOCK; ≥2 → no auto-lock — selection belongs to the App Settings dialog (gear) — that's the next slice.
  • Persistent red alert (WebcamLockAlert, bottom of the Layers panel, + Retry button): shown when the one detected camera can't be locked; re-polls every 5 s (_webcamLockPollTimer) and clears the moment a lock succeeds, or on any first real frame (OnCameraPreviewBitmapChanged).
  • CameraManager.IsRunning(deviceId) — new accessor (session exists, started OR starting) so the pass never double-acquires a lock the loaded identity's configs already hold; a rolled-back (failed) session leaves the dictionary, so the pass retries cleanly.
  • Test seams mirroring LayoutPathOverride: CameraEnumeratorOverride / CameraFrameSourceFactoryOverride — the startup probe must NEVER touch real hardware under test (the reason the 09:15 tests never asserted camera state). Good Dog test WebcamStartupResourceTests x3: single-cam locked + app runs on; zero-cams no-alarm no-lock; lock-fails → red alert → Retry → clears. 304/304 green, build 0 warnings.
  • Attribution correction committed to ai.md: "NVIDIA Broadcast opens the webcam exclusively" is a suspect-list claim — CameraConflictProbe reads running process NAMES only, no device handles; the contention symptoms fit shared-mode/bandwidth just as well. Do not restate it as fact (creator called this out).

Task docs: TASKS.md Open items + ai.md Webcam section updated to match.

⚠️ Open items

  • Webcam take fix 2 of 2 — the 09:16 crash is UNFIXED (hard, untested): hypothesis — native WMF death when MediaCapture.Failed fires mid-stream and SafeStopAsync (fire-and-forget) disposes reader+capture while the frame thread sits in TryAcquireLatestFrame/Marshal.Copy (outside the frame's try/catch). No repro, no integration test, native race not catchable — deferred per spin-guard. Record a second occurrence's pattern.
  • Multi-camera slice (user-queued): when ≥2 webcams exist, add the web-cam selection text + control to the App Settings dialog (gear icon, OverlayHost — left-click opens settings, right-click the context menu). Selecting + successfully allocating/locking activates the WebCam layer. Currently ≥2 cameras → nothing shows in settings.
  • Static (+) catalog slice (user-queued): all catalog rows always listed (Background, "Web Cam", "Countdown Timer", "Web Resource", "YouTube Chat", "YouTube Event"…), gray only for (1) system-level unavailability (persistent alert), (2) max already allocated, (3) not available for the current scene; tooltips must name the reason (e.g. "already in Chat"). Static labels only, no new code-behind.
  • Device re-verify (one take session): 1. slice-18 cache verdicts (NR/WH hits, stalls, band ~60); 2. webcam: fresh launch with the browser holding the camera must show the webcam or a NAMED chip (now also the persistent red alert, and the alert must clear after the browser closes + retry); 3. verify the Chat scene still owns the webcam (so Live's + is intentionally gray with a reason).
  • No push yet — after the take verdict, re-measure the clap offset (/tmp/opencode/avsync.py), then decide push with the user.

Open threads (carried)

  • Audio-silence verification — fixed (724af14); creator heard real audio.
  • Webcam MJPG missing / ~10–14Hz, layer SortOrder, truncation-with-dynamic-scenes — queued.
  • Sync control user-doc tutorial — REQUIRED before 1.0 (creator directive; TASK 22).
  • Signed A/V sync: verify the negative (advance) direction on device.
  • Focus-loss capture lag — closed as NOT the cause (1824: delivery healthy ~60-100/s).

Landmines

  • testhost shares startup.log — filter by time; the app also writes fake-device failures ('test-camera', 'dev-physical-9') from CameraManager unit tests.
  • cmd.exe /c "taskkill /F /IM ytLive.exe" (WSL double-slashes mangle) before rebuilds — a live app process locks ytLive.exe (MSB3021).
  • Build/tests: Windows dotnet host (/mnt/c/Program Files/dotnet/dotnet.exe). 0 warnings — only ./scripts/verify.sh "<files>"'s clean build counts. Running ytLive.csproj alone does NOT rebuild ytLive.Tests.dll — run the Tests csproj before vstest. Known audio flake: Mix_HonorsProviderGains_AndGameMute_KillsTheLoopback (float precision) — re-run if it trips alone; unrelated to camera work.
  • Assert footguns in this repo's xUnit: Assert.Null(x, "msg")/Assert.Single(coll, "msg") do NOT take a message — the 2-arg overloads mean something else (predicate/item). Empty message = 1 arg.
  • The webcam's real state lives in %APPDATA%\ytLlive\ytLlive.db (layout.db there is a 0-byte legacy file). sqlite3 at /home/gramps/android-sdk/platform-tools/sqlite3.
  • Contention failure modes (startup.log): "no frames within 4s" = init OK but starved; "OutputFormatNotSupported" = reader subtype below the MJPG-active camera; "file being used by another process / CaptureMode is SharedReadOnly" = re-negotiation refused while others hold the device. CameraConflictProbe = suspect list, not handle evidence.
  • FramePump tests asserting per-frame CONTENT need a STABLE scene; cache-sensitive assertions can't use a call-count resolver flip (tick resolves twice) — key alternation on OutputIndex.
  • ffmpeg/ffprobe: /mnt/c/Program Files/Krita (x64)/bin/ with Windows paths.
  • MyMistakes.md: WINRT resource-allocation RECIPE, freeze-audit RECIPE, A/V sync recipe, deadline pacing, CoreMessaging DQ, WGC-CLIP + slice blocks. Grep before re-deriving.
  • C:\tmpout is for ffmpeg evidence artifacts; keep them out of the repo.

Next step

The multi-camera App Settings selector slice (+ static catalog rows after), then the creator's take on this build. The alert is the new expected behavior to eyeball on the take: browser holds the camera → fresh launch → red alert in Layers (unless Chat's config grabbed it) that clears after a Retry once the browser closes.