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
This commit is contained in:
2026-11-02 10:47:48 -08:00
parent 6208cb9409
commit 20f059422b
20 changed files with 339 additions and 90 deletions
@@ -436,6 +436,68 @@ public sealed class AlertTickerPreviewDisplayTests
});
}
[Fact]
public void SceneTickersKeepIndependentSettings_AndShowQueueWhileAlertBoxShowsActiveEvent()
{
_app.Run(() =>
{
const int width = 680;
const int height = 48;
var alertBox = new Source
{
Name = "Stream Alerts",
Type = SourceType.AlertBox,
Width = width,
Height = height,
};
var tickerA = new Source
{
Name = "Ticker A",
Type = SourceType.Ticker,
Width = width,
Height = height,
};
var tickerB = new Source
{
Name = "Ticker B",
Type = SourceType.Ticker,
Width = width,
Height = height,
};
tickerA.Ticker.DisplayMethod = AlertDisplayMethod.Solid;
tickerB.Ticker.DisplayMethod = AlertDisplayMethod.Flash;
alertBox.Ticker.DisplayMethod = AlertDisplayMethod.Solid;
var firstScene = new Scene { Name = "Starting", Elements = { alertBox, tickerA } };
var secondScene = new Scene { Name = "BRB", Elements = { tickerB } };
using var layer = new AlertOverlayLayer(new AlertRenderer(), tickerPreviewSink: _ => { });
layer.UpdateConfiguredBoxes(new[] { firstScene, secondScene });
layer.Enqueue(new ChatMessage
{
AuthorName = "Active",
Kind = ChatEventKind.SuperChat,
SuperChatDisplayString = "$10.00",
});
layer.Enqueue(new ChatMessage
{
AuthorName = "Queued",
Kind = ChatEventKind.NewMember,
});
layer.Advance(0.7);
var activeAlert = layer.AlertBoxTickerFrame(alertBox);
var recentFeed = layer.RenderSceneTicker(tickerA);
Assert.NotSame(tickerA.Ticker, tickerB.Ticker);
Assert.Equal(AlertDisplayMethod.Solid, tickerA.Ticker.DisplayMethod);
Assert.Equal(AlertDisplayMethod.Flash, tickerB.Ticker.DisplayMethod);
Assert.NotNull(activeAlert);
Assert.NotNull(recentFeed);
Assert.Null(layer.RenderSceneTicker(tickerB));
Assert.False(activeAlert!.BgraPixels.SequenceEqual(recentFeed!.BgraPixels),
"the standalone feed must include the queued event while Stream Alerts shows only the active event");
});
}
/// <summary>A short alert box must clip the strip's rows, not overrun the target
/// buffer. The strip rasterises at its natural 48px, so a 20px-tall box used to be
/// a heap-corrupting write once the strip was composited at the box's size.</summary>