Files
LlamaCasty/HANDOFF.md
T
gramps f5a9d46881 TASK 36: composite the branding credit into the output, not just the preview
The credit existed only as a WPF BrandFlashLayer TextBlock in the preview at
25% opacity on a 300s timer. A viewer of the stream or the recording never saw
it, so "free tier shows branding" was not actually being delivered. The frame
pump has carried an unused per-frame flashFrame slot for exactly this.

Reverses the documented "Pre-GA posture" (ai.md Monetization), which kept the
flash preview-only so test VODs stayed clean. Creator ruling 2026-09-26: the
free tier has to be honest advertising, so it now reaches the broadcast.

One rendered VideoFrame feeds BOTH the frame pump and the preview, so the
creator's view cannot drift from what viewers get. Spec: 500ms in / 1000ms hold
/ 500ms out, first credit ~5s after go-live then every rand(30s)+30s, single
unwrapped line at a random spot inside the frame every time, neon white core
with a feathered red/blue halo.

Neon recipe is derivative work, per AGENTS.md: bright core + feathered
multi-radius halo with R and B split in opposite directions, from
https://nudaui.dev/components/neon-glow (layered text-shadow falloff),
https://help.maxon.net/rg/en-us/Content/html/Blurs-and-Glows-chromatic-glow.html
("with no displacement, the green channel is all but invisible behind your
white text" once R and B are split) and the OBS obs-stroke-glow-shadow
"feathered stroke with user-defined size and intensity". Neon blue is #4d8bff
rather than the theme's #0f3460, which renders near-black as a halo.

Also fixes a PRE-EXISTING bug found on the way: 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. SceneCompositor.Overlay is now internal (it copies before blitting, so
the bake is never mutated) and the shortcut composites overlays first.

The credit is deliberately NOT folded into SceneGraph.GetBakedBase:
SceneRegion.Matches compares element ids only, so a credit in the cached bake
would freeze there and never expire. Per-frame only, and Epoch is stamped
every read because the 8MB master buffer is recycled - without that the
paste cache would freeze a stale credit.

BrandFlashEnabled is now derived !IsPremium and non-assignable, and the
presenter re-checks the licence on every read, so a key entered mid-credit
cuts the advertisement on the next tick instead of letting it finish.

Tests: 10 new facts. 357 total, 356 pass; the one failure is the known
environment-flaky RealMouseDrag layer test (needs an uncovered desktop
session). Adds RealAppHost.RunAsync - a frame-pump test must await inside the
collection's shared STA thread or the whole suite deadlocks silently.

NOTE: scope-check.sh flags Helpers/InstanceProfile.cs, AppLog.cs,
TokenStore.cs, WebView2Manager.cs and InstanceIsolationTests.cs as outside
this commit. That is a false positive: it uses `git diff HEAD`, which cannot
distinguish a planned second commit in the same session from an unrelated
edit. Those files are the dev multi-instance unit, committed immediately
after this one - as is the InstanceProfile paragraph in ai.md, which shares a
file with the Monetization section corrected here.
2026-09-27 08:55:55 -07:00

79 lines
4.4 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** — two commits ahead of `origin/main`.
Push only when the creator says so.
## Two work units landed this session
### 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. Push the two commits when the creator asks.
2. Test-console chat UX (queued): don't clear the chat window, 20px right padding on the pull-out
input, Enter inserts a newline.
3. Decide the alert-gating copy question above.
4. Ship-checklist items live in `TASKS.md` → "1.0 gates".