diff --git a/CHANGELOG.md b/CHANGELOG.md index ed8fe55ee..0af908000 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Next engine - Use `overlay_qsv` for hardware-accelerated watermarks and image subtitles - Support multiple watermarks (still only `permanent` and `intermittent` modes, not `opacity expression`) + - Support image graphics elements that do not use an opacity expression ### Changed - Upgrade Mesa driver in docker from 25.2.8 to 26.0.3 to fix issues with hevc_vaapi encoder when using radeonsi driver diff --git a/ErsatzTV.Application/Playouts/Commands/SyncNextPlayoutHandler.cs b/ErsatzTV.Application/Playouts/Commands/SyncNextPlayoutHandler.cs index 7d498bddb..735ed1ade 100644 --- a/ErsatzTV.Application/Playouts/Commands/SyncNextPlayoutHandler.cs +++ b/ErsatzTV.Application/Playouts/Commands/SyncNextPlayoutHandler.cs @@ -129,8 +129,13 @@ public partial class SyncNextPlayoutHandler( .ThenInclude(d => d.DecoGraphicsElements) .ThenInclude(d => d.GraphicsElement) + // get watermarks .Include(i => i.Watermarks) + // get graphics elements + .Include(i => i.PlayoutItemGraphicsElements) + .ThenInclude(pige => pige.GraphicsElement) + // get playout templates (and deco templates/decos) .Include(i => i.Playout) .ThenInclude(p => p.Templates) @@ -140,6 +145,15 @@ public partial class SyncNextPlayoutHandler( .ThenInclude(d => d.DecoWatermarks) .ThenInclude(d => d.Watermark) + // get playout templates (and deco templates/decos) + .Include(i => i.Playout) + .ThenInclude(p => p.Templates) + .ThenInclude(t => t.DecoTemplate) + .ThenInclude(t => t.Items) + .ThenInclude(i => i.Deco) + .ThenInclude(d => d.DecoGraphicsElements) + .ThenInclude(d => d.GraphicsElement) + .Include(i => i.MediaItem) .ThenInclude(mi => mi.LibraryPath) .ThenInclude(lp => lp.Library) @@ -232,6 +246,8 @@ public partial class SyncNextPlayoutHandler( .AsNoTracking() .Include(c => c.Watermark) .Include(c => c.Artwork) + .Include(c => c.FFmpegProfile) + .ThenInclude(ff => ff.Resolution) .SingleOrDefaultAsync(c => c.Number == channelNumber, cancellationToken) .Map(Optional); diff --git a/ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs b/ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs index e8ce39dfc..a2a16e47f 100644 --- a/ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs +++ b/ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs @@ -358,7 +358,8 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler< List graphicsElements = _graphicsElementSelector.SelectGraphicsElements( channel, playoutItemWithPath.PlayoutItem, - now); + now, + shouldLogMessages: true); if (playoutItemWithPath.PlayoutItem.MediaItem is Image) { diff --git a/ErsatzTV.Application/Troubleshooting/Commands/PrepareTroubleshootingPlaybackHandler.cs b/ErsatzTV.Application/Troubleshooting/Commands/PrepareTroubleshootingPlaybackHandler.cs index 5005516db..74a547519 100644 --- a/ErsatzTV.Application/Troubleshooting/Commands/PrepareTroubleshootingPlaybackHandler.cs +++ b/ErsatzTV.Application/Troubleshooting/Commands/PrepareTroubleshootingPlaybackHandler.cs @@ -259,6 +259,14 @@ public class PrepareTroubleshootingPlaybackHandler( } } + List graphicsElements = []; + if (request.GraphicsElementIds.Count > 0) + { + graphicsElements = await dbContext.GraphicsElements + .Where(ge => request.GraphicsElementIds.Contains(ge.Id)) + .ToListAsync(cancellationToken); + } + switch (request.StreamingEngine) { case StreamingEngine.Next: @@ -270,6 +278,7 @@ public class PrepareTroubleshootingPlaybackHandler( inPoint, outPoint, watermarks, + graphicsElements, cancellationToken); default: return await GetLegacyProcess( @@ -283,6 +292,7 @@ public class PrepareTroubleshootingPlaybackHandler( channel, inPoint, watermarks, + graphicsElements, cancellationToken); } } @@ -295,6 +305,7 @@ public class PrepareTroubleshootingPlaybackHandler( TimeSpan inPoint, TimeSpan outPoint, List watermarks, + List graphicsElements, CancellationToken cancellationToken) { Validation channelBinaryResult = await ChannelBinaryMustExist(); @@ -345,8 +356,8 @@ public class PrepareTroubleshootingPlaybackHandler( CollectionKey = null, CollectionEtag = null, PlayoutItemWatermarks = [], - GraphicsElements = [], - PlayoutItemGraphicsElements = [] + GraphicsElements = null, + PlayoutItemGraphicsElements = [.. graphicsElements.Map(ge => new PlayoutItemGraphicsElement { GraphicsElement = ge })] }; Option maybeNextPlayoutItem = @@ -412,6 +423,7 @@ public class PrepareTroubleshootingPlaybackHandler( Channel channel, TimeSpan inPoint, List watermarks, + List graphicsElements, CancellationToken cancellationToken) { MediaVersion version = mediaItem.GetHeadVersion(); @@ -470,10 +482,6 @@ public class PrepareTroubleshootingPlaybackHandler( // we cannot burst live input bool hlsRealtime = mediaItem is RemoteStream { IsLive: true }; - List graphicsElements = await dbContext.GraphicsElements - .Where(ge => request.GraphicsElementIds.Contains(ge.Id)) - .ToListAsync(cancellationToken); - PlayoutItemResult playoutItemResult = await ffmpegProcessService.ForPlayoutItem( ffmpegPath, ffprobePath, diff --git a/ErsatzTV.Core.Tests/FFmpeg/GraphicsElementSelectorTests.cs b/ErsatzTV.Core.Tests/FFmpeg/GraphicsElementSelectorTests.cs index 48981a092..9d6ef585e 100644 --- a/ErsatzTV.Core.Tests/FFmpeg/GraphicsElementSelectorTests.cs +++ b/ErsatzTV.Core.Tests/FFmpeg/GraphicsElementSelectorTests.cs @@ -368,7 +368,8 @@ public class GraphicsElementSelectorTests List result = GraphicsElementSelector.SelectGraphicsElements( testCase.channel, testCase.playoutItem, - Now); + Now, + shouldLogMessages: true); result.Map(pige => pige.GraphicsElement).ShouldBe(testCase.expected); } diff --git a/ErsatzTV.Core/FFmpeg/GraphicsElementSelector.cs b/ErsatzTV.Core/FFmpeg/GraphicsElementSelector.cs index c4d97ee76..0f9b46c87 100644 --- a/ErsatzTV.Core/FFmpeg/GraphicsElementSelector.cs +++ b/ErsatzTV.Core/FFmpeg/GraphicsElementSelector.cs @@ -3,6 +3,7 @@ using ErsatzTV.Core.Domain.Filler; using ErsatzTV.Core.Domain.Scheduling; using ErsatzTV.Core.Interfaces.FFmpeg; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; namespace ErsatzTV.Core.FFmpeg; @@ -12,9 +13,14 @@ public class GraphicsElementSelector(IDecoSelector decoSelector, ILogger SelectGraphicsElements( Channel channel, PlayoutItem playoutItem, - DateTimeOffset now) + DateTimeOffset now, + bool shouldLogMessages) { - logger.LogDebug("Checking for graphics elements at {Now}", now); + ILogger log = shouldLogMessages + ? logger + : NullLogger.Instance; + + log.LogDebug("Checking for graphics elements at {Now}", now); var result = new List(); @@ -25,7 +31,7 @@ public class GraphicsElementSelector(IDecoSelector decoSelector, ILogger dge.GraphicsElement).Map(ge => new PlayoutItemGraphicsElement { PlayoutItem = playoutItem, GraphicsElement = ge })); break; } - logger.LogDebug("Graphics elements are disabled by template deco during filler"); + log.LogDebug("Graphics elements are disabled by template deco during filler"); result.Clear(); done = true; break; case DecoMode.Override: if (playoutItem.FillerKind is FillerKind.None || templateDeco.UseGraphicsElementsDuringFiller) { - logger.LogDebug("Graphics elements will come from template deco (replace)"); + log.LogDebug("Graphics elements will come from template deco (replace)"); result.AddRange( templateDeco.DecoGraphicsElements.Map(dge => dge.GraphicsElement).Map(ge => new PlayoutItemGraphicsElement { PlayoutItem = playoutItem, GraphicsElement = ge })); @@ -63,16 +69,16 @@ public class GraphicsElementSelector(IDecoSelector decoSelector, ILogger dge.GraphicsElement).Map(ge => new PlayoutItemGraphicsElement { PlayoutItem = playoutItem, GraphicsElement = ge })); break; } - logger.LogDebug("Graphics elements are disabled by playout deco during filler"); + log.LogDebug("Graphics elements are disabled by playout deco during filler"); result.Clear(); done = true; break; case DecoMode.Override: if (playoutItem.FillerKind is FillerKind.None || playoutDeco.UseGraphicsElementsDuringFiller) { - logger.LogDebug("Graphics elements will come from playout deco (replace)"); + log.LogDebug("Graphics elements will come from playout deco (replace)"); result.AddRange( playoutDeco.DecoGraphicsElements.Map(dge => dge.GraphicsElement).Map(ge => new PlayoutItemGraphicsElement { PlayoutItem = playoutItem, GraphicsElement = ge })); @@ -114,16 +120,16 @@ public class GraphicsElementSelector(IDecoSelector decoSelector, ILogger SelectGraphicsElements( Channel channel, PlayoutItem playoutItem, - DateTimeOffset now); + DateTimeOffset now, + bool shouldLogMessages); } diff --git a/ErsatzTV.Infrastructure/Data/Repositories/TemplateDataRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/TemplateDataRepository.cs index ae4c72017..663603a58 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/TemplateDataRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/TemplateDataRepository.cs @@ -34,6 +34,11 @@ public class TemplateDataRepository(IFileSystem fileSystem, IDbContextFactory>.None; + } + if (channelNumber.Equals( FileSystemLayout.TranscodeTroubleshootingChannel, StringComparison.OrdinalIgnoreCase)) diff --git a/ErsatzTV.Infrastructure/Scheduling/PlayoutItemConverter.cs b/ErsatzTV.Infrastructure/Scheduling/PlayoutItemConverter.cs index 098132df8..6bde6f2f7 100644 --- a/ErsatzTV.Infrastructure/Scheduling/PlayoutItemConverter.cs +++ b/ErsatzTV.Infrastructure/Scheduling/PlayoutItemConverter.cs @@ -11,10 +11,14 @@ using ErsatzTV.Core.Interfaces.FFmpeg; using ErsatzTV.Core.Interfaces.Jellyfin; using ErsatzTV.Core.Interfaces.Plex; using ErsatzTV.Core.Interfaces.Scheduling; +using ErsatzTV.Core.Interfaces.Streaming; +using ErsatzTV.FFmpeg; using ErsatzTV.FFmpeg.State; using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Extensions; using Microsoft.EntityFrameworkCore; +using MediaStream = ErsatzTV.Core.Domain.MediaStream; +using PlayoutItem = ErsatzTV.Core.Domain.PlayoutItem; namespace ErsatzTV.Infrastructure.Scheduling; @@ -26,6 +30,8 @@ public class PlayoutItemConverter( ICustomStreamSelector customStreamSelector, IFFmpegStreamSelector ffmpegStreamSelector, IWatermarkSelector watermarkSelector, + IGraphicsElementSelector graphicsElementSelector, + IGraphicsElementLoader graphicsElementLoader, IDbContextFactory dbContextFactory) : IPlayoutItemConverter { public async Task> ToNext( @@ -207,11 +213,14 @@ public class PlayoutItemConverter( subtitles, shouldLogMessages, cancellationToken); - await SelectWatermark( + await SelectGraphics( maybeGlobalWatermark, channel, playoutItem, - nextPlayoutItem); + nextPlayoutItem, + headVersion.RFrameRate, + shouldLogMessages, + cancellationToken); } } @@ -453,7 +462,7 @@ public class PlayoutItemConverter( } } } - else if (!subtitle.Path.StartsWith("http", StringComparison.OrdinalIgnoreCase)) + else if (!IsRemoteUri(subtitle.Path)) { if (nextPlayoutItem.Tracks?.Subtitle?.Source is null) { @@ -486,12 +495,18 @@ public class PlayoutItemConverter( } } - private async Task SelectWatermark( + private async Task SelectGraphics( Option maybeGlobalWatermark, Channel channel, PlayoutItem playoutItem, - Core.Next.PlayoutItem nextPlayoutItem) + Core.Next.PlayoutItem nextPlayoutItem, + string frameRate, + bool shouldLogMessages, + CancellationToken cancellationToken) { + nextPlayoutItem.Graphics ??= []; + var result = new List>(); + List watermarks = watermarkSelector.SelectWatermarks( maybeGlobalWatermark, channel, @@ -500,71 +515,166 @@ public class PlayoutItemConverter( shouldLogMessages: false); // permanent or intermittent watermarks are supported - if (watermarks.All(wm => wm.Watermark.Mode is ChannelWatermarkMode.Permanent or ChannelWatermarkMode.Intermittent)) + IEnumerable supportedWatermarks = watermarks.Where(wm => + wm.Watermark.Mode is ChannelWatermarkMode.Permanent or ChannelWatermarkMode.Intermittent); + + foreach (WatermarkOptions watermarkOptions in supportedWatermarks) { - nextPlayoutItem.Graphics = []; + var layer = new Core.Next.GraphicsLayer + { + Location = ToGraphics(watermarkOptions.Watermark.Location), + HorizontalMarginPercent = watermarkOptions.Watermark.HorizontalMarginPercent, + VerticalMarginPercent = watermarkOptions.Watermark.VerticalMarginPercent, + OpacityPercent = watermarkOptions.Watermark.Opacity, + StreamIndex = await watermarkOptions.ImageStreamIndex.IfNoneAsync(0), + WithinSourceContent = watermarkOptions.Watermark.PlaceWithinSourceContent, + }; - foreach (WatermarkOptions watermarkOptions in watermarks.OrderBy(wm => wm.Watermark.ZIndex)) + if (watermarkOptions.Watermark.Size is WatermarkSize.Scaled) { - Core.Next.GraphicsLocation location = watermarkOptions.Watermark.Location switch + layer.WidthPercent = watermarkOptions.Watermark.WidthPercent; + } + + if (IsRemoteUri(watermarkOptions.ImagePath)) + { + layer.Source = new Core.Next.PlayoutItemSource { - WatermarkLocation.TopMiddle => Core.Next.GraphicsLocation.TopCenter, - WatermarkLocation.TopRight => Core.Next.GraphicsLocation.TopRight, - WatermarkLocation.LeftMiddle => Core.Next.GraphicsLocation.CenterLeft, - WatermarkLocation.MiddleCenter => Core.Next.GraphicsLocation.Center, - WatermarkLocation.RightMiddle => Core.Next.GraphicsLocation.CenterRight, - WatermarkLocation.BottomLeft => Core.Next.GraphicsLocation.BottomLeft, - WatermarkLocation.BottomMiddle => Core.Next.GraphicsLocation.BottomCenter, - WatermarkLocation.BottomRight => Core.Next.GraphicsLocation.BottomRight, - _ => Core.Next.GraphicsLocation.TopLeft, + SourceType = Core.Next.SourceType.Http, + Uri = watermarkOptions.ImagePath, }; - - var layer = new Core.Next.GraphicsLayer + } + else + { + layer.Source = new Core.Next.PlayoutItemSource { - Location = location, - HorizontalMarginPercent = watermarkOptions.Watermark.HorizontalMarginPercent, - VerticalMarginPercent = watermarkOptions.Watermark.VerticalMarginPercent, - OpacityPercent = watermarkOptions.Watermark.Opacity, - StreamIndex = await watermarkOptions.ImageStreamIndex.IfNoneAsync(0), - WithinSourceContent = watermarkOptions.Watermark.PlaceWithinSourceContent, + SourceType = Core.Next.SourceType.Local, + Path = watermarkOptions.ImagePath, }; + } - if (watermarkOptions.Watermark.Size is WatermarkSize.Scaled) + if (watermarkOptions.Watermark.Mode is ChannelWatermarkMode.Intermittent) + { + layer.Timing = new Core.Next.Timing { - layer.WidthPercent = watermarkOptions.Watermark.WidthPercent; - } + TimingType = Core.Next.TimingType.Periodic, + Clock = Core.Next.PeriodicClock.Wall, + FrequencyMs = watermarkOptions.Watermark.FrequencyMinutes * 60 * 1000, + HoldMs = watermarkOptions.Watermark.DurationSeconds * 1000, + }; + } - if (watermarkOptions.ImagePath.StartsWith("http", StringComparison.OrdinalIgnoreCase)) - { - layer.Source = new Core.Next.PlayoutItemSource + result.Add(new KeyValuePair(layer, watermarkOptions.Watermark.ZIndex)); + } + + List graphicsElements = graphicsElementSelector.SelectGraphicsElements( + channel, + playoutItem, + playoutItem.StartOffset, + shouldLogMessages); + + IEnumerable supportedGraphicsElements = graphicsElements + .Where(ge => ge.GraphicsElement.Kind is GraphicsElementKind.Image); + + var outputFrameSize = new Resolution + { + Width = channel.FFmpegProfile.Resolution.Width, + Height = channel.FFmpegProfile.Resolution.Height, + }; + + var squarePixelFrameSize = new Resolution + { + Width = outputFrameSize.Width, + Height = outputFrameSize.Height + }; + + var headVersion = playoutItem.MediaItem.GetHeadVersion(); + Option maybeVideoStream = headVersion.Streams + .Where(s => s.MediaStreamKind is MediaStreamKind.Video) + .HeadOrNone() + .Select(v => new VideoStream( + v.Index, + v.Codec, + v.Profile, + None, + ColorParams.Unknown, + new FrameSize(headVersion.Width, headVersion.Height), + headVersion.SampleAspectRatio, + headVersion.DisplayAspectRatio, + None, + StillImage: false, + ScanKind.Progressive)); + + foreach (var videoStream in maybeVideoStream) + { + var frameSize = + videoStream.SquarePixelFrameSize(new FrameSize(outputFrameSize.Width, outputFrameSize.Height)); + + squarePixelFrameSize.Width = frameSize.Width; + squarePixelFrameSize.Height = frameSize.Height; + } + + var context = new GraphicsEngineContext( + channel.Number, + playoutItem.MediaItem, + Elements: [], + TemplateVariables: [], + squarePixelFrameSize, + outputFrameSize, + new FrameRate(frameRate), + playoutItem.StartOffset, + playoutItem.StartOffset, + TimeSpan.Zero, + playoutItem.OutPoint - playoutItem.InPoint, + playoutItem.MediaItem.GetDurationForPlayout()); + + context = await graphicsElementLoader.LoadAll(context, [.. supportedGraphicsElements], cancellationToken); + + foreach (GraphicsElementContext element in context?.Elements ?? []) + { + switch (element) + { + case ImageElementDataContext({ } image): + // opacity expressions are not supported yet + if (!string.IsNullOrWhiteSpace(image.OpacityExpression)) { - SourceType = Core.Next.SourceType.Http, - Uri = watermarkOptions.ImagePath, - }; - } - else - { - layer.Source = new Core.Next.PlayoutItemSource + continue; + } + + var layer = new Core.Next.GraphicsLayer { - SourceType = Core.Next.SourceType.Local, - Path = watermarkOptions.ImagePath, + Location = ToGraphics(image.Location), + HorizontalMarginPercent = image.HorizontalMarginPercent, + VerticalMarginPercent = image.VerticalMarginPercent, + OpacityPercent = image.OpacityPercent, + StreamIndex = 0, + WithinSourceContent = image.PlaceWithinSourceContent, + WidthPercent = image.Scale ? image.ScaleWidthPercent ?? 100 : null, }; - } - if (watermarkOptions.Watermark.Mode is ChannelWatermarkMode.Intermittent) - { - layer.Timing = new Core.Next.Timing + if (IsRemoteUri(image.Image)) { - TimingType = Core.Next.TimingType.Periodic, - Clock = Core.Next.PeriodicClock.Wall, - FrequencyMs = watermarkOptions.Watermark.FrequencyMinutes * 60 * 1000, - HoldMs = watermarkOptions.Watermark.DurationSeconds * 1000, - }; - } + layer.Source = new Core.Next.PlayoutItemSource + { + SourceType = Core.Next.SourceType.Http, + Uri = image.Image, + }; + } + else + { + layer.Source = new Core.Next.PlayoutItemSource + { + SourceType = Core.Next.SourceType.Local, + Path = image.Image, + }; + } - nextPlayoutItem.Graphics.Add(layer); + result.Add(new KeyValuePair(layer, image.ZIndex ?? 0)); + break; } } + + nextPlayoutItem.Graphics.Clear(); + nextPlayoutItem.Graphics.AddRange(result.OrderBy(kvp => kvp.Value).Select(kvp => kvp.Key)); } private static async Task> GetSubtitles( @@ -642,4 +752,23 @@ public class PlayoutItemConverter( } } } + + private static Core.Next.GraphicsLocation ToGraphics(WatermarkLocation watermarkLocation) => + watermarkLocation switch + { + WatermarkLocation.TopMiddle => Core.Next.GraphicsLocation.TopCenter, + WatermarkLocation.TopRight => Core.Next.GraphicsLocation.TopRight, + WatermarkLocation.LeftMiddle => Core.Next.GraphicsLocation.CenterLeft, + WatermarkLocation.MiddleCenter => Core.Next.GraphicsLocation.Center, + WatermarkLocation.RightMiddle => Core.Next.GraphicsLocation.CenterRight, + WatermarkLocation.BottomLeft => Core.Next.GraphicsLocation.BottomLeft, + WatermarkLocation.BottomMiddle => Core.Next.GraphicsLocation.BottomCenter, + WatermarkLocation.BottomRight => Core.Next.GraphicsLocation.BottomRight, + _ => Core.Next.GraphicsLocation.TopLeft + }; + + private static bool IsRemoteUri(string path) => + Uri.TryCreate(path, UriKind.Absolute, out Uri uriResult) + && (uriResult.Scheme == Uri.UriSchemeHttp || + uriResult.Scheme == Uri.UriSchemeHttps); } diff --git a/ErsatzTV.Infrastructure/Streaming/Graphics/GraphicsElementLoader.cs b/ErsatzTV.Infrastructure/Streaming/Graphics/GraphicsElementLoader.cs index ff0715a9e..62570a91d 100644 --- a/ErsatzTV.Infrastructure/Streaming/Graphics/GraphicsElementLoader.cs +++ b/ErsatzTV.Infrastructure/Streaming/Graphics/GraphicsElementLoader.cs @@ -1,3 +1,4 @@ +using System.Collections.Concurrent; using System.IO.Abstractions; using System.Text; using System.Text.RegularExpressions; @@ -23,6 +24,8 @@ public partial class GraphicsElementLoader( ILogger logger) : IGraphicsElementLoader { + private readonly ConcurrentDictionary _yamlCache = new(); + public async Task LoadAll( GraphicsEngineContext context, List elements, @@ -31,7 +34,7 @@ public partial class GraphicsElementLoader( try { // get max epg entries - int epgEntries = await GetMaxEpgEntries(elements); + int epgEntries = await GetMaxEpgEntries(elements, cancellationToken); // init template element variables once Dictionary templateVariables = @@ -49,7 +52,8 @@ public partial class GraphicsElementLoader( { Option maybeElement = await LoadText( reference.GraphicsElement.Path, - templateVariables); + templateVariables, + cancellationToken); if (maybeElement.IsNone) { logger.LogWarning( @@ -68,7 +72,8 @@ public partial class GraphicsElementLoader( { Option maybeElement = await LoadImage( reference.GraphicsElement.Path, - templateVariables); + templateVariables, + cancellationToken); if (maybeElement.IsNone) { logger.LogWarning( @@ -87,7 +92,8 @@ public partial class GraphicsElementLoader( { Option maybeElement = await LoadMotion( reference.GraphicsElement.Path, - templateVariables); + templateVariables, + cancellationToken); if (maybeElement.IsNone) { logger.LogWarning( @@ -106,7 +112,8 @@ public partial class GraphicsElementLoader( { Option maybeElement = await LoadSubtitle( reference.GraphicsElement.Path, - templateVariables); + templateVariables, + cancellationToken); if (maybeElement.IsNone) { logger.LogWarning( @@ -132,7 +139,8 @@ public partial class GraphicsElementLoader( { Option maybeElement = await LoadScript( reference.GraphicsElement.Path, - templateVariables); + templateVariables, + cancellationToken); if (maybeElement.IsNone) { logger.LogWarning( @@ -169,7 +177,7 @@ public partial class GraphicsElementLoader( { try { - string yaml = await fileSystem.File.ReadAllTextAsync(fileName, cancellationToken); + string yaml = await ReadFile(fileName, cancellationToken); var template = Template.Parse(yaml); var builder = new StringBuilder(); @@ -200,7 +208,7 @@ public partial class GraphicsElementLoader( return Option.None; } - private async Task GetMaxEpgEntries(List elements) + private async Task GetMaxEpgEntries(List elements, CancellationToken cancellationToken) { var epgEntries = 0; @@ -212,7 +220,8 @@ public partial class GraphicsElementLoader( { try { - foreach (string line in await fileSystem.File.ReadAllLinesAsync(reference.GraphicsElement.Path)) + string text = await ReadFile(reference.GraphicsElement.Path, cancellationToken); + foreach (string line in text.Split("\n")) { Match match = EpgEntriesRegex().Match(line); if (!match.Success || !int.TryParse(match.Groups[1].Value, out int value)) @@ -235,20 +244,35 @@ public partial class GraphicsElementLoader( return epgEntries; } - private Task> LoadImage(string fileName, Dictionary variables) => - GetTemplatedYaml(fileName, variables).BindT(FromYaml); - - private Task> LoadText(string fileName, Dictionary variables) => - GetTemplatedYaml(fileName, variables).BindT(FromYaml); - - private Task> LoadMotion(string fileName, Dictionary variables) => - GetTemplatedYaml(fileName, variables).BindT(FromYaml); - - private Task> LoadSubtitle(string fileName, Dictionary variables) => - GetTemplatedYaml(fileName, variables).BindT(FromYaml); - - private Task> LoadScript(string fileName, Dictionary variables) => - GetTemplatedYaml(fileName, variables).BindT(FromYaml); + private Task> LoadImage( + string fileName, + Dictionary variables, + CancellationToken cancellationToken) => + GetTemplatedYaml(fileName, variables, cancellationToken).BindT(FromYaml); + + private Task> LoadText( + string fileName, + Dictionary variables, + CancellationToken cancellationToken) => + GetTemplatedYaml(fileName, variables, cancellationToken).BindT(FromYaml); + + private Task> LoadMotion( + string fileName, + Dictionary variables, + CancellationToken cancellationToken) => + GetTemplatedYaml(fileName, variables, cancellationToken).BindT(FromYaml); + + private Task> LoadSubtitle( + string fileName, + Dictionary variables, + CancellationToken cancellationToken) => + GetTemplatedYaml(fileName, variables, cancellationToken).BindT(FromYaml); + + private Task> LoadScript( + string fileName, + Dictionary variables, + CancellationToken cancellationToken) => + GetTemplatedYaml(fileName, variables, cancellationToken).BindT(FromYaml); private async Task> InitTemplateVariables( GraphicsEngineContext context, @@ -280,23 +304,29 @@ public partial class GraphicsElementLoader( } // epg variables - DateTimeOffset startTime = context.ContentStartTime + context.Seek; - Option> maybeEpgData = - await templateDataRepository.GetEpgTemplateData(context.ChannelNumber, startTime, epgEntries); - foreach (Dictionary templateData in maybeEpgData) + if (epgEntries > 0) { - foreach (KeyValuePair variable in templateData) + DateTimeOffset startTime = context.ContentStartTime + context.Seek; + Option> maybeEpgData = + await templateDataRepository.GetEpgTemplateData(context.ChannelNumber, startTime, epgEntries); + foreach (Dictionary templateData in maybeEpgData) { - result.Add(variable.Key, variable.Value); + foreach (KeyValuePair variable in templateData) + { + result.Add(variable.Key, variable.Value); + } } } return result; } - private async Task> GetTemplatedYaml(string fileName, Dictionary variables) + private async Task> GetTemplatedYaml( + string fileName, + Dictionary variables, + CancellationToken cancellationToken) { - string yaml = await fileSystem.File.ReadAllTextAsync(fileName); + string yaml = await ReadFile(fileName, cancellationToken); try { var scriptObject = new ScriptObject(); @@ -304,7 +334,9 @@ public partial class GraphicsElementLoader( scriptObject.Import("convert_timezone", templateFunctions.ConvertTimeZone); scriptObject.Import("format_datetime", templateFunctions.FormatDateTime); scriptObject.Import("get_directory_name", (string path) => Path.GetDirectoryName(path)); - scriptObject.Import("get_filename_without_extension", (string path) => Path.GetFileNameWithoutExtension(path)); + scriptObject.Import( + "get_filename_without_extension", + (string path) => Path.GetFileNameWithoutExtension(path)); var context = new TemplateContext { MemberRenamer = member => member.Name }; context.PushGlobal(scriptObject); @@ -361,6 +393,20 @@ public partial class GraphicsElementLoader( } } + private async Task ReadFile(string fileName, CancellationToken cancellationToken) + { + string key = fileSystem.Path.GetFullPath(fileName); + if (_yamlCache.TryGetValue(key, out string cached)) + { + return cached; + } + + string yaml = await fileSystem.File.ReadAllTextAsync(key, cancellationToken); + _yamlCache.TryAdd(key, yaml); + + return yaml; + } + [GeneratedRegex(@"epg_entries:\s*(\d+)")] private static partial Regex EpgEntriesRegex(); diff --git a/ErsatzTV/Pages/Troubleshooting/PlaybackTroubleshooting.razor b/ErsatzTV/Pages/Troubleshooting/PlaybackTroubleshooting.razor index 5e643c05d..7089a5f36 100644 --- a/ErsatzTV/Pages/Troubleshooting/PlaybackTroubleshooting.razor +++ b/ErsatzTV/Pages/Troubleshooting/PlaybackTroubleshooting.razor @@ -126,22 +126,19 @@ } - if (_streamingEngine is StreamingEngine.Legacy) - { - -
- Graphics Elements -
- - @foreach (GraphicsElementViewModel graphicsElement in _graphicsElements) - { - @graphicsElement.Name - } - -
- } + +
+ Graphics Elements +
+ + @foreach (GraphicsElementViewModel graphicsElement in _graphicsElements) + { + @graphicsElement.Name + } + +
Start From Beginning