From 9182a8ad1833e222dfcd0ca171048e260e1e2911 Mon Sep 17 00:00:00 2001 From: Jason Dove <1695733+jasongdove@users.noreply.github.com> Date: Sat, 13 Sep 2025 08:39:20 -0500 Subject: [PATCH] cleanup graphics element loading (#2412) --- .../FFmpeg/FFmpegLibraryProcessService.cs | 17 +- .../Graphics/ImageGraphicsElement.cs | 23 -- .../Graphics/SubtitleGraphicsElement.cs | 25 +- ErsatzTV.Core/Graphics/TextGraphicsElement.cs | 23 -- .../Streaming/GraphicsEngineContext.cs | 6 +- .../Streaming/IGraphicsElementLoader.cs | 11 + .../Graphics/GraphicsElementLoader.cs | 237 ++++++++++++++++++ .../Streaming/Graphics/GraphicsEngine.cs | 194 +------------- .../Graphics/Subtitle/SubtitleElement.cs | 4 +- .../Streaming/Graphics/Text/TextElement.cs | 17 +- .../Core/FFmpeg/TranscodingTests.cs | 3 + ErsatzTV/Startup.cs | 2 + 12 files changed, 280 insertions(+), 282 deletions(-) create mode 100644 ErsatzTV.Core/Interfaces/Streaming/IGraphicsElementLoader.cs create mode 100644 ErsatzTV.Infrastructure/Streaming/Graphics/GraphicsElementLoader.cs diff --git a/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs b/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs index 8940732d0..7db926ef3 100644 --- a/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs +++ b/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs @@ -20,6 +20,7 @@ namespace ErsatzTV.Core.FFmpeg; public class FFmpegLibraryProcessService : IFFmpegProcessService { private readonly IConfigElementRepository _configElementRepository; + private readonly IGraphicsElementLoader _graphicsElementLoader; private readonly ICustomStreamSelector _customStreamSelector; private readonly FFmpegProcessService _ffmpegProcessService; private readonly IFFmpegStreamSelector _ffmpegStreamSelector; @@ -34,6 +35,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService ITempFilePool tempFilePool, IPipelineBuilderFactory pipelineBuilderFactory, IConfigElementRepository configElementRepository, + IGraphicsElementLoader graphicsElementLoader, ILogger logger) { _ffmpegProcessService = ffmpegProcessService; @@ -42,6 +44,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService _tempFilePool = tempFilePool; _pipelineBuilderFactory = pipelineBuilderFactory; _configElementRepository = configElementRepository; + _graphicsElementLoader = graphicsElementLoader; _logger = logger; } @@ -406,15 +409,13 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService // only use graphics engine when we have elements if (graphicsElementContexts.Count > 0 || graphicsElements.Count > 0) { - graphicsEngineInput = new GraphicsEngineInput(); - FrameSize targetSize = await desiredState.CroppedSize.IfNoneAsync(desiredState.ScaledSize); - graphicsEngineContext = new GraphicsEngineContext( + var context = new GraphicsEngineContext( channel.Number, audioVersion.MediaItem, graphicsElementContexts, - graphicsElements, + TemplateVariables: [], new Resolution { Width = targetSize.Width, Height = targetSize.Height }, channel.FFmpegProfile.Resolution, await playbackSettings.FrameRate.IfNoneAsync(24), @@ -422,6 +423,14 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService start, await playbackSettings.StreamSeek.IfNoneAsync(TimeSpan.Zero), finish - now); + + context = await _graphicsElementLoader.LoadAll(context, graphicsElements, cancellationToken); + + if (context.Elements.Count > 0) + { + graphicsEngineInput = new GraphicsEngineInput(); + graphicsEngineContext = context; + } } var ffmpegState = new FFmpegState( diff --git a/ErsatzTV.Core/Graphics/ImageGraphicsElement.cs b/ErsatzTV.Core/Graphics/ImageGraphicsElement.cs index 6acecc46b..fbf170ea7 100644 --- a/ErsatzTV.Core/Graphics/ImageGraphicsElement.cs +++ b/ErsatzTV.Core/Graphics/ImageGraphicsElement.cs @@ -1,6 +1,5 @@ using ErsatzTV.FFmpeg.State; using YamlDotNet.Serialization; -using YamlDotNet.Serialization.NamingConventions; namespace ErsatzTV.Core.Graphics; @@ -35,26 +34,4 @@ public class ImageGraphicsElement [YamlMember(Alias = "scale_width_percent", ApplyNamingConventions = false)] public double? ScaleWidthPercent { get; set; } - - public static Option FromYaml(string yaml) - { - try - { - // TODO: validate schema - // if (await yamlScheduleValidator.ValidateSchedule(yaml, isImport) == false) - // { - // return Option.None; - // } - - IDeserializer deserializer = new DeserializerBuilder() - .WithNamingConvention(CamelCaseNamingConvention.Instance) - .Build(); - - return deserializer.Deserialize(yaml); - } - catch (Exception) - { - return Option.None; - } - } } diff --git a/ErsatzTV.Core/Graphics/SubtitleGraphicsElement.cs b/ErsatzTV.Core/Graphics/SubtitleGraphicsElement.cs index 1987bca20..de9cb5fc8 100644 --- a/ErsatzTV.Core/Graphics/SubtitleGraphicsElement.cs +++ b/ErsatzTV.Core/Graphics/SubtitleGraphicsElement.cs @@ -1,9 +1,8 @@ using YamlDotNet.Serialization; -using YamlDotNet.Serialization.NamingConventions; namespace ErsatzTV.Core.Graphics; -public class SubtitlesGraphicsElement +public class SubtitleGraphicsElement { [YamlMember(Alias = "z_index", ApplyNamingConventions = false)] public int? ZIndex { get; set; } @@ -12,26 +11,4 @@ public class SubtitlesGraphicsElement public int EpgEntries { get; set; } public string Template { get; set; } - - public static Option FromYaml(string yaml) - { - try - { - // TODO: validate schema - // if (await yamlScheduleValidator.ValidateSchedule(yaml, isImport) == false) - // { - // return Option.None; - // } - - IDeserializer deserializer = new DeserializerBuilder() - .WithNamingConvention(CamelCaseNamingConvention.Instance) - .Build(); - - return deserializer.Deserialize(yaml); - } - catch (Exception) - { - return Option.None; - } - } } diff --git a/ErsatzTV.Core/Graphics/TextGraphicsElement.cs b/ErsatzTV.Core/Graphics/TextGraphicsElement.cs index fc2015bcf..3dee14148 100644 --- a/ErsatzTV.Core/Graphics/TextGraphicsElement.cs +++ b/ErsatzTV.Core/Graphics/TextGraphicsElement.cs @@ -1,6 +1,5 @@ using ErsatzTV.FFmpeg.State; using YamlDotNet.Serialization; -using YamlDotNet.Serialization.NamingConventions; namespace ErsatzTV.Core.Graphics; @@ -44,28 +43,6 @@ public class TextGraphicsElement public int EpgEntries { get; set; } public string Text { get; set; } - - public static Option FromYaml(string yaml) - { - try - { - // TODO: validate schema - // if (await yamlScheduleValidator.ValidateSchedule(yaml, isImport) == false) - // { - // return Option.None; - // } - - IDeserializer deserializer = new DeserializerBuilder() - .WithNamingConvention(CamelCaseNamingConvention.Instance) - .Build(); - - return deserializer.Deserialize(yaml); - } - catch (Exception) - { - return Option.None; - } - } } public class StyleDefinition diff --git a/ErsatzTV.Core/Interfaces/Streaming/GraphicsEngineContext.cs b/ErsatzTV.Core/Interfaces/Streaming/GraphicsEngineContext.cs index 50f6deb74..aa7a96554 100644 --- a/ErsatzTV.Core/Interfaces/Streaming/GraphicsEngineContext.cs +++ b/ErsatzTV.Core/Interfaces/Streaming/GraphicsEngineContext.cs @@ -8,7 +8,7 @@ public record GraphicsEngineContext( string ChannelNumber, MediaItem MediaItem, List Elements, - List ElementReferences, + Dictionary TemplateVariables, Resolution SquarePixelFrameSize, Resolution FrameSize, int FrameRate, @@ -30,11 +30,11 @@ public record TextElementDataContext(TextGraphicsElement TextElement, Dictionary public record ImageElementContext(ImageGraphicsElement ImageElement) : GraphicsElementContext; public record SubtitleElementDataContext( - SubtitlesGraphicsElement SubtitlesElement, + SubtitleGraphicsElement SubtitleElement, Dictionary Variables) : GraphicsElementContext, ITemplateDataContext { - public int EpgEntries => SubtitlesElement.EpgEntries; + public int EpgEntries => SubtitleElement.EpgEntries; } public interface ITemplateDataContext diff --git a/ErsatzTV.Core/Interfaces/Streaming/IGraphicsElementLoader.cs b/ErsatzTV.Core/Interfaces/Streaming/IGraphicsElementLoader.cs new file mode 100644 index 000000000..84948e01b --- /dev/null +++ b/ErsatzTV.Core/Interfaces/Streaming/IGraphicsElementLoader.cs @@ -0,0 +1,11 @@ +using ErsatzTV.Core.Domain; + +namespace ErsatzTV.Core.Interfaces.Streaming; + +public interface IGraphicsElementLoader +{ + Task LoadAll( + GraphicsEngineContext context, + List elements, + CancellationToken cancellationToken); +} diff --git a/ErsatzTV.Infrastructure/Streaming/Graphics/GraphicsElementLoader.cs b/ErsatzTV.Infrastructure/Streaming/Graphics/GraphicsElementLoader.cs new file mode 100644 index 000000000..c2d2764c0 --- /dev/null +++ b/ErsatzTV.Infrastructure/Streaming/Graphics/GraphicsElementLoader.cs @@ -0,0 +1,237 @@ +using System.Text.RegularExpressions; +using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Graphics; +using ErsatzTV.Core.Interfaces.Metadata; +using ErsatzTV.Core.Interfaces.Repositories; +using ErsatzTV.Core.Interfaces.Streaming; +using ErsatzTV.Core.Metadata; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using Scriban; +using Scriban.Runtime; +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.NamingConventions; + +namespace ErsatzTV.Infrastructure.Streaming.Graphics; + +public partial class GraphicsElementLoader( + TemplateFunctions templateFunctions, + ILocalFileSystem localFileSystem, + ITemplateDataRepository templateDataRepository, + ILogger logger) + : IGraphicsElementLoader +{ + public async Task LoadAll( + GraphicsEngineContext context, + List elements, + CancellationToken cancellationToken) + { + // get max epg entries + int epgEntries = await GetMaxEpgEntries(elements); + + // init template element variables once + Dictionary templateVariables = + await InitTemplateVariables(context, epgEntries, cancellationToken); + + // subtitles are in separate files, so they need template variables for later processing + context = context with { TemplateVariables = templateVariables }; + + // fully process references (using template variables) + foreach (PlayoutItemGraphicsElement reference in elements) + { + switch (reference.GraphicsElement.Kind) + { + case GraphicsElementKind.Text: + { + Option maybeElement = await LoadText( + reference.GraphicsElement.Path, + templateVariables); + if (maybeElement.IsNone) + { + logger.LogWarning( + "Failed to load text graphics element from file {Path}; ignoring", + reference.GraphicsElement.Path); + } + + foreach (TextGraphicsElement element in maybeElement) + { + var variables = new Dictionary(); + if (!string.IsNullOrWhiteSpace(reference.Variables)) + { + variables = JsonConvert.DeserializeObject>(reference.Variables); + } + + context.Elements.Add(new TextElementDataContext(element, variables)); + } + + break; + } + case GraphicsElementKind.Image: + { + Option maybeElement = await LoadImage( + reference.GraphicsElement.Path, + templateVariables); + if (maybeElement.IsNone) + { + logger.LogWarning( + "Failed to load image graphics element from file {Path}; ignoring", + reference.GraphicsElement.Path); + } + + context.Elements.AddRange(maybeElement.Select(element => new ImageElementContext(element))); + + break; + } + case GraphicsElementKind.Subtitle: + { + Option maybeElement = await LoadSubtitle( + reference.GraphicsElement.Path, + templateVariables); + if (maybeElement.IsNone) + { + logger.LogWarning( + "Failed to load subtitle graphics element from file {Path}; ignoring", + reference.GraphicsElement.Path); + } + + foreach (SubtitleGraphicsElement element in maybeElement) + { + var variables = new Dictionary(); + if (!string.IsNullOrWhiteSpace(reference.Variables)) + { + variables = JsonConvert.DeserializeObject>(reference.Variables); + } + + context.Elements.Add(new SubtitleElementDataContext(element, variables)); + } + + break; + } + default: + logger.LogInformation( + "Ignoring unsupported graphics element kind {Kind}", + nameof(reference.GraphicsElement.Kind)); + break; + } + } + + return context; + } + + private async Task GetMaxEpgEntries(List elements) + { + var epgEntries = 0; + + IEnumerable elementsWithEpg = elements.Where(e => + e.GraphicsElement.Kind is GraphicsElementKind.Text or GraphicsElementKind.Subtitle); + + foreach (var reference in elementsWithEpg) + { + foreach (string line in await localFileSystem.ReadAllLines(reference.GraphicsElement.Path)) + { + Match match = EpgEntriesRegex().Match(line); + if (!match.Success || !int.TryParse(match.Groups[1].Value, out int value)) + { + continue; + } + + epgEntries = Math.Max(epgEntries, value); + } + } + + return epgEntries; + } + + private Task> LoadImage(string fileName, Dictionary variables) => + GetTemplatedYaml(fileName, variables).Map(FromYaml); + + private Task> LoadText(string fileName, Dictionary variables) => + GetTemplatedYaml(fileName, variables).Map(FromYaml); + + private Task> LoadSubtitle(string fileName, Dictionary variables) => + GetTemplatedYaml(fileName, variables).Map(FromYaml); + + private async Task> InitTemplateVariables( + GraphicsEngineContext context, + int epgEntries, + CancellationToken cancellationToken) + { + // common variables + var result = new Dictionary + { + [MediaItemTemplateDataKey.Resolution] = context.FrameSize, + [MediaItemTemplateDataKey.StreamSeek] = context.Seek + }; + + // media item variables + Option> maybeTemplateData = + await templateDataRepository.GetMediaItemTemplateData(context.MediaItem, cancellationToken); + foreach (Dictionary templateData in maybeTemplateData) + { + foreach (KeyValuePair variable in templateData) + { + result.Add(variable.Key, variable.Value); + } + } + + // epg variables + DateTimeOffset startTime = context.ContentStartTime + context.Seek; + Option> maybeEpgData = + await templateDataRepository.GetEpgTemplateData(context.ChannelNumber, startTime, epgEntries); + foreach (Dictionary templateData in maybeEpgData) + { + foreach (KeyValuePair variable in templateData) + { + result.Add(variable.Key, variable.Value); + } + } + + return result; + } + + private async Task GetTemplatedYaml(string fileName, Dictionary variables) + { + string yaml = await localFileSystem.ReadAllText(fileName); + try + { + var scriptObject = new ScriptObject(); + scriptObject.Import(variables, renamer: member => member.Name); + scriptObject.Import("convert_timezone", templateFunctions.ConvertTimeZone); + scriptObject.Import("format_datetime", templateFunctions.FormatDateTime); + + var context = new TemplateContext { MemberRenamer = member => member.Name }; + context.PushGlobal(scriptObject); + return await Template.Parse(yaml).RenderAsync(context); + } + catch (Exception) + { + return yaml; + } + } + + private Option FromYaml(string yaml) + { + try + { + // TODO: validate schema + // if (await yamlScheduleValidator.ValidateSchedule(yaml, isImport) == false) + // { + // return Option.None; + // } + + IDeserializer deserializer = new DeserializerBuilder() + .WithNamingConvention(CamelCaseNamingConvention.Instance) + .Build(); + + return deserializer.Deserialize(yaml); + } + catch (Exception ex) + { + logger.LogWarning(ex, "Failed to load graphics element YAML definition"); + return Option.None; + } + } + + [GeneratedRegex(@"epg_entries:\s*(\d+)")] + private static partial Regex EpgEntriesRegex(); +} diff --git a/ErsatzTV.Infrastructure/Streaming/Graphics/GraphicsEngine.cs b/ErsatzTV.Infrastructure/Streaming/Graphics/GraphicsEngine.cs index 0d19ca657..51e56e4e3 100644 --- a/ErsatzTV.Infrastructure/Streaming/Graphics/GraphicsEngine.cs +++ b/ErsatzTV.Infrastructure/Streaming/Graphics/GraphicsEngine.cs @@ -1,27 +1,16 @@ using System.IO.Pipelines; -using System.Text.RegularExpressions; using ErsatzTV.Core; -using ErsatzTV.Core.Domain; -using ErsatzTV.Core.Graphics; using ErsatzTV.Core.Interfaces.FFmpeg; -using ErsatzTV.Core.Interfaces.Metadata; -using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Streaming; -using ErsatzTV.Core.Metadata; using Microsoft.Extensions.Logging; -using Newtonsoft.Json; -using Scriban; -using Scriban.Runtime; using SkiaSharp; namespace ErsatzTV.Infrastructure.Streaming.Graphics; -public partial class GraphicsEngine( +public class GraphicsEngine( TemplateFunctions templateFunctions, GraphicsEngineFonts graphicsEngineFonts, ITempFilePool tempFilePool, - ITemplateDataRepository templateDataRepository, - ILocalFileSystem localFileSystem, ILogger logger) : IGraphicsEngine { @@ -29,107 +18,6 @@ public partial class GraphicsEngine( { graphicsEngineFonts.LoadFonts(FileSystemLayout.FontsCacheFolder); - // get max epg entries - int epgEntries = 0; - foreach (var reference in context.ElementReferences) - { - if (reference.GraphicsElement.Kind is GraphicsElementKind.Text or GraphicsElementKind.Subtitle) - { - foreach (string line in await localFileSystem.ReadAllLines(reference.GraphicsElement.Path)) - { - Match match = EpgEntriesRegex().Match(line); - if (match.Success && int.TryParse(match.Groups[1].Value, out int value)) - { - epgEntries = Math.Max(epgEntries, value); - break; - } - } - } - } - - // init template element variables once - Dictionary templateVariables = - await InitTemplateVariables(context, epgEntries, cancellationToken); - - // fully process references (using template variables) - foreach (var reference in context.ElementReferences) - { - switch (reference.GraphicsElement.Kind) - { - case GraphicsElementKind.Text: - { - Option maybeElement = TextGraphicsElement.FromYaml( - await GetTemplatedYaml(reference.GraphicsElement.Path, templateVariables)); - if (maybeElement.IsNone) - { - logger.LogWarning( - "Failed to load text graphics element from file {Path}; ignoring", - reference.GraphicsElement.Path); - } - - foreach (TextGraphicsElement element in maybeElement) - { - var variables = new Dictionary(); - if (!string.IsNullOrWhiteSpace(reference.Variables)) - { - variables = JsonConvert.DeserializeObject>(reference.Variables); - } - - context.Elements.Add(new TextElementDataContext(element, variables)); - } - - break; - } - case GraphicsElementKind.Image: - { - Option maybeElement = ImageGraphicsElement.FromYaml( - await GetTemplatedYaml(reference.GraphicsElement.Path, templateVariables)); - if (maybeElement.IsNone) - { - logger.LogWarning( - "Failed to load image graphics element from file {Path}; ignoring", - reference.GraphicsElement.Path); - } - - foreach (ImageGraphicsElement element in maybeElement) - { - context.Elements.Add(new ImageElementContext(element)); - } - - break; - } - case GraphicsElementKind.Subtitle: - { - Option maybeElement = SubtitlesGraphicsElement.FromYaml( - await GetTemplatedYaml(reference.GraphicsElement.Path, templateVariables)); - if (maybeElement.IsNone) - { - logger.LogWarning( - "Failed to load subtitle graphics element from file {Path}; ignoring", - reference.GraphicsElement.Path); - } - - foreach (SubtitlesGraphicsElement element in maybeElement) - { - var variables = new Dictionary(); - if (!string.IsNullOrWhiteSpace(reference.Variables)) - { - variables = JsonConvert.DeserializeObject>(reference.Variables); - } - - context.Elements.Add(new SubtitleElementDataContext(element, variables)); - } - - break; - } - default: - logger.LogInformation( - "Ignoring unsupported graphics element kind {Kind}", - nameof(reference.GraphicsElement.Kind)); - break; - } - } - var elements = new List(); foreach (GraphicsElementContext element in context.Elements) { @@ -150,26 +38,13 @@ public partial class GraphicsEngine( case TextElementDataContext textElementContext: { - var variables = templateVariables.ToDictionary(); - foreach (KeyValuePair variable in textElementContext.Variables) - { - variables.Add(variable.Key, variable.Value); - } - - var textElement = new TextElement( - templateFunctions, - graphicsEngineFonts, - textElementContext.TextElement, - variables, - logger); - - elements.Add(textElement); + elements.Add(new TextElement(graphicsEngineFonts, textElementContext.TextElement, logger)); break; } case SubtitleElementDataContext subtitleElementContext: { - var variables = templateVariables.ToDictionary(); + var variables = context.TemplateVariables.ToDictionary(); foreach (KeyValuePair variable in subtitleElementContext.Variables) { variables.Add(variable.Key, variable.Value); @@ -178,7 +53,7 @@ public partial class GraphicsEngine( var subtitleElement = new SubtitleElement( templateFunctions, tempFilePool, - subtitleElementContext.SubtitlesElement, + subtitleElementContext.SubtitleElement, variables, logger); @@ -305,65 +180,4 @@ public partial class GraphicsEngine( } } } - - private async Task> InitTemplateVariables( - GraphicsEngineContext context, - int epgEntries, - CancellationToken cancellationToken) - { - // common variables - var result = new Dictionary - { - [MediaItemTemplateDataKey.Resolution] = context.FrameSize, - [MediaItemTemplateDataKey.StreamSeek] = context.Seek - }; - - // media item variables - Option> maybeTemplateData = - await templateDataRepository.GetMediaItemTemplateData(context.MediaItem, cancellationToken); - foreach (Dictionary templateData in maybeTemplateData) - { - foreach (KeyValuePair variable in templateData) - { - result.Add(variable.Key, variable.Value); - } - } - - // epg variables - DateTimeOffset startTime = context.ContentStartTime + context.Seek; - Option> maybeEpgData = - await templateDataRepository.GetEpgTemplateData(context.ChannelNumber, startTime, epgEntries); - foreach (Dictionary templateData in maybeEpgData) - { - foreach (KeyValuePair variable in templateData) - { - result.Add(variable.Key, variable.Value); - } - } - - return result; - } - - private async Task GetTemplatedYaml(string fileName, Dictionary variables) - { - string yaml = await localFileSystem.ReadAllText(fileName); - try - { - var scriptObject = new ScriptObject(); - scriptObject.Import(variables, renamer: member => member.Name); - scriptObject.Import("convert_timezone", templateFunctions.ConvertTimeZone); - scriptObject.Import("format_datetime", templateFunctions.FormatDateTime); - - var context = new TemplateContext { MemberRenamer = member => member.Name }; - context.PushGlobal(scriptObject); - return await Template.Parse(yaml).RenderAsync(context); - } - catch (Exception) - { - return yaml; - } - } - - [GeneratedRegex(@"epg_entries:\s*(\d+)")] - private static partial Regex EpgEntriesRegex(); } diff --git a/ErsatzTV.Infrastructure/Streaming/Graphics/Subtitle/SubtitleElement.cs b/ErsatzTV.Infrastructure/Streaming/Graphics/Subtitle/SubtitleElement.cs index 244b46cd3..53ff24655 100644 --- a/ErsatzTV.Infrastructure/Streaming/Graphics/Subtitle/SubtitleElement.cs +++ b/ErsatzTV.Infrastructure/Streaming/Graphics/Subtitle/SubtitleElement.cs @@ -16,7 +16,7 @@ namespace ErsatzTV.Infrastructure.Streaming.Graphics; public class SubtitleElement( TemplateFunctions templateFunctions, ITempFilePool tempFilePool, - SubtitlesGraphicsElement subtitlesElement, + SubtitleGraphicsElement subtitleElement, Dictionary variables, ILogger logger) : GraphicsElement, IDisposable @@ -78,7 +78,7 @@ public class SubtitleElement( var context = new TemplateContext { MemberRenamer = member => member.Name }; context.PushGlobal(scriptObject); - string inputText = await File.ReadAllTextAsync(subtitlesElement.Template, cancellationToken); + string inputText = await File.ReadAllTextAsync(subtitleElement.Template, cancellationToken); string textToRender = await Template.Parse(inputText).RenderAsync(context); await File.WriteAllTextAsync(subtitleTemplateFile, textToRender, cancellationToken); diff --git a/ErsatzTV.Infrastructure/Streaming/Graphics/Text/TextElement.cs b/ErsatzTV.Infrastructure/Streaming/Graphics/Text/TextElement.cs index cd2cbd6a9..25d9aac4a 100644 --- a/ErsatzTV.Infrastructure/Streaming/Graphics/Text/TextElement.cs +++ b/ErsatzTV.Infrastructure/Streaming/Graphics/Text/TextElement.cs @@ -11,10 +11,8 @@ using RichTextKit = Topten.RichTextKit; namespace ErsatzTV.Infrastructure.Streaming.Graphics; public partial class TextElement( - TemplateFunctions templateFunctions, GraphicsEngineFonts graphicsEngineFonts, TextGraphicsElement textElement, - Dictionary variables, ILogger logger) : GraphicsElement, IDisposable { @@ -33,7 +31,7 @@ public partial class TextElement( _image = null; } - public override async Task InitializeAsync( + public override Task InitializeAsync( Resolution squarePixelFrameSize, Resolution frameSize, int frameRate, @@ -68,16 +66,7 @@ public partial class TextElement( } } - var scriptObject = new ScriptObject(); - scriptObject.Import(variables, renamer: member => member.Name); - scriptObject.Import("convert_timezone", templateFunctions.ConvertTimeZone); - scriptObject.Import("format_datetime", templateFunctions.FormatDateTime); - - var context = new TemplateContext { MemberRenamer = member => member.Name }; - context.PushGlobal(scriptObject); - string textToRender = await Template.Parse(textElement.Text).RenderAsync(context); - - RichTextKit.TextBlock textBlock = BuildTextBlock(textToRender); + RichTextKit.TextBlock textBlock = BuildTextBlock(textElement.Text); _image = new SKBitmap( (int)Math.Ceiling(textBlock.MeasuredWidth), @@ -106,6 +95,8 @@ public partial class TextElement( IsFailed = true; logger.LogWarning(ex, "Failed to initialize text element; will disable for this content"); } + + return Task.CompletedTask; } public override ValueTask> PrepareImage( diff --git a/ErsatzTV.Scanner.Tests/Core/FFmpeg/TranscodingTests.cs b/ErsatzTV.Scanner.Tests/Core/FFmpeg/TranscodingTests.cs index 4b8c5007d..a977dafae 100644 --- a/ErsatzTV.Scanner.Tests/Core/FFmpeg/TranscodingTests.cs +++ b/ErsatzTV.Scanner.Tests/Core/FFmpeg/TranscodingTests.cs @@ -13,6 +13,7 @@ using ErsatzTV.Core.Interfaces.FFmpeg; using ErsatzTV.Core.Interfaces.Images; using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; +using ErsatzTV.Core.Interfaces.Streaming; using ErsatzTV.Core.Metadata; using ErsatzTV.FFmpeg; using ErsatzTV.FFmpeg.Capabilities; @@ -271,6 +272,7 @@ public class TranscodingTests LoggerFactory.CreateLogger()), LoggerFactory.CreateLogger()), Substitute.For(), + Substitute.For(), LoggerFactory.CreateLogger()); var songVideoGenerator = new SongVideoGenerator(tempFilePool, mockImageCache, service); @@ -947,6 +949,7 @@ public class TranscodingTests LoggerFactory.CreateLogger()), LoggerFactory.CreateLogger()), Substitute.For(), + Substitute.For(), LoggerFactory.CreateLogger()); return service; diff --git a/ErsatzTV/Startup.cs b/ErsatzTV/Startup.cs index 42c45d4f9..1b02ee453 100644 --- a/ErsatzTV/Startup.cs +++ b/ErsatzTV/Startup.cs @@ -13,6 +13,7 @@ using ErsatzTV.Core; using ErsatzTV.Core.Emby; using ErsatzTV.Core.Errors; using ErsatzTV.Core.FFmpeg; +using ErsatzTV.Core.Graphics; using ErsatzTV.Core.Health; using ErsatzTV.Core.Health.Checks; using ErsatzTV.Core.Images; @@ -782,6 +783,7 @@ public class Startup services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped();