Files
LlamaCasty/ViewModels/MainViewModel.AlertTicker.cs
T
gramps 20f059422b feat(alerts): add scene-local recent ticker
Model the AlertBox and standalone ticker with shared settings; scene tickers render the in-memory active and queued events without saving history. Keep AlertBox content active-event-only.

Ref: https://obsproject.com/kb/obs-studio-overview
2026-11-02 10:47:48 -08:00

94 lines
4.3 KiB
C#

using System.Linq;
using ytLive.Models;
using ytLive.Services;
namespace ytLive.ViewModels;
public partial class MainViewModel
{
/// <summary>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).
/// <para><b>Creator ruling 2026-09-26:</b> the strip draws INSIDE the alert box, so
/// the pane positions and sizes this element to the box's own rect
/// (<see cref="AlertTickerLeft"/>/<see cref="AlertTickerTop"/>/
/// <see cref="AlertTickerWidth"/>/<see cref="AlertTickerHeight"/>) rather than
/// pinning a 1920px bar to the top edge of the preview. The frame carries its
/// origin on <see cref="VideoFrame.Placement"/>, which is the same number the
/// encoder composites at — so the preview cannot drift from the broadcast, exactly
/// like the branding credit.</para></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>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.</summary>
public double AlertTickerLeft { get; private set; }
public double AlertTickerTop { get; private set; }
public double AlertTickerWidth { get; private set; }
public double AlertTickerHeight { get; private set; }
/// <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? incomingFrame)
{
var frame = incomingFrame;
var previewScene = IsEditMode ? StagedScene : LiveScene ?? StagedScene;
var alertBox = previewScene?.Elements.OfType<Source>()
.FirstOrDefault(source => source.Type == SourceType.AlertBox && source.IsVisible);
if (_alertLayer.IsPlaying)
frame = _alertLayer.AlertBoxTickerFrame(alertBox);
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));
}
}