Browse Source

refactor graphics engine; async frame generation

pull/2282/head
Jason Dove 2 days ago
parent
commit
ef8f38a769
No known key found for this signature in database
  1. 47
      ErsatzTV.Infrastructure/Streaming/GraphicsEngine.cs
  2. 5
      ErsatzTV.Infrastructure/Streaming/IGraphicsElement.cs
  3. 6
      ErsatzTV.Infrastructure/Streaming/PreparedElementImage.cs
  4. 13
      ErsatzTV.Infrastructure/Streaming/WatermarkElement.cs

47
ErsatzTV.Infrastructure/Streaming/GraphicsEngine.cs

@ -58,30 +58,39 @@ public class GraphicsEngine(ILogger<GraphicsEngine> logger) : IGraphicsEngine @@ -58,30 +58,39 @@ public class GraphicsEngine(ILogger<GraphicsEngine> logger) : IGraphicsEngine
context.FrameSize.Height,
Color.Transparent);
// prepare images outside mutate to allow async image generation
var preparedElementImages = new List<PreparedElementImage>();
foreach (var element in elements.Where(e => !e.IsFailed).OrderBy(e => e.ZIndex))
{
try
{
var maybePreparedImage = await element.PrepareImage(
frameTime.TimeOfDay,
contentTime,
contentTotalTime,
channelTime,
cancellationToken);
preparedElementImages.AddRange(maybePreparedImage);
}
catch (Exception ex)
{
element.IsFailed = true;
logger.LogWarning(ex,
"Failed to draw graphics element of type {Type}; will disable for this content",
element.GetType().Name);
}
}
// draw each element
outputFrame.Mutate(ctx =>
{
foreach (var element in elements.OrderBy(e => e.ZIndex))
foreach (var preparedImage in preparedElementImages)
{
try
{
if (!element.IsFailed)
{
element.Draw(
ctx,
frameTime.TimeOfDay,
contentTime,
contentTotalTime,
channelTime,
cancellationToken);
}
}
catch (Exception ex)
ctx.DrawImage(preparedImage.Image, preparedImage.Point, preparedImage.Opacity);
if (preparedImage.Dispose)
{
element.IsFailed = true;
logger.LogWarning(ex,
"Failed to draw graphics element of type {Type}; will disable for this content",
element.GetType().Name);
preparedImage.Image.Dispose();
}
}
});

5
ErsatzTV.Core/Interfaces/Streaming/IGraphicsElement.cs → ErsatzTV.Infrastructure/Streaming/IGraphicsElement.cs

@ -1,6 +1,6 @@ @@ -1,6 +1,6 @@
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Core.Interfaces.Streaming;
namespace ErsatzTV.Infrastructure.Streaming;
public interface IGraphicsElement
{
@ -10,8 +10,7 @@ public interface IGraphicsElement @@ -10,8 +10,7 @@ public interface IGraphicsElement
Task InitializeAsync(Resolution frameSize, int frameRate, CancellationToken cancellationToken);
void Draw(
object context,
ValueTask<Option<PreparedElementImage>> PrepareImage(
TimeSpan timeOfDay,
TimeSpan contentTime,
TimeSpan contentTotalTime,

6
ErsatzTV.Infrastructure/Streaming/PreparedElementImage.cs

@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
using SixLabors.ImageSharp;
using Image=SixLabors.ImageSharp.Image;
namespace ErsatzTV.Infrastructure.Streaming;
public record PreparedElementImage(Image Image, Point Point, float Opacity, bool Dispose);

13
ErsatzTV.Infrastructure/Streaming/WatermarkElement.cs

@ -1,7 +1,6 @@ @@ -1,7 +1,6 @@
using System.Globalization;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.FFmpeg;
using ErsatzTV.Core.Interfaces.Streaming;
using ErsatzTV.FFmpeg.State;
using NCalc;
using NCalc.Handlers;
@ -161,19 +160,13 @@ public class WatermarkElement : IGraphicsElement, IDisposable @@ -161,19 +160,13 @@ public class WatermarkElement : IGraphicsElement, IDisposable
}
}
public void Draw(
object context,
public ValueTask<Option<PreparedElementImage>> PrepareImage(
TimeSpan timeOfDay,
TimeSpan contentTime,
TimeSpan contentTotalTime,
TimeSpan channelTime,
CancellationToken cancellationToken)
{
if (context is not IImageProcessingContext imageProcessingContext)
{
return;
}
_expression.Parameters["content_seconds"] = contentTime.TotalSeconds;
_expression.Parameters["content_total_seconds"] = contentTotalTime.TotalSeconds;
_expression.Parameters["channel_seconds"] = channelTime.TotalSeconds;
@ -183,11 +176,11 @@ public class WatermarkElement : IGraphicsElement, IDisposable @@ -183,11 +176,11 @@ public class WatermarkElement : IGraphicsElement, IDisposable
float opacity = Convert.ToSingle(expressionResult, CultureInfo.InvariantCulture);
if (opacity == 0)
{
return;
return ValueTask.FromResult(Option<PreparedElementImage>.None);
}
Image frameForTimestamp = GetFrameForTimestamp(contentTime);
imageProcessingContext.DrawImage(frameForTimestamp, _location, opacity);
return ValueTask.FromResult(Optional(new PreparedElementImage(frameForTimestamp, _location, opacity, false)));
}
private Image GetFrameForTimestamp(TimeSpan timestamp)

Loading…
Cancel
Save