cleanup graphics element loading (#2412)
This commit is contained in:
@@ -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<FFmpegLibraryProcessService> 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(
|
||||
|
||||
@@ -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<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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<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
|
||||
|
||||
@@ -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
|
||||
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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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(
|
||||
{
|
||||
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(
|
||||
|
||||
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(
|
||||
var subtitleElement = new SubtitleElement(
|
||||
templateFunctions,
|
||||
tempFilePool,
|
||||
subtitleElementContext.SubtitlesElement,
|
||||
subtitleElementContext.SubtitleElement,
|
||||
variables,
|
||||
logger);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
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);
|
||||
|
||||
|
||||
@@ -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(
|
||||
_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<Option<PreparedElementImage>> PrepareImage(
|
||||
|
||||
@@ -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<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
|
||||
LoggerFactory.CreateLogger<HardwareCapabilitiesFactory>()),
|
||||
LoggerFactory.CreateLogger<PipelineBuilderFactory>()),
|
||||
Substitute.For<IConfigElementRepository>(),
|
||||
Substitute.For<IGraphicsElementLoader>(),
|
||||
LoggerFactory.CreateLogger<FFmpegLibraryProcessService>());
|
||||
|
||||
return service;
|
||||
|
||||
@@ -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<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>();
|
||||
|
||||
Reference in New Issue
Block a user