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.
This commit is contained in:
+68
-99
@@ -1,109 +1,78 @@
|
||||
# HANDOFF — 2026-09-26, end of session (take four: ticker display methods)
|
||||
# HANDOFF — current state
|
||||
|
||||
## Where we are
|
||||
`main`, **5 local commits ahead of `origin/main` (= `ecb329e`), NOTHING PUSHED.**
|
||||
Last pushed commit is still `ecb329e`. Do not push without the creator saying so.
|
||||
**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.
|
||||
|
||||
| commit | what |
|
||||
|---|---|
|
||||
| `a11b15e` | TASK 47 alert video (first landing) |
|
||||
| `7b940b6` | poller-thread marshal crash fix |
|
||||
| `80038ff` | silent-decoder fallback + diagnostics |
|
||||
| `aea0670` | video pacing via `FfmpegFrameRateProbe` |
|
||||
| `e2ecc24` | **the real alert fix: `PreviewPane.xaml` `IsAlertBox` trigger** + 2 Good Dog tests + resolver/preview diagnostics |
|
||||
| (last, unpushed) | **alert announcement strip: visible in the preview + 3 display methods** (Scroll / Flash / Solid), paced in reads-per-alert |
|
||||
## Two work units landed this session
|
||||
|
||||
## THE ANSWER, take two (do not re-derive this)
|
||||
The **ticker was output-only**. `AlertTickerFrame` existed solely as a frame-pump
|
||||
callback (blitted at 0,0 into the OUTPUT). `PreviewPane.xaml` had no element for it
|
||||
because the strip is master-width and GLOBAL, not a `Source`, so it cannot ride a
|
||||
per-element `Image`. Nothing was broken in the renderer — there was simply no consumer
|
||||
in the preview. Fixed with a `tickerPreviewSink` on `AlertOverlayLayer` (published from
|
||||
`RefreshAlertPreviews()`, UI thread only) + `MainViewModel.AlertTickerImageSource` +
|
||||
a global `AlertTickerElement` in `PreviewPane.xaml` mirroring `SocialBarElement`.
|
||||
### 1. Branding flash is now composited into the OUTPUT (TASK 36 shipped)
|
||||
|
||||
The marquee was also **unreadably paced**: a fixed `140px/s` needed ~17s per pass, so a
|
||||
10s alert showed the message barely once, entering from the right and never crossing. It
|
||||
is now paced in **reads per alert** (`TickerReadsPerAlert = 3` inside the alert's own
|
||||
length), which is the incumbent's own unit (Streamlabs: "Alert Duration" + "Text Delay",
|
||||
never a scroll-speed slider).
|
||||
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:
|
||||
|
||||
## THE ANSWER (do not re-derive this)
|
||||
The alert video was **never drawn**, never mis-decoded. Build 90's diagnostics showed
|
||||
`Alert preview: frame=680x200 a255` and `Output resolver: frame 680x200 playing=True`
|
||||
every second for the whole clip — a real, opaque, correctly-sized frame reaching BOTH
|
||||
consumers. `Controls/PreviewPane.xaml` keeps the per-element `Image` `Collapsed` unless a
|
||||
DataTrigger fires, and there was **no `IsAlertBox` trigger** (only IsImageSource /
|
||||
IsChatBox / IsWebSource / IsWebcam). Chat boxes rendered; alert boxes never could.
|
||||
Removing/re-adding the layer could not have fixed it.
|
||||
- `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.
|
||||
|
||||
Clip itself is fine: DB asset `941785b0-d022-45fa-a8a2-2cd59ae2ba48` is byte-identical
|
||||
(md5 `0ee1f4960dd3b23dee5930a2af79d410`) to the creator's
|
||||
`C:\Users\gramp\Downloads\llamacasty-dancingLlama-thankyou.mp4` — h264 1280x720 10s,
|
||||
every sampled frame full bright content.
|
||||
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.
|
||||
|
||||
## Working tree
|
||||
Clean apart from the uncommitted work unit above (it is committed; check `git status`).
|
||||
If the app is running, `ytLive.dll`/`.exe` are LOCKED and any build fails with MSB3021/
|
||||
MSB3027 — the creator must close ytLive first. That is normal, not a broken build.
|
||||
### 2. Dev-only: two instances side by side
|
||||
|
||||
## Build / test commands (Windows host, always)
|
||||
```bash
|
||||
"/mnt/c/Program Files/dotnet/dotnet.exe" build "C:\Users\gramp\Documents\Code\projects\ytLive\ytLive.csproj" --no-restore
|
||||
"/mnt/c/Program Files/dotnet/dotnet.exe" vstest "C:\Users\gramp\Documents\Code\projects\ytLive\ytLive.Tests\bin\Debug\net8.0-windows10.0.19041.0\ytLive.Tests.dll"
|
||||
```
|
||||
Last full suite: **339/339 pass** (15 new facts for the ticker feature). Run it with ytLive CLOSED or the RealMouseDrag env
|
||||
flake fires.
|
||||
`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.
|
||||
|
||||
## Queued next (creator's words, 2026-09-26) — the Good Dog queue
|
||||
1. **Test-console chat must not clear the chat window.** "when I add a chat message from
|
||||
the test console, or from any console, that chat text should not clear the current
|
||||
chat window."
|
||||
2. **Test pull-out chat input: 20px right padding** — the text box is clipped at the right
|
||||
edge of the div; needs 20px padding against the div's right side.
|
||||
3. **Test pull-out chat input: Enter inserts a newline**, it must not send.
|
||||
4. ~~Verify the alert video live~~ — **DONE, the creator confirmed it plays.**
|
||||
5. **Verify the three ticker display methods live** (Scroll / Flash / Solid) and pick a
|
||||
default. Flash is 0.5s on / 0.5s off; Solid is centred; Scroll does 3 passes per 10s
|
||||
alert. Nothing here has been SEEN on screen yet — the tests prove pixels and bindings,
|
||||
not the look.
|
||||
6. **Decide whether the once-per-second alert diagnostics stay.** `ViewModels/MainViewModel.cs`
|
||||
(`ResolveOutputFrame`) and `Services/AlertOverlayLayer.cs` (`UpdatePreview`) still log
|
||||
`Alert preview:` / `Output resolver:` once a second while an alert plays. They earned
|
||||
their keep; they are noise in normal use. Creator's call.
|
||||
**Usage:** `$env:YTLIVE_INSTANCE=2; dotnet run`
|
||||
|
||||
## Landmines / facts worth keeping
|
||||
- The pinned BtbN ffmpeg has **no libx264** — generate test clips with `-c:v mpeg4`.
|
||||
- **A bound `ItemsSource` ComboBox in `LeftPanel.xaml` breaks
|
||||
`LayerReorderPersistenceTests.RealMouseDrag_OnTheLayerList_PersistsTheReorder`** (it
|
||||
injects PHYSICAL mouse input, so a load-time re-measure moves the rows out from under
|
||||
the cursor). Use inline `<ComboBoxItem>`s like the chat Font selector does. If a UI-only
|
||||
change breaks a real-input test, suspect the control's binding STYLE first, and bisect
|
||||
with `git checkout <the one xaml file>`.
|
||||
- The `Source` row is read back by **POSITIONAL** index in `LayoutStore.Load.cs`
|
||||
(`GetInt32(32..34)`). Append new Source columns to the END of the SELECT, or a
|
||||
mid-list insert silently shifts a neighbour instead of failing to load. New columns use
|
||||
the idempotent `PRAGMA table_info` guard in `LayoutStore.Migrations.cs` (no
|
||||
`user_version` bump needed).
|
||||
- **A seamless marquee never goes blank** (the wrapped copy enters from the right as the
|
||||
pill clears the left), so an "empty frame" cannot count a pass — count the pill's
|
||||
leading edge resetting to the right edge.
|
||||
- `SceneCompositor.Render` fills its base with **opaque black**, so "any non-zero byte"
|
||||
is not proof a layer painted. Compare against an idle render, or count COLOURFUL px.
|
||||
- `sqlite3` on WSL: `/home/gramps/android-sdk/platform-tools/sqlite3`. Extract a BLOB with
|
||||
`SELECT writefile('/tmp/opencode/x.bin', Data) FROM Asset WHERE Id='…'` (Linux paths
|
||||
only — a `C:/…` path creates a junk `C:` dir IN THE REPO; one was made and removed).
|
||||
- Layout DB: `/mnt/c/Users/gramp/AppData/Roaming/ytLlive/ytLlive.db` (tables: `Asset`
|
||||
`Id/Hash/Data`, `Settings`, `Source`, `Scene`, …). Alert default video setting key:
|
||||
`AlertDefaultVideoAssetId`. ffmpeg/ffprobe live in
|
||||
`/mnt/c/Users/gramp/AppData/Roaming/ytLlive/tools/` (run the `.exe` directly from WSL).
|
||||
- Two diagnostics were ADDED and are still in the tree (once-per-second, alert only):
|
||||
`Output resolver: alert box …` (MainViewModel.cs) and `Alert preview: box …`
|
||||
(AlertOverlayLayer.cs). Decide with the creator whether to keep them or strip them now
|
||||
that the root cause is known.
|
||||
- `PreviewPane.xaml` has the same latent gap for `IsMediaSource` (no trigger either) —
|
||||
NOT fixed (out of scope), noted as a follow-up.
|
||||
- Known perf item, untouched: FramePump stalls ~140-180ms renders
|
||||
(`render=full-render split=0 … totalMs=140`). Unrelated to the alert bug.
|
||||
- YouTube rejects the `complete` transition (403) at end-of-stream; `enableAutoStop`
|
||||
finishes the broadcast. Harmless, already handled.
|
||||
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".
|
||||
|
||||
Reference in New Issue
Block a user