using ytLive.Services;
namespace ytLive.ViewModels;
public partial class MainViewModel
{
/// The alert announcement strip as the preview pane shows it: a bitmap the
/// size of the Stream Alerts box, or null when no alert is playing (or a Flash is
/// in its off half).
/// Creator ruling 2026-09-26: the strip draws INSIDE the alert box, so
/// the pane positions and sizes this element to the box's own rect
/// (//
/// /) rather than
/// pinning a 1920px bar to the top edge of the preview. The frame carries its
/// origin on , which is the same number the
/// encoder composites at — so the preview cannot drift from the broadcast, exactly
/// like the branding credit.
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;
/// Alert-box rect (master pixels) the ticker element is placed at in the
/// preview canvas. Set from the published frame, so preview and output agree.
public double AlertTickerLeft { get; private set; }
public double AlertTickerTop { get; private set; }
public double AlertTickerWidth { get; private set; }
public double AlertTickerHeight { get; private set; }
/// 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 and
/// overwrites its back buffer — a fresh bitmap every tick would churn the
/// render cache for no reason.
internal void PublishAlertTickerPreview(VideoFrame? frame)
{
if (frame == null)
{
if (_alertTickerBitmap != null)
{
AlertTickerImageSource = null;
OnPropertyChanged(nameof(AlertTickerVisible));
}
return;
}
// The box can be moved or resized mid-alert, and the element has to track it —
// raise the rect unconditionally rather than only on a bitmap resize.
if (AlertTickerLeft != frame.OriginX) { AlertTickerLeft = frame.OriginX; OnPropertyChanged(nameof(AlertTickerLeft)); }
if (AlertTickerTop != frame.OriginY) { AlertTickerTop = frame.OriginY; OnPropertyChanged(nameof(AlertTickerTop)); }
if (AlertTickerWidth != frame.Width) { AlertTickerWidth = frame.Width; OnPropertyChanged(nameof(AlertTickerWidth)); }
if (AlertTickerHeight != frame.Height) { AlertTickerHeight = frame.Height; OnPropertyChanged(nameof(AlertTickerHeight)); }
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));
}
}