Files
LlamaCasty/HANDOFF.md
T
gramps 0d0e55de31 fix(recording-dialog): +20% height so the file-name box is not clipped; unhook full-screen hook on shutdown
RenameRecordingDialog was 230px tall for ~225px of stacked content —
the file-name textbox clipped (old client area ~193px). Height is now 276
(+20%). Integration test shows the real dialog in the app host and asserts
the textbox bottom edge stays inside the client area; the dialog's Icon moved
to the explicit /ytLive;component/ URLC form MainWindow already uses so tests
can construct it (root-relative /Assets/... only resolves in production via
Application.ResourceAssembly).

Dependency fix: MainViewModel.Shutdown() now calls
_fullScreenDetector.StopWatching(). The global EVENT_SYSTEM_FOREGROUND hook
was never unhooked; after VM collection the next foreground event invoked a
garbage-collected WinEventProc delegate, crashing the whole test run. Harmless
in production (exits the process) but fatal in the multi-window test host.

Reference: WPF WinEvent hook lifecycle guidance — SetWinEventHook callbacks
must be unhooked before the owning object is collected (obs-projector
fullscreen-detection pattern).
2026-09-18 08:33:13 -07:00

171 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# HANDOFF — 2026-09-18 (drag-reorder persistence + recording-dialog height, both local — no push)
## Branch / Commit State
`main` HEAD = **Rename Recording dialog 20% taller + teardown unhooks the full-screen hook** (local).
Below it: the layer drag-reorder persistence fix (`LayerReorderPersistenceTests`), then webcam work.
**No push yet.** Working tree clean.
## ⚠️ 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 — webcam app-default gate + attainability (`9d00955`, `75723ac`)
**Background:** the DB truth (`%APPDATA%\ytLlive\ytLlive.db`; NOT `layout.db`, a 0-byte legacy
file) put the single `WebcamSceneConfig` in **Chat**, so the old TASK 26 app-wide rule grayed
Live's "+" → Web Cam even though the camera was available and in use. The creator ruled (rigorous
resource model): **the webcam is an app-level resource — ONE app-wide default selection, usable in
every scene that can host it; it is NOT blocked by other scenes' placements.** Scope of this slice:
the availability layer. TASK 26's app-wide gate is **superseded** by creator directive.
- **Per-scene gate:** `CanAddWebcam` = `StagedScene is { WebcamConfig: null } && IsWebcamAttainable`.
A scene already hosting the webcam stays gray (one webcam per stream); another scene holding it
does NOT gray the row elsewhere.
- **Attainable = a live lock, not a saved identity** (`75723ac` refinement): `IsWebcamAttainable` =
`_webcam != null && CameraManager.IsRunning(_webcam.DeviceId)`. An identity whose camera is
unplugged / can't start leaves the row greyed (tooltip "No webcam is currently available…"), and
it un-greys the moment a session is running (startup lock, first frame, picker + acquire).
- **Add places the default directly:** when attainable, "Add Webcam" creates the placement with NO
picker; the Windows picker runs only for the initial selection (`_webcam == null`).
- **Identity survives last-placement removal:** `RemoveElement` no longer nulls `_webcam` (that was
the TASK 26 rule). `LayoutStore` writes the singleton whenever it's non-null, so it persists.
- **Startup lays an app-wide BASE lock:** the single-camera branch ALWAYS acquires (the old
`IsRunning` skip is gone) — on an already-running session it just bumps the refcount, and that ref
is the app's own hold. Result: removing every scene's placement leaves `RefCount = 1`, the session
stays alive, and the row stays offered (the app default outlives the scenes). It still **adopts a
solo camera as the app default** when no identity exists, so a clean layout offers Web Cam at once.
- **Dynamic why-gray tooltip:** `WebcamAddToolTip` (raised wherever the gate can flip: staging,
removal, startup lock success, first frame, camera failure, identity swap) — "Already in this
scene — one webcam per stream. A second camera means you've graduated to OBS." / "No webcam is
currently available — plug one in, or allow camera access in Windows." / "Adds the app default
webcam to this scene."
- **Good Dog integration tests** `WebcamMenuGateTests` (real app + temp DB + camera seams): (1) Chat's
identity does NOT gray Live; Add in Live places the same `wc-1` without a picker; scene-with-
placement stays gray; identity survives both removals (DB row count keeps 1, session kept by the
base lock). (2) **Negation:** identity loaded but its session can't start → row NOT offered.
`WebcamStartupResourceTests` test 1 strengthened: after startup the single camera is adopted →
`CanAddWebcam` true. **305 tests (304 pass + the known audio flake, see Landmines); build 0 warnings.**
**Task docs:** `TASKS.md` Open items + `ai.md` Webcam section updated (supersession recorded).
## ✅ Committed — layer-list drag-to-reorder persists (creator-reported, 2026-09-18)
The drag code in `Controls/LeftPanel.xaml.cs` reordered `StagedScene.Elements` directly
(`RemoveAt`/`Insert` in `List_PreviewMouseMove`), bypassing `SceneGraph`'s mutation surface — the
collection changed visually but nothing ever scheduled a save, so the new z-order was lost on
restart. Fix: the drag sets `_dragReordered` and `EndListDrag` funnels the drop through the new
`MainViewModel.OnSceneElementsReordered()` (Sources.cs) = `SceneGraph.InvalidateBake(staged) +
ScheduleSave()`, i.e. the same background-save path every other mutation uses. **Good Dog test**
`LayerReorderPersistenceTests` (real App + temp DB): reproduces the exact code-behind mutation,
calls the trap, pumps the dispatcher until the debounced save lands, asserts the DB `Source`
SortOrder matches the in-memory `Elements` order.
## ✅ Committed — Rename Recording dialog taller + full-screen hook unhooked on shutdown (2026-09-18)
- `RenameRecordingDialog.xaml` `Height` 230 → **276** (+20%, creator: textbox cut off). The fix is
real: content naturally needs ~225px, the old client area at 230 was ~193px (clipped), at 276 it's
~239px (fits). **Good Dog test** `RenameRecordingDialogSizingTests`: shows the real dialog in the
RealApp host and asserts the file-name textbox bottom edge is inside the client area. The dialog's
`Icon` was changed from root-relative `/Assets/...` to the explicit `/ytLive;component/...` (the
form `MainWindow.xaml` already uses) — root-relative only resolves in production via
`Application.ResourceAssembly`, so dialogs couldn't be constructed by tests otherwise. The other
dialogs still use the fragile form; fix as they get tested.
- **Teardown leak fix (dependency, discovered by the above):** `MainViewModel.Shutdown()` now calls
`_fullScreenDetector.StopWatching()`. The global `EVENT_SYSTEM_FOREGROUND` hook was never unhooked,
so after the VM was collected the next foreground event invoked a **garbage-collected
`WinEventProc` delegate → process crash** ("Test Run Aborted" in the suite). Harmless in production
(exits the process) but fatal to the multi-window test host when a test shows/foregrounds a window.
## ✅ Earlier committed — webcam resource lifecycle (startup slice, `1e4017d`)
Startup poll + tri-state (`ValidateWebcamResourceStartupAsync` after `LoadLayout`): 0 → run on,
no alarm; **1 → `AcquireAsync` as app-wide lock** (now always runs — see base-lock note above);
**≥2 → no auto-lock** (App Settings selector
next). Lock failure → **persistent red alert** (`WebcamLockAlert` in the Layers panel + Retry),
re-polls every 5 s (`_webcamLockPollTimer`), clears on lock success or any first real frame.
`CameraManager.IsRunning(deviceId)` = session exists (started OR starting). Camera test seams
`CameraEnumeratorOverride`/`CameraFrameSourceFactoryOverride` (mirror `LayoutPathOverride`).
Attribution correction committed: "NVIDIA Broadcast opens the webcam exclusively" is a *suspect-list*
claim — `CameraConflictProbe` reads process names only, no device handles; do not restate as fact.
## ⚠️ Open items
- **RenameRecordingDialog too short — DONE (see below).**
- **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 selector slice (user-queued):** when ≥2 webcams exist, add 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.** Also: the "Change Webcam" picker
should carry the OBS one-liner (one webcam per stream — a second means OBS).
- **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. 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 Live now OFFERS the Web Cam layer even while Chat holds the camera (the exact creator
goal: "webcam offered in the layer stack when I switch over to the Live view").
- **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` — a stale loopback sample still in the pipe
when the mute flips (phase 2's `Assert.Equal(0f, f, 6)` sees 0.4). **Confirmed pre-existing and
currently deterministic in this environment: it fails on clean `9d00955` (stash test) and on the
working tree, alone and in-suite.** Unrelated to camera work; do not "fix" it inside a webcam slice.
- **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.
- **Test-host crash (fixed 2026-09-18):** "callback was made on a garbage collected delegate …
`Win32FullScreenDetector+WinEventProc`" — `Shutdown()` now unhooks the foreground-event hook; never
re-introduce `StartWatching` without a matching teardown, and dialogs whose `Icon` is the
root-relative `/Assets/…` form can't be instantiated by tests (use `/ytLive;component/…`).
- **Scene.Elements layer tests:** any seeded scene with `HasBackground=1` loads a healed
`Background` element pinned at `Elements[0]` (created by `EnsureBackground` on `StagedScene` set) —
compute expected layer orders from the live collection, never hardcode indices.
- `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 webcam gate slice answers the creator's "go" ask — switching to the Live view
must now offer Web Cam in the layer stack (Chat holding the camera no longer blocks it). The red
Layers alert remains the new expected behavior to eyeball on the take: browser holds the camera →
fresh launch → alert (unless Chat's config grabbed it) that clears after a Retry once the browser
closes.