Files
LlamaCasty/ViewModels/MainViewModel.AlertTicker.cs
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

62 lines
2.4 KiB
C#

using ytLive.Services;
namespace ytLive.ViewModels;
public partial class MainViewModel
{
/// <summary>The alert announcement strip as the preview pane shows it: a
/// full-master-width bitmap, or null when no alert is playing (or a Flash is in
/// its off half). Mirrors the social bar's global-overlay pattern — the strip is
/// NOT a per-source frame, so it can't ride the alert box's
/// <c>VideoImageSource</c>.</summary>
private System.Windows.Media.Imaging.WriteableBitmap? _alertTickerBitmap;
public System.Windows.Media.Imaging.WriteableBitmap? AlertTickerImageSource
{
get => _alertTickerBitmap;
private set
{
_alertTickerBitmap = value;
OnPropertyChanged(nameof(AlertTickerImageSource));
}
}
public bool AlertTickerVisible => _alertTickerBitmap != null;
/// <summary>Publishes one ticker frame to the preview pane. Called on the UI
/// thread from the alert layer's own tick, so the WPF bitmap write is legal.
/// Reuses one <see cref="System.Windows.Media.Imaging.WriteableBitmap"/> and
/// overwrites its back buffer — a fresh bitmap every tick would churn the
/// render cache for no reason.</summary>
internal void PublishAlertTickerPreview(VideoFrame? frame)
{
if (frame == null)
{
if (_alertTickerBitmap != null)
{
AlertTickerImageSource = null;
OnPropertyChanged(nameof(AlertTickerVisible));
}
return;
}
if (_alertTickerBitmap == null
|| _alertTickerBitmap.PixelWidth != frame.Width
|| _alertTickerBitmap.PixelHeight != frame.Height)
{
AlertTickerImageSource = new System.Windows.Media.Imaging.WriteableBitmap(
frame.Width, frame.Height, 96, 96,
System.Windows.Media.PixelFormats.Bgra32, null);
OnPropertyChanged(nameof(AlertTickerVisible));
}
var target = _alertTickerBitmap!;
target.WritePixels(
new System.Windows.Int32Rect(0, 0, frame.Width, frame.Height),
frame.BgraPixels, frame.Stride, 0);
// The bitmap instance is unchanged, so the pane's Source binding still
// points at it — nudge it so WPF re-reads the back buffer this tick.
OnPropertyChanged(nameof(AlertTickerImageSource));
}
}