feat(chat-overlay): add ChatBox config UI + schema persistence
- Elements panel config UI for ChatBox source (font size slider, font/member/super-chat/background color inputs, max messages slider, timestamp/badge toggles) - Schema migration: 8 new ChatBox columns on Source table (ChatFontSize, ChatFontColor, ChatBackgroundColor, ChatMaxMessages, ChatShowTimestamps, ChatShowBadges, ChatMemberColor, ChatSuperChatColor) - Load/save: Source SELECT and INSERT now include all ChatBox fields - IsElementsPanelEmpty excludes ChatBox so hint doesn't cover config - ChatBoxRenderer.cs created: WPF FormattedText + RenderTargetBitmap renderer for on-stream chat overlay - Slider bindings set to TwoWay for live propagation
This commit is contained in:
@@ -251,6 +251,8 @@
|
|||||||
ToolTip="Timer overlay — Starting and BRB scenes only"/>
|
ToolTip="Timer overlay — Starting and BRB scenes only"/>
|
||||||
<MenuItem Header="Web" Command="{Binding AddSourceCommand}" CommandParameter="{x:Static models:SourceType.WebSource}"
|
<MenuItem Header="Web" Command="{Binding AddSourceCommand}" CommandParameter="{x:Static models:SourceType.WebSource}"
|
||||||
ToolTip="Embedded web page — set URI in Elements panel"/>
|
ToolTip="Embedded web page — set URI in Elements panel"/>
|
||||||
|
<MenuItem Header="Chat" Command="{Binding AddSourceCommand}" CommandParameter="{x:Static models:SourceType.ChatBox}"
|
||||||
|
ToolTip="YouTube live chat overlay on stream"/>
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
</Button.ContextMenu>
|
</Button.ContextMenu>
|
||||||
<Path Data="M12,4 L12,20 M4,12 L20,12" Stroke="#e94560" StrokeThickness="2"
|
<Path Data="M12,4 L12,20 M4,12 L20,12" Stroke="#e94560" StrokeThickness="2"
|
||||||
@@ -473,6 +475,67 @@
|
|||||||
</DockPanel>
|
</DockPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- Chat box config -->
|
||||||
|
<StackPanel Visibility="{Binding SelectedElement.IsChatBox, Converter={StaticResource BoolToVis}}" Margin="0,0,0,8">
|
||||||
|
<TextBlock Text="Chat Overlay" Foreground="#a0a0b0" FontSize="11" FontWeight="SemiBold" Margin="0,0,0,6"/>
|
||||||
|
|
||||||
|
<!-- Font Size -->
|
||||||
|
<DockPanel Margin="0,0,0,4">
|
||||||
|
<TextBlock DockPanel.Dock="Left" Text="Font Size" Foreground="#a0a0b0" FontSize="11" VerticalAlignment="Center" Width="70"/>
|
||||||
|
<Slider Minimum="8" Maximum="72" TickFrequency="2" IsSnapToTickEnabled="True"
|
||||||
|
Value="{Binding SelectedElement.ChatFontSize, Mode=TwoWay}"
|
||||||
|
Background="#16213e" Foreground="#e94560"/>
|
||||||
|
<TextBlock DockPanel.Dock="Right" Text="{Binding SelectedElement.ChatFontSize, StringFormat={}{0:0}}" Foreground="#e0e0e0" FontSize="11" Margin="6,0,0,0" VerticalAlignment="Center" Width="24"/>
|
||||||
|
</DockPanel>
|
||||||
|
|
||||||
|
<!-- Font Color -->
|
||||||
|
<DockPanel Margin="0,0,0,4">
|
||||||
|
<TextBlock DockPanel.Dock="Left" Text="Font Color" Foreground="#a0a0b0" FontSize="11" VerticalAlignment="Center" Width="70"/>
|
||||||
|
<TextBox Text="{Binding SelectedElement.ChatFontColor, UpdateSourceTrigger=PropertyChanged}"
|
||||||
|
Background="#0f3460" Foreground="#e0e0e0" BorderThickness="1"
|
||||||
|
BorderBrush="#2c3a5c" Padding="4,2" FontSize="11" Width="80"/>
|
||||||
|
</DockPanel>
|
||||||
|
|
||||||
|
<!-- Member Color -->
|
||||||
|
<DockPanel Margin="0,0,0,4">
|
||||||
|
<TextBlock DockPanel.Dock="Left" Text="Member" Foreground="#a0a0b0" FontSize="11" VerticalAlignment="Center" Width="70"/>
|
||||||
|
<TextBox Text="{Binding SelectedElement.ChatMemberColor, UpdateSourceTrigger=PropertyChanged}"
|
||||||
|
Background="#0f3460" Foreground="#e0e0e0" BorderThickness="1"
|
||||||
|
BorderBrush="#2c3a5c" Padding="4,2" FontSize="11" Width="80"/>
|
||||||
|
</DockPanel>
|
||||||
|
|
||||||
|
<!-- Super Chat Color -->
|
||||||
|
<DockPanel Margin="0,0,0,4">
|
||||||
|
<TextBlock DockPanel.Dock="Left" Text="Super Chat" Foreground="#a0a0b0" FontSize="11" VerticalAlignment="Center" Width="70"/>
|
||||||
|
<TextBox Text="{Binding SelectedElement.ChatSuperChatColor, UpdateSourceTrigger=PropertyChanged}"
|
||||||
|
Background="#0f3460" Foreground="#e0e0e0" BorderThickness="1"
|
||||||
|
BorderBrush="#2c3a5c" Padding="4,2" FontSize="11" Width="80"/>
|
||||||
|
</DockPanel>
|
||||||
|
|
||||||
|
<!-- Max Messages -->
|
||||||
|
<DockPanel Margin="0,0,0,4">
|
||||||
|
<TextBlock DockPanel.Dock="Left" Text="Max Msgs" Foreground="#a0a0b0" FontSize="11" VerticalAlignment="Center" Width="70"/>
|
||||||
|
<Slider Minimum="1" Maximum="20" TickFrequency="1" IsSnapToTickEnabled="True"
|
||||||
|
Value="{Binding SelectedElement.ChatMaxMessages, Mode=TwoWay}"
|
||||||
|
Background="#16213e" Foreground="#e94560"/>
|
||||||
|
<TextBlock DockPanel.Dock="Right" Text="{Binding SelectedElement.ChatMaxMessages}" Foreground="#e0e0e0" FontSize="11" Margin="6,0,0,0" VerticalAlignment="Center" Width="24"/>
|
||||||
|
</DockPanel>
|
||||||
|
|
||||||
|
<!-- Background Color -->
|
||||||
|
<DockPanel Margin="0,0,0,4">
|
||||||
|
<TextBlock DockPanel.Dock="Left" Text="Background" Foreground="#a0a0b0" FontSize="11" VerticalAlignment="Center" Width="70"/>
|
||||||
|
<TextBox Text="{Binding SelectedElement.ChatBackgroundColor, UpdateSourceTrigger=PropertyChanged}"
|
||||||
|
Background="#0f3460" Foreground="#e0e0e0" BorderThickness="1"
|
||||||
|
BorderBrush="#2c3a5c" Padding="4,2" FontSize="11" Width="80"/>
|
||||||
|
</DockPanel>
|
||||||
|
|
||||||
|
<!-- Toggles -->
|
||||||
|
<CheckBox Content="Show timestamps" Foreground="#a0a0b0" FontSize="11" Margin="0,4,0,2"
|
||||||
|
IsChecked="{Binding SelectedElement.ChatShowTimestamps, Mode=TwoWay}"/>
|
||||||
|
<CheckBox Content="Show badges" Foreground="#a0a0b0" FontSize="11" Margin="0,0,0,2"
|
||||||
|
IsChecked="{Binding SelectedElement.ChatShowBadges, Mode=TwoWay}"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
<!-- Placeholder hint when no config available -->
|
<!-- Placeholder hint when no config available -->
|
||||||
<TextBlock Text="No configurable elements" Foreground="#555" FontSize="11"
|
<TextBlock Text="No configurable elements" Foreground="#555" FontSize="11"
|
||||||
HorizontalAlignment="Center" Margin="0,8,0,0"
|
HorizontalAlignment="Center" Margin="0,8,0,0"
|
||||||
|
|||||||
@@ -0,0 +1,173 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using System.Windows.Media.Imaging;
|
||||||
|
using ytLive.Models;
|
||||||
|
|
||||||
|
namespace ytLive.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Renders chat messages into a VideoFrame for the chat overlay on stream.
|
||||||
|
/// Uses WPF's FormattedText + RenderTargetBitmap to produce BGRA8 output.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ChatBoxRenderer
|
||||||
|
{
|
||||||
|
private static readonly CultureInfo InvariantCulture = CultureInfo.InvariantCulture;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Render the last N messages into a VideoFrame at the specified dimensions.
|
||||||
|
/// Messages are drawn bottom-up (newest at bottom, oldest at top) with
|
||||||
|
/// configurable font, colors, and background.
|
||||||
|
/// </summary>
|
||||||
|
public VideoFrame Render(
|
||||||
|
IReadOnlyList<ChatMessage> messages,
|
||||||
|
int width,
|
||||||
|
int height,
|
||||||
|
double fontSize,
|
||||||
|
string fontColorHex,
|
||||||
|
string backgroundColorHex,
|
||||||
|
int maxMessages,
|
||||||
|
bool showTimestamps,
|
||||||
|
bool showBadges,
|
||||||
|
string memberColorHex,
|
||||||
|
string superChatColorHex)
|
||||||
|
{
|
||||||
|
var dpi = 96.0;
|
||||||
|
var visual = new DrawingVisual();
|
||||||
|
|
||||||
|
using (var ctx = visual.RenderOpen())
|
||||||
|
{
|
||||||
|
// Background
|
||||||
|
var bgColor = ParseColor(backgroundColorHex);
|
||||||
|
ctx.DrawRectangle(
|
||||||
|
new SolidColorBrush(bgColor),
|
||||||
|
null,
|
||||||
|
new Rect(0, 0, width, height));
|
||||||
|
|
||||||
|
if (messages.Count == 0)
|
||||||
|
return CreateFrame(visual, width, height, dpi);
|
||||||
|
|
||||||
|
// Layout parameters
|
||||||
|
var lineHeight = fontSize * 1.4;
|
||||||
|
var padding = 8.0;
|
||||||
|
var maxVisibleLines = (int)((height - padding * 2) / lineHeight);
|
||||||
|
if (maxVisibleLines < 1) maxVisibleLines = 1;
|
||||||
|
|
||||||
|
// Take the last N messages that fit
|
||||||
|
var visibleMessages = new List<(ChatMessage msg, string text, Brush brush)>();
|
||||||
|
var totalLines = 0;
|
||||||
|
|
||||||
|
for (var i = messages.Count - 1; i >= 0 && visibleMessages.Count < maxMessages; i--)
|
||||||
|
{
|
||||||
|
var msg = messages[i];
|
||||||
|
var brush = GetMessageBrush(msg, fontColorHex, memberColorHex, superChatColorHex);
|
||||||
|
var text = FormatMessage(msg, showTimestamps, showBadges);
|
||||||
|
var lineCount = EstimateLineCount(text, fontSize, width - padding * 2);
|
||||||
|
if (totalLines + lineCount > maxVisibleLines) break;
|
||||||
|
totalLines += lineCount;
|
||||||
|
visibleMessages.Add((msg, text, brush));
|
||||||
|
}
|
||||||
|
|
||||||
|
visibleMessages.Reverse();
|
||||||
|
|
||||||
|
// Draw messages bottom-up
|
||||||
|
var y = height - padding;
|
||||||
|
for (var i = visibleMessages.Count - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
var (_, text, brush) = visibleMessages[i];
|
||||||
|
var formatted = new FormattedText(
|
||||||
|
text,
|
||||||
|
InvariantCulture,
|
||||||
|
FlowDirection.LeftToRight,
|
||||||
|
new Typeface(new FontFamily("Segoe UI"), FontStyles.Normal, FontWeights.Normal, FontStretches.Normal),
|
||||||
|
fontSize,
|
||||||
|
brush,
|
||||||
|
dpi);
|
||||||
|
|
||||||
|
formatted.MaxTextWidth = width - padding * 2;
|
||||||
|
formatted.Trimming = TextTrimming.CharacterEllipsis;
|
||||||
|
|
||||||
|
y -= formatted.Height;
|
||||||
|
ctx.DrawText(formatted, new Point(padding, y));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return CreateFrame(visual, width, height, dpi);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FormatMessage(ChatMessage msg, bool showTimestamps, bool showBadges)
|
||||||
|
{
|
||||||
|
var parts = new List<string>();
|
||||||
|
|
||||||
|
if (showTimestamps)
|
||||||
|
parts.Add($"[{msg.Timestamp:HH:mm}]");
|
||||||
|
|
||||||
|
if (showBadges)
|
||||||
|
{
|
||||||
|
if (msg.IsSuperChat)
|
||||||
|
parts.Add("💰");
|
||||||
|
else if (msg.IsMember)
|
||||||
|
parts.Add("⭐");
|
||||||
|
}
|
||||||
|
|
||||||
|
parts.Add($"{msg.AuthorName}: {msg.Message}");
|
||||||
|
return string.Join(" ", parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int EstimateLineCount(string text, double fontSize, double maxWidth)
|
||||||
|
{
|
||||||
|
// Rough estimate: ~12 chars per 100px at font size 16
|
||||||
|
var charsPerLine = (int)(maxWidth / (fontSize * 0.6));
|
||||||
|
if (charsPerLine < 1) charsPerLine = 1;
|
||||||
|
return Math.Max(1, (int)Math.Ceiling((double)text.Length / charsPerLine));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Brush GetMessageBrush(ChatMessage msg, string fontColorHex, string memberColorHex, string superChatColorHex)
|
||||||
|
{
|
||||||
|
if (msg.IsSuperChat)
|
||||||
|
return new SolidColorBrush(ParseColor(superChatColorHex));
|
||||||
|
if (msg.IsMember)
|
||||||
|
return new SolidColorBrush(ParseColor(memberColorHex));
|
||||||
|
return new SolidColorBrush(ParseColor(fontColorHex));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Color ParseColor(string hex)
|
||||||
|
{
|
||||||
|
hex = hex.Trim().TrimStart('#');
|
||||||
|
if (hex.Length == 6)
|
||||||
|
{
|
||||||
|
var r = byte.Parse(hex.AsSpan(0, 2), NumberStyles.HexNumber);
|
||||||
|
var g = byte.Parse(hex.AsSpan(2, 2), NumberStyles.HexNumber);
|
||||||
|
var b = byte.Parse(hex.AsSpan(4, 2), NumberStyles.HexNumber);
|
||||||
|
return Color.FromRgb(r, g, b);
|
||||||
|
}
|
||||||
|
if (hex.Length == 8)
|
||||||
|
{
|
||||||
|
var a = byte.Parse(hex.AsSpan(0, 2), NumberStyles.HexNumber);
|
||||||
|
var r = byte.Parse(hex.AsSpan(2, 2), NumberStyles.HexNumber);
|
||||||
|
var g = byte.Parse(hex.AsSpan(4, 2), NumberStyles.HexNumber);
|
||||||
|
var b = byte.Parse(hex.AsSpan(6, 2), NumberStyles.HexNumber);
|
||||||
|
return Color.FromArgb(a, r, g, b);
|
||||||
|
}
|
||||||
|
return Colors.White;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static VideoFrame CreateFrame(DrawingVisual visual, int width, int height, double dpi)
|
||||||
|
{
|
||||||
|
var rtb = new RenderTargetBitmap(width, height, dpi, dpi, PixelFormats.Pbgra32);
|
||||||
|
rtb.Render(visual);
|
||||||
|
|
||||||
|
var stride = width * 4;
|
||||||
|
var pixels = new byte[stride * height];
|
||||||
|
rtb.CopyPixels(pixels, stride, 0);
|
||||||
|
|
||||||
|
// Pbgra32 → Bgra8: swap R and B channels (Pbgra premultiplies alpha but
|
||||||
|
// for opaque backgrounds the swap is all we need)
|
||||||
|
for (var i = 0; i < pixels.Length; i += 4)
|
||||||
|
{
|
||||||
|
(pixels[i], pixels[i + 2]) = (pixels[i + 2], pixels[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new VideoFrame(width, height, pixels);
|
||||||
|
}
|
||||||
|
}
|
||||||
+83
-3
@@ -211,6 +211,56 @@ public class LayoutStore : IDisposable
|
|||||||
cmd.CommandText = "ALTER TABLE Source ADD COLUMN CaptureKey TEXT;";
|
cmd.CommandText = "ALTER TABLE Source ADD COLUMN CaptureKey TEXT;";
|
||||||
cmd.ExecuteNonQuery();
|
cmd.ExecuteNonQuery();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TASK 3.18: ChatBox overlay source
|
||||||
|
if (!columns.Contains("ChatFontSize"))
|
||||||
|
{
|
||||||
|
using var cmd = _connection.CreateCommand();
|
||||||
|
cmd.CommandText = "ALTER TABLE Source ADD COLUMN ChatFontSize REAL NOT NULL DEFAULT 16;";
|
||||||
|
cmd.ExecuteNonQuery();
|
||||||
|
}
|
||||||
|
if (!columns.Contains("ChatFontColor"))
|
||||||
|
{
|
||||||
|
using var cmd = _connection.CreateCommand();
|
||||||
|
cmd.CommandText = "ALTER TABLE Source ADD COLUMN ChatFontColor TEXT NOT NULL DEFAULT '#FFFFFF';";
|
||||||
|
cmd.ExecuteNonQuery();
|
||||||
|
}
|
||||||
|
if (!columns.Contains("ChatBackgroundColor"))
|
||||||
|
{
|
||||||
|
using var cmd = _connection.CreateCommand();
|
||||||
|
cmd.CommandText = "ALTER TABLE Source ADD COLUMN ChatBackgroundColor TEXT NOT NULL DEFAULT '#80000000';";
|
||||||
|
cmd.ExecuteNonQuery();
|
||||||
|
}
|
||||||
|
if (!columns.Contains("ChatMaxMessages"))
|
||||||
|
{
|
||||||
|
using var cmd = _connection.CreateCommand();
|
||||||
|
cmd.CommandText = "ALTER TABLE Source ADD COLUMN ChatMaxMessages INTEGER NOT NULL DEFAULT 5;";
|
||||||
|
cmd.ExecuteNonQuery();
|
||||||
|
}
|
||||||
|
if (!columns.Contains("ChatShowTimestamps"))
|
||||||
|
{
|
||||||
|
using var cmd = _connection.CreateCommand();
|
||||||
|
cmd.CommandText = "ALTER TABLE Source ADD COLUMN ChatShowTimestamps INTEGER NOT NULL DEFAULT 1;";
|
||||||
|
cmd.ExecuteNonQuery();
|
||||||
|
}
|
||||||
|
if (!columns.Contains("ChatShowBadges"))
|
||||||
|
{
|
||||||
|
using var cmd = _connection.CreateCommand();
|
||||||
|
cmd.CommandText = "ALTER TABLE Source ADD COLUMN ChatShowBadges INTEGER NOT NULL DEFAULT 1;";
|
||||||
|
cmd.ExecuteNonQuery();
|
||||||
|
}
|
||||||
|
if (!columns.Contains("ChatMemberColor"))
|
||||||
|
{
|
||||||
|
using var cmd = _connection.CreateCommand();
|
||||||
|
cmd.CommandText = "ALTER TABLE Source ADD COLUMN ChatMemberColor TEXT NOT NULL DEFAULT '#e94560';";
|
||||||
|
cmd.ExecuteNonQuery();
|
||||||
|
}
|
||||||
|
if (!columns.Contains("ChatSuperChatColor"))
|
||||||
|
{
|
||||||
|
using var cmd = _connection.CreateCommand();
|
||||||
|
cmd.CommandText = "ALTER TABLE Source ADD COLUMN ChatSuperChatColor TEXT NOT NULL DEFAULT '#eab308';";
|
||||||
|
cmd.ExecuteNonQuery();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// v3 → v4: WebcamSceneConfig gains RectWidth/RectHeight (the pre-Round rect,
|
// v3 → v4: WebcamSceneConfig gains RectWidth/RectHeight (the pre-Round rect,
|
||||||
@@ -510,7 +560,9 @@ public class LayoutStore : IDisposable
|
|||||||
cmd.CommandText = """
|
cmd.CommandText = """
|
||||||
SELECT Id, SceneId, AssetId, Type, Name, IsEnabled,
|
SELECT Id, SceneId, AssetId, Type, Name, IsEnabled,
|
||||||
X, Y, Width, Height, Opacity, MonitorIndex, ClipShape, IsMirrored,
|
X, Y, Width, Height, Opacity, MonitorIndex, ClipShape, IsMirrored,
|
||||||
IsBackdrop, CaptureKey
|
IsBackdrop, CaptureKey,
|
||||||
|
ChatFontSize, ChatFontColor, ChatBackgroundColor, ChatMaxMessages,
|
||||||
|
ChatShowTimestamps, ChatShowBadges, ChatMemberColor, ChatSuperChatColor
|
||||||
FROM Source ORDER BY SortOrder
|
FROM Source ORDER BY SortOrder
|
||||||
""";
|
""";
|
||||||
using var reader = cmd.ExecuteReader();
|
using var reader = cmd.ExecuteReader();
|
||||||
@@ -534,6 +586,14 @@ public class LayoutStore : IDisposable
|
|||||||
IsMirrored = reader.GetInt32(13) != 0,
|
IsMirrored = reader.GetInt32(13) != 0,
|
||||||
IsBackdrop = reader.GetInt32(14) != 0,
|
IsBackdrop = reader.GetInt32(14) != 0,
|
||||||
CaptureKey = reader.IsDBNull(15) ? null : reader.GetString(15),
|
CaptureKey = reader.IsDBNull(15) ? null : reader.GetString(15),
|
||||||
|
ChatFontSize = reader.IsDBNull(16) ? 16 : reader.GetDouble(16),
|
||||||
|
ChatFontColor = reader.IsDBNull(17) ? "#FFFFFF" : reader.GetString(17),
|
||||||
|
ChatBackgroundColor = reader.IsDBNull(18) ? "#80000000" : reader.GetString(18),
|
||||||
|
ChatMaxMessages = reader.IsDBNull(19) ? 5 : reader.GetInt32(19),
|
||||||
|
ChatShowTimestamps = reader.IsDBNull(20) ? true : reader.GetInt32(20) != 0,
|
||||||
|
ChatShowBadges = reader.IsDBNull(21) ? true : reader.GetInt32(21) != 0,
|
||||||
|
ChatMemberColor = reader.IsDBNull(22) ? "#e94560" : reader.GetString(22),
|
||||||
|
ChatSuperChatColor = reader.IsDBNull(23) ? "#eab308" : reader.GetString(23),
|
||||||
};
|
};
|
||||||
if (!sourcesByScene.TryGetValue(sceneId, out var list))
|
if (!sourcesByScene.TryGetValue(sceneId, out var list))
|
||||||
sourcesByScene[sceneId] = list = new List<Source>();
|
sourcesByScene[sceneId] = list = new List<Source>();
|
||||||
@@ -724,10 +784,14 @@ public class LayoutStore : IDisposable
|
|||||||
cmd.CommandText = """
|
cmd.CommandText = """
|
||||||
INSERT INTO Source (Id, SceneId, AssetId, Type, Name, IsEnabled,
|
INSERT INTO Source (Id, SceneId, AssetId, Type, Name, IsEnabled,
|
||||||
X, Y, Width, Height, Opacity, MonitorIndex,
|
X, Y, Width, Height, Opacity, MonitorIndex,
|
||||||
ClipShape, IsMirrored, IsBackdrop, CaptureKey, SortOrder)
|
ClipShape, IsMirrored, IsBackdrop, CaptureKey, SortOrder,
|
||||||
|
ChatFontSize, ChatFontColor, ChatBackgroundColor, ChatMaxMessages,
|
||||||
|
ChatShowTimestamps, ChatShowBadges, ChatMemberColor, ChatSuperChatColor)
|
||||||
VALUES ($id, $sceneId, $assetId, $type, $name, $isEnabled,
|
VALUES ($id, $sceneId, $assetId, $type, $name, $isEnabled,
|
||||||
$x, $y, $w, $h, $opacity, $monitor,
|
$x, $y, $w, $h, $opacity, $monitor,
|
||||||
$clip, $mirrored, $isBackdrop, $captureKey, $sort)
|
$clip, $mirrored, $isBackdrop, $captureKey, $sort,
|
||||||
|
$chatFontSize, $chatFontColor, $chatBgColor, $chatMaxMsgs,
|
||||||
|
$chatTimestamps, $chatBadges, $chatMemberColor, $chatSuperChatColor)
|
||||||
""";
|
""";
|
||||||
cmd.Transaction = tx;
|
cmd.Transaction = tx;
|
||||||
var idP = cmd.Parameters.Add("$id", SqliteType.Text);
|
var idP = cmd.Parameters.Add("$id", SqliteType.Text);
|
||||||
@@ -747,6 +811,14 @@ public class LayoutStore : IDisposable
|
|||||||
var isBackdropP = cmd.Parameters.Add("$isBackdrop", SqliteType.Integer);
|
var isBackdropP = cmd.Parameters.Add("$isBackdrop", SqliteType.Integer);
|
||||||
var captureKeyP = cmd.Parameters.Add("$captureKey", SqliteType.Text);
|
var captureKeyP = cmd.Parameters.Add("$captureKey", SqliteType.Text);
|
||||||
var sortP = cmd.Parameters.Add("$sort", SqliteType.Integer);
|
var sortP = cmd.Parameters.Add("$sort", SqliteType.Integer);
|
||||||
|
var chatFontSizeP = cmd.Parameters.Add("$chatFontSize", SqliteType.Real);
|
||||||
|
var chatFontColorP = cmd.Parameters.Add("$chatFontColor", SqliteType.Text);
|
||||||
|
var chatBgColorP = cmd.Parameters.Add("$chatBgColor", SqliteType.Text);
|
||||||
|
var chatMaxMsgsP = cmd.Parameters.Add("$chatMaxMsgs", SqliteType.Integer);
|
||||||
|
var chatTimestampsP = cmd.Parameters.Add("$chatTimestamps", SqliteType.Integer);
|
||||||
|
var chatBadgesP = cmd.Parameters.Add("$chatBadges", SqliteType.Integer);
|
||||||
|
var chatMemberColorP = cmd.Parameters.Add("$chatMemberColor", SqliteType.Text);
|
||||||
|
var chatSuperChatColorP = cmd.Parameters.Add("$chatSuperChatColor", SqliteType.Text);
|
||||||
|
|
||||||
foreach (var scene in scenes)
|
foreach (var scene in scenes)
|
||||||
{
|
{
|
||||||
@@ -771,6 +843,14 @@ public class LayoutStore : IDisposable
|
|||||||
isBackdropP.Value = source.IsBackdrop ? 1 : 0;
|
isBackdropP.Value = source.IsBackdrop ? 1 : 0;
|
||||||
captureKeyP.Value = (object?)source.CaptureKey ?? DBNull.Value;
|
captureKeyP.Value = (object?)source.CaptureKey ?? DBNull.Value;
|
||||||
sortP.Value = sort++;
|
sortP.Value = sort++;
|
||||||
|
chatFontSizeP.Value = source.ChatFontSize;
|
||||||
|
chatFontColorP.Value = source.ChatFontColor;
|
||||||
|
chatBgColorP.Value = source.ChatBackgroundColor;
|
||||||
|
chatMaxMsgsP.Value = source.ChatMaxMessages;
|
||||||
|
chatTimestampsP.Value = source.ChatShowTimestamps ? 1 : 0;
|
||||||
|
chatBadgesP.Value = source.ChatShowBadges ? 1 : 0;
|
||||||
|
chatMemberColorP.Value = source.ChatMemberColor;
|
||||||
|
chatSuperChatColorP.Value = source.ChatSuperChatColor;
|
||||||
cmd.ExecuteNonQuery();
|
cmd.ExecuteNonQuery();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -126,6 +126,7 @@ public class MainViewModel : ViewModelBase
|
|||||||
private readonly IFullScreenDetector _fullScreenDetector;
|
private readonly IFullScreenDetector _fullScreenDetector;
|
||||||
private readonly ScreenCaptureManager _screenCaptureManager;
|
private readonly ScreenCaptureManager _screenCaptureManager;
|
||||||
private readonly ScreenCaptureSourceFactory _screenCaptureFactory;
|
private readonly ScreenCaptureSourceFactory _screenCaptureFactory;
|
||||||
|
private readonly ChatBoxRenderer _chatBoxRenderer;
|
||||||
private int? _lastForegroundFullScreenMonitor;
|
private int? _lastForegroundFullScreenMonitor;
|
||||||
private CancellationTokenSource? _deactivateCts;
|
private CancellationTokenSource? _deactivateCts;
|
||||||
private ImageSource? _backdropImage;
|
private ImageSource? _backdropImage;
|
||||||
@@ -270,7 +271,7 @@ public class MainViewModel : ViewModelBase
|
|||||||
/// <summary>True when the Elements panel should show the "No configurable elements" hint.</summary>
|
/// <summary>True when the Elements panel should show the "No configurable elements" hint.</summary>
|
||||||
public bool IsElementsPanelEmpty => SelectedElement is { } el
|
public bool IsElementsPanelEmpty => SelectedElement is { } el
|
||||||
&& !el.IsDraggable && !el.IsWebcam
|
&& !el.IsDraggable && !el.IsWebcam
|
||||||
&& el is not Source { Type: SourceType.Countdown or SourceType.WebSource };
|
&& el is not Source { Type: SourceType.Countdown or SourceType.WebSource or SourceType.ChatBox };
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The reason the webcam feed is down (device in use, offline, locked, no frames),
|
/// The reason the webcam feed is down (device in use, offline, locked, no frames),
|
||||||
@@ -1267,6 +1268,8 @@ public class MainViewModel : ViewModelBase
|
|||||||
_screenCaptureManager.CaptureFailed += (key, message) =>
|
_screenCaptureManager.CaptureFailed += (key, message) =>
|
||||||
AppLog.Write($"ScreenCaptureManager: capture '{key}' failed: {message}");
|
AppLog.Write($"ScreenCaptureManager: capture '{key}' failed: {message}");
|
||||||
|
|
||||||
|
_chatBoxRenderer = new ChatBoxRenderer();
|
||||||
|
|
||||||
_rtmpUrlProvider = () => _reusableStreamUrl; // TASK 5: the reusable stream's ingest URL
|
_rtmpUrlProvider = () => _reusableStreamUrl; // TASK 5: the reusable stream's ingest URL
|
||||||
_framePump = new FramePump(
|
_framePump = new FramePump(
|
||||||
sceneProvider: () => ActiveScene,
|
sceneProvider: () => ActiveScene,
|
||||||
@@ -1761,6 +1764,7 @@ public class MainViewModel : ViewModelBase
|
|||||||
SourceType.TextOverlay => "Text",
|
SourceType.TextOverlay => "Text",
|
||||||
SourceType.Countdown => "Countdown",
|
SourceType.Countdown => "Countdown",
|
||||||
SourceType.WebSource => "Web",
|
SourceType.WebSource => "Web",
|
||||||
|
SourceType.ChatBox => "Chat",
|
||||||
_ => "Source",
|
_ => "Source",
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -2414,11 +2418,33 @@ public class MainViewModel : ViewModelBase
|
|||||||
{
|
{
|
||||||
WebcamSceneConfig webcam => _cameraManager.GetLatestFrame(webcam.WebcamId),
|
WebcamSceneConfig webcam => _cameraManager.GetLatestFrame(webcam.WebcamId),
|
||||||
Source { IsLiveCapture: true, CaptureKey: not null } live => _screenCaptureManager.GetLatestFrame(live.CaptureKey),
|
Source { IsLiveCapture: true, CaptureKey: not null } live => _screenCaptureManager.GetLatestFrame(live.CaptureKey),
|
||||||
|
Source { Type: SourceType.ChatBox } chatBox => RenderChatBox(chatBox),
|
||||||
Source { AssetId: not null } image => StaticPixelCache.Get(image.AssetId),
|
Source { AssetId: not null } image => StaticPixelCache.Get(image.AssetId),
|
||||||
_ => null,
|
_ => null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private VideoFrame? RenderChatBox(Source chatBox)
|
||||||
|
{
|
||||||
|
if (ChatMessages.Count == 0) return null;
|
||||||
|
|
||||||
|
var width = (int)Math.Max(1, chatBox.Width);
|
||||||
|
var height = (int)Math.Max(1, chatBox.Height);
|
||||||
|
|
||||||
|
return _chatBoxRenderer.Render(
|
||||||
|
ChatMessages,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
chatBox.ChatFontSize,
|
||||||
|
chatBox.ChatFontColor,
|
||||||
|
chatBox.ChatBackgroundColor,
|
||||||
|
chatBox.ChatMaxMessages,
|
||||||
|
chatBox.ChatShowTimestamps,
|
||||||
|
chatBox.ChatShowBadges,
|
||||||
|
chatBox.ChatMemberColor,
|
||||||
|
chatBox.ChatSuperChatColor);
|
||||||
|
}
|
||||||
|
|
||||||
// The tier's crop rect over the 1920x1080 master, integer-aligned (the VM's
|
// The tier's crop rect over the 1920x1080 master, integer-aligned (the VM's
|
||||||
// OutputRect* are doubles — the vertical 607.5 half-pixel crop rounds to 608).
|
// OutputRect* are doubles — the vertical 607.5 half-pixel crop rounds to 608).
|
||||||
private CompositorOptions BuildCompositorOptions()
|
private CompositorOptions BuildCompositorOptions()
|
||||||
|
|||||||
Reference in New Issue
Block a user