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)); } }