Browse Source

cleanup graphics element loading (#2412)

pull/2414/head
Jason Dove 4 months ago committed by GitHub
parent
commit
9182a8ad18
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 17
      ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs
  2. 23
      ErsatzTV.Core/Graphics/ImageGraphicsElement.cs
  3. 25
      ErsatzTV.Core/Graphics/SubtitleGraphicsElement.cs
  4. 23
      ErsatzTV.Core/Graphics/TextGraphicsElement.cs
  5. 6
      ErsatzTV.Core/Interfaces/Streaming/GraphicsEngineContext.cs
  6. 11
      ErsatzTV.Core/Interfaces/Streaming/IGraphicsElementLoader.cs
  7. 237
      ErsatzTV.Infrastructure/Streaming/Graphics/GraphicsElementLoader.cs
  8. 194
      ErsatzTV.Infrastructure/Streaming/Graphics/GraphicsEngine.cs
  9. 4
      ErsatzTV.Infrastructure/Streaming/Graphics/Subtitle/SubtitleElement.cs
  10. 17
      ErsatzTV.Infrastructure/Streaming/Graphics/Text/TextElement.cs
  11. 3
      ErsatzTV.Scanner.Tests/Core/FFmpeg/TranscodingTests.cs
  12. 2
      ErsatzTV/Startup.cs

17
ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs

@ -20,6 +20,7 @@ namespace ErsatzTV.Core.FFmpeg; @@ -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 @@ -34,6 +35,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
ITempFilePool tempFilePool,
IPipelineBuilderFactory pipelineBuilderFactory,
IConfigElementRepository configElementRepository,
IGraphicsElementLoader graphicsElementLoader,
ILogger<FFmpegLibraryProcessService> logger)
{
_ffmpegProcessService = ffmpegProcessService;
@ -42,6 +44,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService @@ -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 @@ -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 @@ -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(

23
ErsatzTV.Core/Graphics/ImageGraphicsElement.cs

@ -1,6 +1,5 @@ @@ -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 @@ -35,26 +34,4 @@ public class ImageGraphicsElement
[YamlMember(Alias = "scale_width_percent", ApplyNamingConventions = false)]
public double? ScaleWidthPercent { get; set; }
public static Option<ImageGraphicsElement> FromYaml(string yaml)
{
try
{
// TODO: validate schema
// if (await yamlScheduleValidator.ValidateSchedule(yaml, isImport) == false)
// {
// return Option<YamlPlayoutDefinition>.None;
// }
IDeserializer deserializer = new DeserializerBuilder()
.WithNamingConvention(CamelCaseNamingConvention.Instance)
.Build();
return deserializer.Deserialize<ImageGraphicsElement>(yaml);
}
catch (Exception)
{
return Option<ImageGraphicsElement>.None;
}
}
}

25
ErsatzTV.Core/Graphics/SubtitleGraphicsElement.cs

@ -1,9 +1,8 @@ @@ -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 @@ -12,26 +11,4 @@ public class SubtitlesGraphicsElement
public int EpgEntries { get; set; }
public string Template { get; set; }
public static Option<SubtitlesGraphicsElement> FromYaml(string yaml)
{
try
{
// TODO: validate schema
// if (await yamlScheduleValidator.ValidateSchedule(yaml, isImport) == false)
// {
// return Option<YamlPlayoutDefinition>.None;
// }
IDeserializer deserializer = new DeserializerBuilder()
.WithNamingConvention(CamelCaseNamingConvention.Instance)
.Build();
return deserializer.Deserialize<SubtitlesGraphicsElement>(yaml);
}
catch (Exception)
{
return Option<SubtitlesGraphicsElement>.None;
}
}
}

23
ErsatzTV.Core/Graphics/TextGraphicsElement.cs

@ -1,6 +1,5 @@ @@ -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 @@ -44,28 +43,6 @@ public class TextGraphicsElement
public int EpgEntries { get; set; }
public string Text { get; set; }
public static Option<TextGraphicsElement> FromYaml(string yaml)
{
try
{
// TODO: validate schema
// if (await yamlScheduleValidator.ValidateSchedule(yaml, isImport) == false)
// {
// return Option<YamlPlayoutDefinition>.None;
// }
IDeserializer deserializer = new DeserializerBuilder()
.WithNamingConvention(CamelCaseNamingConvention.Instance)
.Build();
return deserializer.Deserialize<TextGraphicsElement>(yaml);
}
catch (Exception)
{
return Option<TextGraphicsElement>.None;
}
}
}
public class StyleDefinition

6
ErsatzTV.Core/Interfaces/Streaming/GraphicsEngineContext.cs

@ -8,7 +8,7 @@ public record GraphicsEngineContext( @@ -8,7 +8,7 @@ public record GraphicsEngineContext(
string ChannelNumber,
MediaItem MediaItem,
List<GraphicsElementContext> Elements,
List<PlayoutItemGraphicsElement> ElementReferences,
Dictionary<string, object> TemplateVariables,
Resolution SquarePixelFrameSize,
Resolution FrameSize,
int FrameRate,
@ -30,11 +30,11 @@ public record TextElementDataContext(TextGraphicsElement TextElement, Dictionary @@ -30,11 +30,11 @@ public record TextElementDataContext(TextGraphicsElement TextElement, Dictionary
public record ImageElementContext(ImageGraphicsElement ImageElement) : GraphicsElementContext;
public record SubtitleElementDataContext(
SubtitlesGraphicsElement SubtitlesElement,
SubtitleGraphicsElement SubtitleElement,
Dictionary<string, string> Variables)
: GraphicsElementContext, ITemplateDataContext
{
public int EpgEntries => SubtitlesElement.EpgEntries;
public int EpgEntries => SubtitleElement.EpgEntries;
}
public interface ITemplateDataContext

11
ErsatzTV.Core/Interfaces/Streaming/IGraphicsElementLoader.cs

@ -0,0 +1,11 @@ @@ -0,0 +1,11 @@
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Core.Interfaces.Streaming;
public interface IGraphicsElementLoader
{
Task<GraphicsEngineContext> LoadAll(
GraphicsEngineContext context,
List<PlayoutItemGraphicsElement> elements,
CancellationToken cancellationToken);
}

237
ErsatzTV.Infrastructure/Streaming/Graphics/GraphicsElementLoader.cs

@ -0,0 +1,237 @@ @@ -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<GraphicsElementLoader> logger)
: IGraphicsElementLoader
{
public async Task<GraphicsEngineContext> LoadAll(
GraphicsEngineContext context,
List<PlayoutItemGraphicsElement> elements,
CancellationToken cancellationToken)
{
// get max epg entries
int epgEntries = await GetMaxEpgEntries(elements);
// init template element variables once
Dictionary<string, object> 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<TextGraphicsElement> 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<string, string>();
if (!string.IsNullOrWhiteSpace(reference.Variables))
{
variables = JsonConvert.DeserializeObject<Dictionary<string, string>>(reference.Variables);
}
context.Elements.Add(new TextElementDataContext(element, variables));
}
break;
}
case GraphicsElementKind.Image:
{
Option<ImageGraphicsElement> 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<SubtitleGraphicsElement> 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<string, string>();
if (!string.IsNullOrWhiteSpace(reference.Variables))
{
variables = JsonConvert.DeserializeObject<Dictionary<string, string>>(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<int> GetMaxEpgEntries(List<PlayoutItemGraphicsElement> elements)
{
var epgEntries = 0;
IEnumerable<PlayoutItemGraphicsElement> 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<Option<ImageGraphicsElement>> LoadImage(string fileName, Dictionary<string, object> variables) =>
GetTemplatedYaml(fileName, variables).Map(FromYaml<ImageGraphicsElement>);
private Task<Option<TextGraphicsElement>> LoadText(string fileName, Dictionary<string, object> variables) =>
GetTemplatedYaml(fileName, variables).Map(FromYaml<TextGraphicsElement>);
private Task<Option<SubtitleGraphicsElement>> LoadSubtitle(string fileName, Dictionary<string, object> variables) =>
GetTemplatedYaml(fileName, variables).Map(FromYaml<SubtitleGraphicsElement>);
private async Task<Dictionary<string, object>> InitTemplateVariables(
GraphicsEngineContext context,
int epgEntries,
CancellationToken cancellationToken)
{
// common variables
var result = new Dictionary<string, object>
{
[MediaItemTemplateDataKey.Resolution] = context.FrameSize,
[MediaItemTemplateDataKey.StreamSeek] = context.Seek
};
// media item variables
Option<Dictionary<string, object>> maybeTemplateData =
await templateDataRepository.GetMediaItemTemplateData(context.MediaItem, cancellationToken);
foreach (Dictionary<string, object> templateData in maybeTemplateData)
{
foreach (KeyValuePair<string, object> variable in templateData)
{
result.Add(variable.Key, variable.Value);
}
}
// epg variables
DateTimeOffset startTime = context.ContentStartTime + context.Seek;
Option<Dictionary<string, object>> maybeEpgData =
await templateDataRepository.GetEpgTemplateData(context.ChannelNumber, startTime, epgEntries);
foreach (Dictionary<string, object> templateData in maybeEpgData)
{
foreach (KeyValuePair<string, object> variable in templateData)
{
result.Add(variable.Key, variable.Value);
}
}
return result;
}
private async Task<string> GetTemplatedYaml(string fileName, Dictionary<string, object> 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<T> FromYaml<T>(string yaml)
{
try
{
// TODO: validate schema
// if (await yamlScheduleValidator.ValidateSchedule(yaml, isImport) == false)
// {
// return Option<YamlPlayoutDefinition>.None;
// }
IDeserializer deserializer = new DeserializerBuilder()
.WithNamingConvention(CamelCaseNamingConvention.Instance)
.Build();
return deserializer.Deserialize<T>(yaml);
}
catch (Exception ex)
{
logger.LogWarning(ex, "Failed to load graphics element YAML definition");
return Option<T>.None;
}
}
[GeneratedRegex(@"epg_entries:\s*(\d+)")]
private static partial Regex EpgEntriesRegex();
}

194
ErsatzTV.Infrastructure/Streaming/Graphics/GraphicsEngine.cs

@ -1,27 +1,16 @@ @@ -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<GraphicsEngine> logger)
: IGraphicsEngine
{
@ -29,107 +18,6 @@ public partial class GraphicsEngine( @@ -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<string, object> 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<TextGraphicsElement> 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<string, string>();
if (!string.IsNullOrWhiteSpace(reference.Variables))
{
variables = JsonConvert.DeserializeObject<Dictionary<string, string>>(reference.Variables);
}
context.Elements.Add(new TextElementDataContext(element, variables));
}
break;
}
case GraphicsElementKind.Image:
{
Option<ImageGraphicsElement> 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<SubtitlesGraphicsElement> 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<string, string>();
if (!string.IsNullOrWhiteSpace(reference.Variables))
{
variables = JsonConvert.DeserializeObject<Dictionary<string, string>>(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<IGraphicsElement>();
foreach (GraphicsElementContext element in context.Elements)
{
@ -150,26 +38,13 @@ public partial class GraphicsEngine( @@ -150,26 +38,13 @@ public partial class GraphicsEngine(
case TextElementDataContext textElementContext:
{
var variables = templateVariables.ToDictionary();
foreach (KeyValuePair<string, string> 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<string, string> variable in subtitleElementContext.Variables)
{
variables.Add(variable.Key, variable.Value);
@ -178,7 +53,7 @@ public partial class GraphicsEngine( @@ -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( @@ -305,65 +180,4 @@ public partial class GraphicsEngine(
}
}
}
private async Task<Dictionary<string, object>> InitTemplateVariables(
GraphicsEngineContext context,
int epgEntries,
CancellationToken cancellationToken)
{
// common variables
var result = new Dictionary<string, object>
{
[MediaItemTemplateDataKey.Resolution] = context.FrameSize,
[MediaItemTemplateDataKey.StreamSeek] = context.Seek
};
// media item variables
Option<Dictionary<string, object>> maybeTemplateData =
await templateDataRepository.GetMediaItemTemplateData(context.MediaItem, cancellationToken);
foreach (Dictionary<string, object> templateData in maybeTemplateData)
{
foreach (KeyValuePair<string, object> variable in templateData)
{
result.Add(variable.Key, variable.Value);
}
}
// epg variables
DateTimeOffset startTime = context.ContentStartTime + context.Seek;
Option<Dictionary<string, object>> maybeEpgData =
await templateDataRepository.GetEpgTemplateData(context.ChannelNumber, startTime, epgEntries);
foreach (Dictionary<string, object> templateData in maybeEpgData)
{
foreach (KeyValuePair<string, object> variable in templateData)
{
result.Add(variable.Key, variable.Value);
}
}
return result;
}
private async Task<string> GetTemplatedYaml(string fileName, Dictionary<string, object> 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();
}

4
ErsatzTV.Infrastructure/Streaming/Graphics/Subtitle/SubtitleElement.cs

@ -16,7 +16,7 @@ namespace ErsatzTV.Infrastructure.Streaming.Graphics; @@ -16,7 +16,7 @@ namespace ErsatzTV.Infrastructure.Streaming.Graphics;
public class SubtitleElement(
TemplateFunctions templateFunctions,
ITempFilePool tempFilePool,
SubtitlesGraphicsElement subtitlesElement,
SubtitleGraphicsElement subtitleElement,
Dictionary<string, object> variables,
ILogger logger)
: GraphicsElement, IDisposable
@ -78,7 +78,7 @@ public class SubtitleElement( @@ -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);

17
ErsatzTV.Infrastructure/Streaming/Graphics/Text/TextElement.cs

@ -11,10 +11,8 @@ using RichTextKit = Topten.RichTextKit; @@ -11,10 +11,8 @@ using RichTextKit = Topten.RichTextKit;
namespace ErsatzTV.Infrastructure.Streaming.Graphics;
public partial class TextElement(
TemplateFunctions templateFunctions,
GraphicsEngineFonts graphicsEngineFonts,
TextGraphicsElement textElement,
Dictionary<string, object> variables,
ILogger logger)
: GraphicsElement, IDisposable
{
@ -33,7 +31,7 @@ public partial class TextElement( @@ -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( @@ -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( @@ -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<Option<PreparedElementImage>> PrepareImage(

3
ErsatzTV.Scanner.Tests/Core/FFmpeg/TranscodingTests.cs

@ -13,6 +13,7 @@ using ErsatzTV.Core.Interfaces.FFmpeg; @@ -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 @@ -271,6 +272,7 @@ public class TranscodingTests
LoggerFactory.CreateLogger<HardwareCapabilitiesFactory>()),
LoggerFactory.CreateLogger<PipelineBuilderFactory>()),
Substitute.For<IConfigElementRepository>(),
Substitute.For<IGraphicsElementLoader>(),
LoggerFactory.CreateLogger<FFmpegLibraryProcessService>());
var songVideoGenerator = new SongVideoGenerator(tempFilePool, mockImageCache, service);
@ -947,6 +949,7 @@ public class TranscodingTests @@ -947,6 +949,7 @@ public class TranscodingTests
LoggerFactory.CreateLogger<HardwareCapabilitiesFactory>()),
LoggerFactory.CreateLogger<PipelineBuilderFactory>()),
Substitute.For<IConfigElementRepository>(),
Substitute.For<IGraphicsElementLoader>(),
LoggerFactory.CreateLogger<FFmpegLibraryProcessService>());
return service;

2
ErsatzTV/Startup.cs

@ -13,6 +13,7 @@ using ErsatzTV.Core; @@ -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 @@ -782,6 +783,7 @@ public class Startup
services.AddScoped<IGraphicsEngine, GraphicsEngine>();
services.AddScoped<IGraphicsElementRepository, GraphicsElementRepository>();
services.AddScoped<ITemplateDataRepository, TemplateDataRepository>();
services.AddScoped<IGraphicsElementLoader, GraphicsElementLoader>();
services.AddScoped<TemplateFunctions>();
services.AddScoped<IDecoSelector, DecoSelector>();
services.AddScoped<IWatermarkSelector, WatermarkSelector>();

Loading…
Cancel
Save