Browse Source

add subtitle graphics element (#2321)

pull/2322/head
Jason Dove 9 months ago committed by GitHub
parent
commit
d71443ef60
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 13
      CHANGELOG.md
  2. 18
      ErsatzTV.Application/Graphics/Commands/RefreshGraphicsElementsHandler.cs
  3. 1
      ErsatzTV.Application/Graphics/Mapper.cs
  4. 3
      ErsatzTV.Core/Domain/GraphicsElementKind.cs
  5. 34
      ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs
  6. 2
      ErsatzTV.Core/FFmpeg/TempFilePool.cs
  7. 2
      ErsatzTV.Core/FileSystemLayout.cs
  8. 26
      ErsatzTV.Core/Graphics/StyleDefinition.cs
  9. 39
      ErsatzTV.Core/Graphics/SubtitleGraphicsElement.cs
  10. 25
      ErsatzTV.Core/Graphics/TextGraphicsElement.cs
  11. 20
      ErsatzTV.Core/Interfaces/Streaming/GraphicsEngineContext.cs
  12. 6
      ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj.DotSettings
  13. 2
      ErsatzTV.Infrastructure/Streaming/Graphics/Fonts/CustomFontMapper.cs
  14. 2
      ErsatzTV.Infrastructure/Streaming/Graphics/Fonts/GraphicsEngineFonts.cs
  15. 34
      ErsatzTV.Infrastructure/Streaming/Graphics/GraphicsEngine.cs
  16. 2
      ErsatzTV.Infrastructure/Streaming/Graphics/Image/ImageElement.cs
  17. 2
      ErsatzTV.Infrastructure/Streaming/Graphics/Image/WatermarkElement.cs
  18. 158
      ErsatzTV.Infrastructure/Streaming/Graphics/Subtitle/SubtitleElement.cs
  19. 2
      ErsatzTV.Infrastructure/Streaming/Graphics/Text/TemplateFunctions.cs
  20. 3
      ErsatzTV.Infrastructure/Streaming/Graphics/Text/TextElement.cs
  21. 3
      ErsatzTV/Startup.cs

13
CHANGELOG.md

@ -30,9 +30,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -30,9 +30,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- EPG data can also load a configurable number of subsequent (up next) entries
- Media Item data is sourced from the currently playing media item
- Add `image` graphics element type
- Supported in playback troubleshooting and YAML playouts
- Displays an image, similar to a watermark
- Supports constant opacity and opacity expression
- Supported in playback troubleshooting and YAML playouts
- Displays an image, similar to a watermark
- Supports constant opacity and opacity expression
- Add `subtitle` graphics element type
- Supported in playback troubleshooting and YAML playouts
- Supports SRT and SSA/ASS subtitle formats
- Supports EPG and Media Item variable replacement
- EPG data is sourced from XMLTV for the current time
- EPG data can also load a configurable number of subsequent (up next) entries
- Media Item data is sourced from the currently playing media item
- YAML playout: add `graphics_on` and `graphics_off` instructions to control graphics elements
- `graphics_on` requires the name of a graphics element template, e.g. `text/cool_element.yml`
- The `variables` property can be used to dynamically replace text from the template

18
ErsatzTV.Application/Graphics/Commands/RefreshGraphicsElementsHandler.cs

@ -66,6 +66,24 @@ public class RefreshGraphicsElementsHandler( @@ -66,6 +66,24 @@ public class RefreshGraphicsElementsHandler(
await dbContext.AddAsync(graphicsElement, cancellationToken);
}
// add new subtitle elements
var newSubtitlePaths = localFileSystem.ListFiles(FileSystemLayout.GraphicsElementsSubtitleTemplatesFolder)
.Where(f => allExisting.All(e => e.Path != f))
.ToList();
foreach (var path in newSubtitlePaths)
{
logger.LogDebug("Adding new graphics element from file {File}", path);
var graphicsElement = new GraphicsElement
{
Path = path,
Kind = GraphicsElementKind.Subtitle
};
await dbContext.AddAsync(graphicsElement, cancellationToken);
}
await dbContext.SaveChangesAsync(cancellationToken);
}
}

1
ErsatzTV.Application/Graphics/Mapper.cs

@ -11,6 +11,7 @@ public static class Mapper @@ -11,6 +11,7 @@ public static class Mapper
{
GraphicsElementKind.Text => new GraphicsElementViewModel(graphicsElement.Id, $"text/{fileName}"),
GraphicsElementKind.Image => new GraphicsElementViewModel(graphicsElement.Id, $"image/{fileName}"),
GraphicsElementKind.Subtitle => new GraphicsElementViewModel(graphicsElement.Id, $"subtitle/{fileName}"),
_ => new GraphicsElementViewModel(graphicsElement.Id, graphicsElement.Path)
};
}

3
ErsatzTV.Core/Domain/GraphicsElementKind.cs

@ -3,5 +3,6 @@ namespace ErsatzTV.Core.Domain; @@ -3,5 +3,6 @@ namespace ErsatzTV.Core.Domain;
public enum GraphicsElementKind
{
Image = 0,
Text = 1
Text = 1,
Subtitle = 2
}

34
ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs

@ -472,7 +472,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService @@ -472,7 +472,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
playoutItemGraphicsElement.Variables);
}
graphicsElementContexts.Add(new TextElementContext(element, variables));
graphicsElementContexts.Add(new TextElementDataContext(element, variables));
}
break;
@ -490,18 +490,36 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService @@ -490,18 +490,36 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
foreach (var element in maybeElement)
{
// var variables = new Dictionary<string, string>();
// if (!string.IsNullOrWhiteSpace(playoutItemGraphicsElement.Variables))
// {
// variables = JsonConvert.DeserializeObject<Dictionary<string, string>>(
// playoutItemGraphicsElement.Variables);
// }
graphicsElementContexts.Add(new ImageElementContext(element));
}
break;
}
case GraphicsElementKind.Subtitle:
{
var maybeElement =
await SubtitlesGraphicsElement.FromFile(playoutItemGraphicsElement.GraphicsElement.Path);
if (maybeElement.IsNone)
{
_logger.LogWarning(
"Failed to load subtitle graphics element from file {Path}; ignoring",
playoutItemGraphicsElement.GraphicsElement.Path);
}
foreach (var element in maybeElement)
{
var variables = new Dictionary<string, string>();
if (!string.IsNullOrWhiteSpace(playoutItemGraphicsElement.Variables))
{
variables = JsonConvert.DeserializeObject<Dictionary<string, string>>(
playoutItemGraphicsElement.Variables);
}
graphicsElementContexts.Add(new SubtitleElementDataContext(element, variables));
}
break;
}
default:
_logger.LogInformation(
"Ignoring unsupported graphics element kind {Kind}",

2
ErsatzTV.Core/FFmpeg/TempFilePool.cs

@ -5,7 +5,7 @@ namespace ErsatzTV.Core.FFmpeg; @@ -5,7 +5,7 @@ namespace ErsatzTV.Core.FFmpeg;
public class TempFilePool : ITempFilePool
{
private const int ItemLimit = 10;
private readonly object _lock = new();
private readonly Lock _lock = new();
private readonly Dictionary<TempFileCategory, int> _state = new();
public string GetNextTempFile(TempFileCategory category)

2
ErsatzTV.Core/FileSystemLayout.cs

@ -50,6 +50,7 @@ public static class FileSystemLayout @@ -50,6 +50,7 @@ public static class FileSystemLayout
public static readonly string GraphicsElementsTemplatesFolder;
public static readonly string GraphicsElementsTextTemplatesFolder;
public static readonly string GraphicsElementsImageTemplatesFolder;
public static readonly string GraphicsElementsSubtitleTemplatesFolder;
public static readonly string ScriptsFolder;
@ -169,6 +170,7 @@ public static class FileSystemLayout @@ -169,6 +170,7 @@ public static class FileSystemLayout
GraphicsElementsTemplatesFolder = Path.Combine(TemplatesFolder, "graphics-elements");
GraphicsElementsTextTemplatesFolder = Path.Combine(GraphicsElementsTemplatesFolder, "text");
GraphicsElementsImageTemplatesFolder = Path.Combine(GraphicsElementsTemplatesFolder, "image");
GraphicsElementsSubtitleTemplatesFolder = Path.Combine(GraphicsElementsTemplatesFolder, "subtitle");
ScriptsFolder = Path.Combine(AppDataFolder, "scripts");

26
ErsatzTV.Core/Graphics/StyleDefinition.cs

@ -1,26 +0,0 @@ @@ -1,26 +0,0 @@
using YamlDotNet.Serialization;
namespace ErsatzTV.Core.Graphics;
public class StyleDefinition
{
public string Name { get; set; }
[YamlMember(Alias = "font_size", ApplyNamingConventions = false)]
public float? FontSize { get; set; }
[YamlMember(Alias = "font_weight", ApplyNamingConventions = false)]
public int? FontWeight { get; set; }
[YamlMember(Alias = "font_italic", ApplyNamingConventions = false)]
public bool? FontItalic { get; set; }
[YamlMember(Alias = "font_family", ApplyNamingConventions = false)]
public string FontFamily { get; set; }
[YamlMember(Alias = "text_color", ApplyNamingConventions = false)]
public string TextColor { get; set; }
[YamlMember(Alias = "letter_spacing", ApplyNamingConventions = false)]
public float? LetterSpacing { get; set; }
}

39
ErsatzTV.Core/Graphics/SubtitleGraphicsElement.cs

@ -0,0 +1,39 @@ @@ -0,0 +1,39 @@
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
namespace ErsatzTV.Core.Graphics;
public class SubtitlesGraphicsElement
{
[YamlMember(Alias = "z_index", ApplyNamingConventions = false)]
public int? ZIndex { get; set; }
[YamlMember(Alias = "epg_entries", ApplyNamingConventions = false)]
public int EpgEntries { get; set; }
public string Template { get; set; }
public static async Task<Option<SubtitlesGraphicsElement>> FromFile(string fileName)
{
try
{
string yaml = await File.ReadAllTextAsync(fileName);
// 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;
}
}
}

25
ErsatzTV.Core/Graphics/TextGraphicsElement.cs

@ -68,4 +68,27 @@ public class TextGraphicsElement @@ -68,4 +68,27 @@ public class TextGraphicsElement
return Option<TextGraphicsElement>.None;
}
}
}
}
public class StyleDefinition
{
public string Name { get; set; }
[YamlMember(Alias = "font_size", ApplyNamingConventions = false)]
public float? FontSize { get; set; }
[YamlMember(Alias = "font_weight", ApplyNamingConventions = false)]
public int? FontWeight { get; set; }
[YamlMember(Alias = "font_italic", ApplyNamingConventions = false)]
public bool? FontItalic { get; set; }
[YamlMember(Alias = "font_family", ApplyNamingConventions = false)]
public string FontFamily { get; set; }
[YamlMember(Alias = "text_color", ApplyNamingConventions = false)]
public string TextColor { get; set; }
[YamlMember(Alias = "letter_spacing", ApplyNamingConventions = false)]
public float? LetterSpacing { get; set; }
}

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

@ -20,7 +20,21 @@ public abstract record GraphicsElementContext; @@ -20,7 +20,21 @@ public abstract record GraphicsElementContext;
public record WatermarkElementContext(WatermarkOptions Options) : GraphicsElementContext;
public record TextElementContext(TextGraphicsElement TextElement, Dictionary<string, string> Variables)
: GraphicsElementContext;
public record TextElementDataContext(TextGraphicsElement TextElement, Dictionary<string, string> Variables)
: GraphicsElementContext, ITemplateDataContext
{
public int EpgEntries => TextElement.EpgEntries;
}
public record ImageElementContext(ImageGraphicsElement ImageElement) : GraphicsElementContext;
public record ImageElementContext(ImageGraphicsElement ImageElement) : GraphicsElementContext;
public record SubtitleElementDataContext(SubtitlesGraphicsElement SubtitlesElement, Dictionary<string, string> Variables)
: GraphicsElementContext, ITemplateDataContext
{
public int EpgEntries => SubtitlesElement.EpgEntries;
}
public interface ITemplateDataContext
{
int EpgEntries { get; }
}

6
ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj.DotSettings

@ -3,4 +3,8 @@ @@ -3,4 +3,8 @@
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=data_005Cconfigurations_005Clibrary/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=data_005Cconfigurations_005Cmediaitem/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=data_005Cconfigurations_005Cmediasource/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=data_005Cconfigurations_005Cmetadata/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=data_005Cconfigurations_005Cmetadata/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=streaming_005Cgraphics_005Cfonts/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=streaming_005Cgraphics_005Cimage/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=streaming_005Cgraphics_005Csubtitle/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=streaming_005Cgraphics_005Ctext/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

2
ErsatzTV.Infrastructure/Streaming/Graphics/Fonts/CustomFontMapper.cs

@ -2,7 +2,7 @@ using Microsoft.Extensions.Logging; @@ -2,7 +2,7 @@ using Microsoft.Extensions.Logging;
using SkiaSharp;
using Topten.RichTextKit;
namespace ErsatzTV.Infrastructure.Streaming.Graphics.Fonts;
namespace ErsatzTV.Infrastructure.Streaming.Graphics;
public sealed class CustomFontMapper(ILogger<CustomFontMapper> logger) : FontMapper
{

2
ErsatzTV.Infrastructure/Streaming/Graphics/Fonts/GraphicsEngineFonts.cs

@ -1,6 +1,6 @@ @@ -1,6 +1,6 @@
using Topten.RichTextKit;
namespace ErsatzTV.Infrastructure.Streaming.Graphics.Fonts;
namespace ErsatzTV.Infrastructure.Streaming.Graphics;
public class GraphicsEngineFonts(CustomFontMapper mapper)
{

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

@ -1,11 +1,9 @@ @@ -1,11 +1,9 @@
using System.IO.Pipelines;
using ErsatzTV.Core;
using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Streaming;
using ErsatzTV.Core.Metadata;
using ErsatzTV.Infrastructure.Streaming.Graphics.Fonts;
using ErsatzTV.Infrastructure.Streaming.Graphics.Image;
using ErsatzTV.Infrastructure.Streaming.Graphics.Text;
using Microsoft.Extensions.Logging;
using SkiaSharp;
@ -14,6 +12,7 @@ namespace ErsatzTV.Infrastructure.Streaming.Graphics; @@ -14,6 +12,7 @@ namespace ErsatzTV.Infrastructure.Streaming.Graphics;
public class GraphicsEngine(
TemplateFunctions templateFunctions,
GraphicsEngineFonts graphicsEngineFonts,
ITempFilePool tempFilePool,
ITemplateDataRepository templateDataRepository,
ILogger<GraphicsEngine> logger)
: IGraphicsEngine
@ -24,8 +23,8 @@ public class GraphicsEngine( @@ -24,8 +23,8 @@ public class GraphicsEngine(
var templateVariables = new Dictionary<string, object>();
// init text element variables once
if (context.Elements.OfType<TextElementContext>().Any())
// init template element variables once
if (context.Elements.OfType<ITemplateDataContext>().Any())
{
// common variables
templateVariables[MediaItemTemplateDataKey.Resolution] = context.FrameSize;
@ -43,7 +42,7 @@ public class GraphicsEngine( @@ -43,7 +42,7 @@ public class GraphicsEngine(
}
// epg variables
int maxEpg = context.Elements.OfType<TextElementContext>().Max(c => c.TextElement.EpgEntries);
int maxEpg = context.Elements.OfType<ITemplateDataContext>().Max(c => c.EpgEntries);
var startTime = context.ContentStartTime + context.Seek;
var maybeEpgData =
await templateDataRepository.GetEpgTemplateData(context.ChannelNumber, startTime, maxEpg);
@ -73,7 +72,8 @@ public class GraphicsEngine( @@ -73,7 +72,8 @@ public class GraphicsEngine(
elements.Add(new ImageElement(imageElementContext.ImageElement, logger));
break;
case TextElementContext textElementContext:
case TextElementDataContext textElementContext:
{
var variables = templateVariables.ToDictionary();
foreach (var variable in textElementContext.Variables)
{
@ -89,6 +89,26 @@ public class GraphicsEngine( @@ -89,6 +89,26 @@ public class GraphicsEngine(
elements.Add(textElement);
break;
}
case SubtitleElementDataContext subtitleElementContext:
{
var variables = templateVariables.ToDictionary();
foreach (var variable in subtitleElementContext.Variables)
{
variables.Add(variable.Key, variable.Value);
}
var subtitleElement = new SubtitleElement(
templateFunctions,
tempFilePool,
subtitleElementContext.SubtitlesElement,
variables,
logger);
elements.Add(subtitleElement);
break;
}
}
}

2
ErsatzTV.Infrastructure/Streaming/Graphics/Image/ImageElement.cs

@ -4,7 +4,7 @@ using Microsoft.Extensions.Logging; @@ -4,7 +4,7 @@ using Microsoft.Extensions.Logging;
using NCalc;
using SkiaSharp;
namespace ErsatzTV.Infrastructure.Streaming.Graphics.Image;
namespace ErsatzTV.Infrastructure.Streaming.Graphics;
public class ImageElement(ImageGraphicsElement imageGraphicsElement, ILogger logger) : GraphicsElement, IDisposable
{

2
ErsatzTV.Infrastructure/Streaming/Graphics/Image/WatermarkElement.cs

@ -5,7 +5,7 @@ using Microsoft.Extensions.Logging; @@ -5,7 +5,7 @@ using Microsoft.Extensions.Logging;
using NCalc;
using SkiaSharp;
namespace ErsatzTV.Infrastructure.Streaming.Graphics.Image;
namespace ErsatzTV.Infrastructure.Streaming.Graphics;
public class WatermarkElement : GraphicsElement, IDisposable
{

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

@ -0,0 +1,158 @@ @@ -0,0 +1,158 @@
using System.Buffers;
using System.IO.Pipelines;
using CliWrap;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.FFmpeg;
using ErsatzTV.Core.Graphics;
using ErsatzTV.Core.Interfaces.FFmpeg;
using Microsoft.Extensions.Logging;
using Scriban;
using Scriban.Runtime;
using SkiaSharp;
namespace ErsatzTV.Infrastructure.Streaming.Graphics;
public class SubtitleElement(
TemplateFunctions templateFunctions,
ITempFilePool tempFilePool,
SubtitlesGraphicsElement subtitlesElement,
Dictionary<string, object> variables,
ILogger logger)
: GraphicsElement, IDisposable
{
private CommandTask<CommandResult> _commandTask;
private CancellationTokenSource _cancellationTokenSource;
private PipeReader _pipeReader;
private SKBitmap _videoFrame;
private int _frameSize;
private SKPointI _point;
public override async Task InitializeAsync(
Resolution squarePixelFrameSize,
Resolution frameSize,
int frameRate,
CancellationToken cancellationToken)
{
try
{
var pipe = new Pipe();
_pipeReader = pipe.Reader;
// video size is the same as the main frame size
_frameSize = frameSize.Width * frameSize.Height * 4;
_videoFrame = new SKBitmap(frameSize.Width, frameSize.Height, SKColorType.Bgra8888, SKAlphaType.Unpremul);
// subtitles contain their own positioning info
_point = SKPointI.Empty;
var subtitleTemplateFile = tempFilePool.GetNextTempFile(TempFileCategory.Subtitle);
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);
var inputText = await File.ReadAllTextAsync(subtitlesElement.Template, cancellationToken);
string textToRender = await Template.Parse(inputText).RenderAsync(context);
await File.WriteAllTextAsync(subtitleTemplateFile, textToRender, cancellationToken);
var subtitleFile = Path.GetFileName(subtitleTemplateFile);
List<string> arguments =
[
"-f", "lavfi",
"-i",
$"color=c=black@0.0:s={frameSize.Width}x{frameSize.Height}:r={frameRate},format=bgra,subtitles='{subtitleFile}':alpha=1",
"-f", "image2pipe",
"-pix_fmt", "bgra",
"-vcodec", "rawvideo",
"-"
];
Command command = Cli.Wrap("ffmpeg")
.WithArguments(arguments)
.WithWorkingDirectory(FileSystemLayout.TempFilePoolFolder)
.WithStandardOutputPipe(PipeTarget.ToStream(pipe.Writer.AsStream()));
_cancellationTokenSource = new CancellationTokenSource();
var linkedToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _cancellationTokenSource.Token);
_commandTask = command.ExecuteAsync(linkedToken.Token);
}
catch (Exception ex)
{
IsFailed = true;
logger.LogWarning(ex, "Failed to initialize subtitle element; will disable for this content");
}
}
public override async ValueTask<Option<PreparedElementImage>> PrepareImage(
TimeSpan timeOfDay,
TimeSpan contentTime,
TimeSpan contentTotalTime,
TimeSpan channelTime,
CancellationToken cancellationToken)
{
while (true)
{
ReadResult readResult = await _pipeReader.ReadAsync(cancellationToken);
var buffer = readResult.Buffer;
var consumed = buffer.Start;
var examined = buffer.End;
try
{
if (buffer.Length >= _frameSize)
{
var sequence = buffer.Slice(0, _frameSize);
using (SKPixmap pixmap = _videoFrame.PeekPixels())
{
sequence.CopyTo(pixmap.GetPixelSpan());
}
// mark this frame as consumed
consumed = sequence.End;
// we are done, return the frame
return new PreparedElementImage(_videoFrame, _point, 1.0f, false);
}
if (readResult.IsCompleted)
{
await _pipeReader.CompleteAsync();
return Option<PreparedElementImage>.None;
}
}
finally
{
// advance the reader, consuming the processed frame and examining the entire buffer
_pipeReader.AdvanceTo(consumed, examined);
}
}
}
public void Dispose()
{
GC.SuppressFinalize(this);
_pipeReader?.Complete();
_cancellationTokenSource?.Cancel();
try
{
#pragma warning disable VSTHRD002
_commandTask?.Task.Wait();
#pragma warning restore VSTHRD002
}
catch (Exception)
{
// do nothing
}
_cancellationTokenSource?.Dispose();
_videoFrame?.Dispose();
}
}

2
ErsatzTV.Infrastructure/Streaming/Graphics/Text/TemplateFunctions.cs

@ -2,7 +2,7 @@ using System.Globalization; @@ -2,7 +2,7 @@ using System.Globalization;
using Microsoft.Extensions.Logging;
using TimeZoneConverter;
namespace ErsatzTV.Infrastructure.Streaming.Graphics.Text;
namespace ErsatzTV.Infrastructure.Streaming.Graphics;
public class TemplateFunctions(ILogger<TemplateFunctions> logger)
{

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

@ -1,7 +1,6 @@ @@ -1,7 +1,6 @@
using System.Text.RegularExpressions;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Graphics;
using ErsatzTV.Infrastructure.Streaming.Graphics.Fonts;
using Microsoft.Extensions.Logging;
using NCalc;
using Topten.RichTextKit;
@ -10,7 +9,7 @@ using Scriban.Runtime; @@ -10,7 +9,7 @@ using Scriban.Runtime;
using SkiaSharp;
using RichTextKit=Topten.RichTextKit;
namespace ErsatzTV.Infrastructure.Streaming.Graphics.Text;
namespace ErsatzTV.Infrastructure.Streaming.Graphics;
public partial class TextElement(
TemplateFunctions templateFunctions,

3
ErsatzTV/Startup.cs

@ -66,8 +66,6 @@ using ErsatzTV.Infrastructure.Search; @@ -66,8 +66,6 @@ using ErsatzTV.Infrastructure.Search;
using ErsatzTV.Infrastructure.Sqlite.Data;
using ErsatzTV.Infrastructure.Streaming;
using ErsatzTV.Infrastructure.Streaming.Graphics;
using ErsatzTV.Infrastructure.Streaming.Graphics.Fonts;
using ErsatzTV.Infrastructure.Streaming.Graphics.Text;
using ErsatzTV.Infrastructure.Trakt;
using ErsatzTV.Serialization;
using ErsatzTV.Services;
@ -342,6 +340,7 @@ public class Startup @@ -342,6 +340,7 @@ public class Startup
FileSystemLayout.GraphicsElementsTemplatesFolder,
FileSystemLayout.GraphicsElementsTextTemplatesFolder,
FileSystemLayout.GraphicsElementsImageTemplatesFolder,
FileSystemLayout.GraphicsElementsSubtitleTemplatesFolder,
FileSystemLayout.ScriptsFolder,
FileSystemLayout.MultiEpisodeShuffleTemplatesFolder,
FileSystemLayout.AudioStreamSelectorScriptsFolder

Loading…
Cancel
Save