Files
LlamaCasty/HANDOFF.md
T
gramps 9761b1d4be branding credit: run in every scene and in recordings, not only while live
Creator 2026-09-26: "the made with llamacasty flash should appear in all scenes,
not just live" and "should also appear in recordings". The presenter was
Start()/Stop()-ed from UpdateLiveVisuals()'s IsLive branch, so a recording made
WITHOUT ever going live carried no credit at all -- the exact case the creator hit
while judging compositing from local recordings.

It is now Start()ed once in the MainViewModel ctor and never stopped on live-state
churn. One start covers every scene, the preview, the stream and the recording,
because go-live and local recording are the SAME FramePump: both
Streaming.Operations.cs:68 and :199 call StartAsync with the same brandFlash:
delegate. Verified by reading both call sites, not assumed -- there is no second
encoder path that needed a "redirect". The licence gate needs no live branch at
all: IsPremium's setter already pushes BrandFlashPresenter.Enabled from anywhere.

Fixed a trap that app-lifetime exposed: Enabled = false stops the presenter's
DispatcherTimer to cut the advertisement mid-credit. When Start() was per-go-live
the next go-live restarted it; with a single app-lifetime Start() nothing would,
so a key entered mid-session would leave the credit dead until the process was
restarted. The setter now restarts the timer when re-enabling while _running.

Tests (12 facts in BrandFlashOutputTests, +2):
  Credit_IsComposited_WithNoLiveSession_AndSoARecordingCarriesIt -- drives a real
    never-live VM past the 5s first-flash delay and asks the frame the pump would
    composite. Uses a new internal AdvanceBrandFlash seam; because Advance only
    advances the cadence while the presenter is running, a credit coming out
    proves Start() happened at construction.
  ADowngradeMidSession_RestartsTheCadenceTimer -- asserts the premium/downgrade
    edge decision directly (IsCadenceTimerEnabled) instead of sleeping through a
    30-60s interval, which a synchronous test body cannot observe.

Full suite 364/364.
2026-09-27 09:33:30 -07:00

131 lines
8.1 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 — current state
**Branch:** `main` (pre-1.0, no feature branches). **Last pushed:** `7f14ffb`.
**This session's work is COMMITTED LOCALLY, NOT PUSHED** — **four** commits ahead of `origin/main`
(`fae8ec5`, `5aedca7`, `f5a9d46`, `de81fa3`) plus this unit. Push only when the creator says so.
## The 2026-09-26 proof-of-concept round (creator reporting)
Context: **no YouTube private test recordings are being saved**, so the creator is judging
compositing from **local recordings**. They assumed the live path needed a separate "redirect" —
**it does not, and that is verified, not assumed:** one `_framePump` is constructed in the
`MainViewModel` ctor with a single `brandFlash:` callback, and both `Streaming.Operations.cs:68`
(go-live) and `:199` (record) call that same `StartAsync`. Record+simulcast is one ffmpeg with two
outputs. Five items, tracked in `TASKS.md` → "Creator-reported batch".
### Done: the branding flash is no longer a go-live-only behaviour
The presenter was `Start()`/`Stop()`-ed from `UpdateLiveVisuals()`'s `IsLive` branch, so a
recording made **without ever going live** carried no credit — exactly the creator's complaint.
It is now started **once in the `MainViewModel` ctor** and never stopped on live-state churn, so it
runs in every scene, reaches the preview, and rides the same `FramePump` into the stream and the
recording. The licence gate needs no live branch: `IsPremium`'s setter already pushes
`BrandFlashPresenter.Enabled`.
**Trap that came with it (fixed):** `Enabled = false` stops the presenter's `DispatcherTimer`.
When `Start()` was per-go-live, the next go-live restarted it; with one app-lifetime `Start()`
nothing would, so a key entered mid-session would leave the credit dead until the process
restarted. The setter now restarts the timer when re-enabling while `_running`.
Test-arithmetic trap, hit twice now: `Advance(5.1)` in one call can never observe a credit —
`Advance` opens *and* ages the presentation by the same delta, so it jumps the 2s window. Step at
1/30s like the real timer (the `Step` helpers).
### Done this unit: recording save dialog — Cancel now discards
`RenameRecordingDialog` was always correct (Enter → `DialogResult=true`, Cancel/X/Escape → `false`).
**The caller was not**: `FinalizeRecordingAsync` only overwrote `stem` when the dialog returned true
and then ran `File.Move` **unconditionally**, so a cancelled dialog silently saved the recording
under the default name. Extracted the decision into internal
`MainViewModel.CompleteRecordingSave(startPath, dir, autoStem, chosenStem, creatorSaved)`:
- `creatorSaved == false` (Cancel/Escape/X) → **delete the temp file**; the videos folder is left
empty. A failed delete returns `DiscardFailed` and the toast names the file + folder.
- `creatorSaved == true` (Save, or Enter on the pre-filled default) → `File.Move` to the final name.
Blank box still means "keep the auto name" — but only on an explicit Save.
Test: `ytLive.Tests/RecordingSaveDialogTests.cs` (5 facts, real temp files, no WPF needed) — the
headline asserts Cancel leaves the directory **empty**, not merely "the stem is unchanged".
Lesson recorded in `MyMistakes.md` (falsy `ShowDialog()` falling through into a side effect).
## Two earlier work units (committed)
### 1. Branding flash is now composited into the OUTPUT (TASK 36 shipped)
It used to be a WPF `BrandFlashLayer` TextBlock in the preview at 25% opacity on a 300s timer — a
credit the creator could see and **no viewer ever could**. Now one rendered `VideoFrame` goes to
**both** the frame pump and the preview, so they cannot drift:
- `Services/Compositor/BrandFlashPresenter.cs` (new) — neon raster (white core + feathered red/blue
halo, channels split opposite), 2s envelope, random on-canvas placement per presentation,
licence gate re-checked on every read.
- `ViewModels/MainViewModel.BrandFlash.cs` (new) — `BrandFlashFrame()` for the pump +
`PublishBrandFlashPreview()` for the pane.
- `FramePump` gained `brandFlash:` → the existing per-frame `flashFrame` slot, and mixes it into the
C4 cache signature. `SceneCompositor.Overlay` is now `internal` for the shortcut path.
- `PreviewPane.xaml`: `BrandFlashLayer` → `BrandFlashElement` (bound to the published frame).
- `BrandFlashEnabled` is derived `!IsPremium` and no longer assignable.
Spec met: 500ms in / 1000ms hold / 500ms out, first credit ~5s after go-live then every
`rand(30s)+30s`, single unwrapped line, random spot every time, fully on canvas.
### 2. Dev-only: two instances side by side
`Helpers/InstanceProfile.cs` (new). Set `YTLIVE_INSTANCE=<id>` and that process gets a private
`%APPDATA%\ytLlive\instances/<id>/` root for the **layout DB, auth file, startup.log and the
WebView2 user data folder**. Chromium locks that folder exclusively — without this the second
instance does not start at all. Recording folder and the ffmpeg `tools` cache stay shared on
purpose; global hotkeys stay un-namespaced.
**Usage:** `$env:YTLIVE_INSTANCE=2; dotnet run`
The entire implementation is inside `#if DEBUG`. Release compiles to `DataRoot => DefaultRoot` +
`WebViewDataFolder => null`; the call sites are unconditional so Release cannot drift.
## Bugs found and fixed along the way
- **Pre-existing, not mine:** `FramePump`'s **fully-static shortcut** stretched the cached bake and
returned it, silently discarding **every per-frame overlay** — the social bar and the alert ticker
were already lost there, not just the flash. Now composites overlays onto a copy before stretching.
- **Cadence bug caught by my own test:** the interval was an absolute deadline, not a period, so
gaps collapsed (6.7s / 3.5s / 2.8s instead of 30–60s). Fixed.
- `BrandFlashPresenter` recycles one 8MB master buffer and **must** stamp `VideoFrame.Epoch` per
read, or the paste cache freezes the credit. It does.
## Test state — VERIFIED
- **357 total, 356 pass.** 18 new facts (10 brand flash, 8 instance isolation).
- The one failure is `LayerReorderPersistenceTests.RealMouseDrag_OnTheLayerList_PersistsTheReorder`,
and it is **flaky by environment, not a regression** — 2 fail / 1 pass in isolation. Its own doc
comment: "Requires an interactive desktop session: if the window is covered or the session is
locked, the pointer no-ops and the drag never lands." The symptom matches exactly (order
unchanged). **Run the suite with ytLive CLOSED** (see `MyMistakes.md` 2026-08 era note).
- `RealAppHost.RunAsync` was added for this: a frame-pump test MUST `await` inside it. A blocking
wait in `RealAppHost.Run` occupies the one shared STA thread and hangs the whole suite with no
output. **Always background a `vstest` run and poll the log** — foreground piped `vstest` returns
nothing in this shell even on success.
## Landmines
- A stale `ytLive.exe` (PID 2544) locked `bin/.../ytLive.exe` and broke `dotnet run` with MSB3027.
Killed. **If the build fails to copy `ytLive.exe`, check for a running instance first.**
- `GlobalHotkeys`: two instances registering the SAME global hotkey — Windows refuses the second.
Left alone deliberately.
- `OverlayHost.xaml` says Polar unlocks alerts too; alerts are not gated. Unresolved, queued.
## Next
1. **Confine the ticker to the alert box** (#3) — it is still a global 1920×48 top overlay
(`BlitOverlay(…, 0, 0)`); needs a rect + scale/clip decision in both preview and output.
2. **Multi-instance** (#4) — route `FfmpegLocator._toolsDir` through `InstanceProfile`, and confirm
the launch method: `dotnet run` while the first app holds `bin/…/ytLive.exe` fails with MSB3027
*before any instance starts* (a build-output lock, not a log conflict). `$env:YTLIVE_INSTANCE=2`
+ `dotnet run --no-build` in a second shell is the known-good path.
3. **Post-session efficacy report** (#5) — roll up `CurrentHealth` (dropped frames, duration, health
message) when a stream or recording ends.
4. Push the commits when the creator asks.
5. Test-console chat UX (queued): don't clear the chat window, 20px right padding on the pull-out
input, Enter inserts a newline.
6. Decide the alert-gating copy question above.
7. Ship-checklist items live in `TASKS.md` → "1.0 gates".