using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.Windows; using System.Windows.Media; using System.Windows.Media.Imaging; using ytLive.Models; namespace ytLive.Services; public sealed class TextOverlayRenderer { private const int MaxRasterDimension = 4096; private readonly ConcurrentDictionary _cache = new(); public VideoFrame? RenderFrame(Source source) => _cache.TryGetValue(source, out var entry) ? entry.Frame : null; public void Remove(Source source) => _cache.TryRemove(source, out _); public void Retain(IEnumerable sources) { var active = new HashSet(sources); foreach (var source in _cache.Keys) if (!active.Contains(source)) _cache.TryRemove(source, out _); } public void UpdatePreview(Source source) { var dispatcher = Application.Current?.Dispatcher; if (dispatcher != null && !dispatcher.CheckAccess()) { dispatcher.Invoke(() => UpdatePreview(source)); return; } var config = Capture(source); if (_cache.TryGetValue(source, out var cached) && cached.Config == config) { if (!ReferenceEquals(source.VideoImageSource, cached.Preview)) source.VideoImageSource = cached.Preview; return; } var frame = Rasterize(config); var bitmap = new WriteableBitmap(frame.Width, frame.Height, 96, 96, PixelFormats.Bgra32, null); bitmap.WritePixels(new Int32Rect(0, 0, frame.Width, frame.Height), frame.BgraPixels, frame.Stride, 0); _cache[source] = new CacheEntry(config, frame, bitmap); source.VideoImageSource = bitmap; } private static RenderConfig Capture(Source source) => new( source.TextContent, source.TextFontFamily, source.TextFontSize, source.TextColor, source.TextBold, source.TextItalic, source.TextUnderline, source.TextAlignment, RasterDimension(source.Width), RasterDimension(source.Height)); private static int RasterDimension(double value) => double.IsFinite(value) ? (int)Math.Clamp(Math.Ceiling(value), 1, MaxRasterDimension) : 1; private static VideoFrame Rasterize(RenderConfig config) { var visual = new DrawingVisual(); if (!string.IsNullOrEmpty(config.Content)) { using var drawing = visual.RenderOpen(); var family = new FontFamily(config.FontFamily); var typeface = new Typeface( family, config.Italic ? FontStyles.Italic : FontStyles.Normal, config.Bold ? FontWeights.Bold : FontWeights.Normal, FontStretches.Normal); var brush = new SolidColorBrush(ParseColor(config.Color)); brush.Freeze(); var formatted = new FormattedText( config.Content, CultureInfo.CurrentUICulture, FlowDirection.LeftToRight, typeface, config.FontSize, brush, 1.0) { MaxTextWidth = config.Width, MaxTextHeight = config.Height, TextAlignment = config.Alignment switch { TextLayerAlignment.Center => System.Windows.TextAlignment.Center, TextLayerAlignment.Right => System.Windows.TextAlignment.Right, _ => System.Windows.TextAlignment.Left, }, Trimming = TextTrimming.CharacterEllipsis, }; if (config.Underline) formatted.SetTextDecorations(TextDecorations.Underline, 0, config.Content.Length); drawing.DrawText(formatted, new Point(0, 0)); } var bitmap = new RenderTargetBitmap(config.Width, config.Height, 96, 96, PixelFormats.Pbgra32); bitmap.Render(visual); var pixels = new byte[config.Width * config.Height * 4]; bitmap.CopyPixels(pixels, config.Width * 4, 0); UnpremultiplyAlpha(pixels); return new VideoFrame(config.Width, config.Height, pixels); } private static Color ParseColor(string hex) => (Color)ColorConverter.ConvertFromString(hex)!; private static void UnpremultiplyAlpha(byte[] pixels) { for (var i = 0; i < pixels.Length; i += 4) { var alpha = pixels[i + 3]; if (alpha == 0) { pixels[i] = pixels[i + 1] = pixels[i + 2] = 0; continue; } if (alpha == 255) continue; pixels[i] = Unpremultiply(pixels[i], alpha); pixels[i + 1] = Unpremultiply(pixels[i + 1], alpha); pixels[i + 2] = Unpremultiply(pixels[i + 2], alpha); } } private static byte Unpremultiply(byte value, byte alpha) => (byte)Math.Min(255, (value * 255 + alpha / 2) / alpha); private sealed record CacheEntry(RenderConfig Config, VideoFrame Frame, WriteableBitmap Preview); private readonly record struct RenderConfig( string Content, string FontFamily, double FontSize, string Color, bool Bold, bool Italic, bool Underline, TextLayerAlignment Alignment, int Width, int Height); }