c11788574e
List_PreviewMouseMove reorders StagedScene.Elements via RemoveAt/Insert, bypassing SceneGraph's mutation surface, but never scheduled a save — the new z-order was lost on restart. The drop now sets _dragReordered and EndListDrag funnels it through MainViewModel.OnSceneElementsReordered() (InvalidateBake + ScheduleSave, same background-save path as every other mutation). Integration test (RealApp + temp DB) reproduces the exact code-behind mutation and asserts the debounced save lands the new Source SortOrder. Derivation reference: standard WPF ItemsControl drag-reorder pattern (OBS layering semantics: bottom-most layer = index 0).
159 lines
12 KiB
Markdown
159 lines
12 KiB
Markdown
# HANDOFF — 2026-09-18 (layer drag-reorder persistence fix committed locally — no push)
|
||
|
||
## Branch / Commit State
|
||
|
||
`main` HEAD = **layer drag-reorder now traps + persists** (local, this session — the new
|
||
`LayerReorderPersistenceTests`). Below it:
|
||
|
||
- `75723ac` fix(webcam): offer the Web Cam row only when a camera is attainable (live lock)
|
||
- `9d00955` feat(webcam): app-default gate slice — per-scene offer, Add places default directly,
|
||
identity survives removal
|
||
- `1e4017d` feat(webcam): resource lifecycle startup slice — poll-on-start, single-cam lock,
|
||
persistent Layers alert
|
||
|
||
**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.
|
||
|
||
## ✅ 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 (creator-reported, QUEUED next):** the recording save/confirm
|
||
dialog cuts off the file-name textbox — increase its height by ~20% (+ its one integration test,
|
||
Good Dog).
|
||
- **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.
|
||
- **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.
|