Files
LlamaCasty/HANDOFF.md
T
gramps 7f14ffb11e TASK 47 take four: alert ticker visible in the preview + 3 display methods
The creator confirmed the clip fix ("the video plays now"), then asked where the
scrolling text was — it was invisible, for a structural reason. AlertTickerFrame
existed only as a frame-pump callback blitted 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 wrong in the renderer: there was no
consumer in the preview. Same class of defect as the missing IsAlertBox trigger, one
layer up (MyMistakes RULE 5/6).

- tickerPreviewSink on AlertOverlayLayer, published from RefreshAlertPreviews() so it
  is always the UI thread; MainViewModel.AlertTicker writes it into one reused
  WriteableBitmap bound to a new global AlertTickerElement, mirroring SocialBarElement.
- Source.AlertDisplayMethod + panel "Display" selector: TickerScroll / Flash / Solid.
  Flash pulses 0.5s on / 0.5s off for the whole alert; Solid is centred and still.
- The marquee was also unreadable: a fixed 140px/s took ~17s per pass, so a 10s alert
  showed the text once, entering from the right and never crossing. Paced in reads per
  alert instead (TickerReadsPerAlert = 3 inside the alert's own length, speed derived
  from it) — never px/s. Research (websearch: how do OBS/Streamlabs/StreamElements
  alert boxes present announcement timing?) settled the unit: Streamlabs exposes "Alert
  Duration: choose how long your alert stays on your stream" and "Text Delay", never a
  scroll-speed slider (https://support.streamlabs.com/hc/en-us/articles/52499995174299-Setting-up-Your-Streamlabs-Alerts).
  Run is phase-started half a frame in so the first frame isn't blank.
- Persistence: AlertDisplayMethod INTEGER NOT NULL DEFAULT 0 via the idempotent
  table_info migration, appended LAST in the SELECT because the Source reader is
  positional (GetInt32(32..34)) — a mid-list insert would silently shift a neighbour.

Tests: 15 new facts (suite 339/339). RealApp STA host: the pane draws the strip and
collapses at alert end; the layer publishes a real 1920x48 frame for all three methods
and nothing when the ticker is off; three passes counted in 10s by the pill's leading
edge resetting (a seamless marquee never blanks, so an empty frame cannot count a pass);
Solid byte-identical at every moment; Flash on for half of each second; the panel shows
and writes back the choice; the DB round-trips all three alert fields together.

Incidental finding: a bound ItemsSource ComboBox in LeftPanel.xaml broke
LayerReorderPersistenceTests.RealMouseDrag (that test injects PHYSICAL mouse input, so a
load-time re-measure moves the rows out from under the cursor). Rewritten as inline
ComboBoxItems, the shape the chat Font selector already uses in that panel. Recorded as
MyMistakes RULE (8).
2026-09-26 15:10:26 -07:00

7.1 KiB

HANDOFF — 2026-09-26, end of session (take four: ticker display methods)

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.

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

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.

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).

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.

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.

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.

Build / test commands (Windows host, always)

"/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.

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.

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.