Browse Source

pass music video variables to text element (#2285)

* pass music video variables to text element

* remove unused file
pull/2288/head
Jason Dove 12 months ago committed by GitHub
parent
commit
075f3fcac7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 3
      CHANGELOG.md
  2. 2
      ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs
  3. 1
      ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs
  4. 11
      ErsatzTV.Core/Interfaces/Repositories/ITemplateDataRepository.cs
  5. 1
      ErsatzTV.Core/Interfaces/Streaming/GraphicsEngineContext.cs
  6. 61
      ErsatzTV.Infrastructure/Data/Repositories/TemplateDataRepository.cs
  7. 28
      ErsatzTV.Infrastructure/FFmpeg/MusicVideoCreditsGenerator.cs
  8. 28
      ErsatzTV.Infrastructure/Streaming/GraphicsEngine.cs
  9. 9
      ErsatzTV.Infrastructure/Streaming/TextElement.cs
  10. 1
      ErsatzTV/Startup.cs

3
CHANGELOG.md

@ -22,9 +22,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -22,9 +22,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Add *experimental* `Graphics Element` template system
- Graphics elements are defined in YAML files inside ETV config folder / templates / graphics-elements subfolder
- Add `Text` graphics element type
- Supported in playback troubleshooting
- Supported in playback troubleshooting and YAML playouts
- Displays multi-line text in a specified font, color, location, z-index
- Supports constant opacity and opacity expression
- Supports variable replacement for music videos
- 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

2
ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs

@ -523,7 +523,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler< @@ -523,7 +523,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
subtitles.AddRange(
await Optional(musicVideo.MusicVideoMetadata).Flatten().HeadOrNone()
.Map(mm => mm.Subtitles)
.IfNoneAsync(new List<Subtitle>()));
.IfNoneAsync([]));
break;
}

1
ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs

@ -414,6 +414,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService @@ -414,6 +414,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
graphicsEngineInput = new GraphicsEngineInput();
graphicsEngineContext = new GraphicsEngineContext(
audioVersion.MediaItem,
graphicsElementContexts,
channel.FFmpegProfile.Resolution,
await playbackSettings.FrameRate.IfNoneAsync(24),

11
ErsatzTV.Core/Interfaces/Repositories/ITemplateDataRepository.cs

@ -0,0 +1,11 @@ @@ -0,0 +1,11 @@
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Core.Interfaces.Repositories;
public interface ITemplateDataRepository
{
public Task<Option<Dictionary<string, object>>> GetMusicVideoTemplateData(
Resolution resolution,
TimeSpan streamSeek,
int musicVideoId);
}

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

@ -5,6 +5,7 @@ using ErsatzTV.Core.Graphics; @@ -5,6 +5,7 @@ using ErsatzTV.Core.Graphics;
namespace ErsatzTV.Core.Interfaces.Streaming;
public record GraphicsEngineContext(
MediaItem MediaItem,
List<GraphicsElementContext> Elements,
Resolution FrameSize,
int FrameRate,

61
ErsatzTV.Infrastructure/Data/Repositories/TemplateDataRepository.cs

@ -0,0 +1,61 @@ @@ -0,0 +1,61 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Extensions;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Infrastructure.Data.Repositories;
public class TemplateDataRepository(IDbContextFactory<TvContext> dbContextFactory) : ITemplateDataRepository
{
public async Task<Option<Dictionary<string, object>>> GetMusicVideoTemplateData(
Resolution resolution,
TimeSpan streamSeek,
int musicVideoId)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync();
Option<MusicVideo> maybeMusicVideo = await dbContext.MusicVideos
.AsNoTracking()
.Include(mv => mv.MediaVersions)
.Include(mv => mv.Artist)
.ThenInclude(a => a.ArtistMetadata)
.Include(mv => mv.MusicVideoMetadata)
.ThenInclude(mvm => mvm.Artists)
.Include(mv => mv.MusicVideoMetadata)
.ThenInclude(mvm => mvm.Studios)
.Include(mv => mv.MusicVideoMetadata)
.ThenInclude(mvm => mvm.Directors)
.SelectOneAsync(mv => mv.Id, mv => mv.Id == musicVideoId);
foreach (var musicVideo in maybeMusicVideo)
{
foreach (var metadata in musicVideo.MusicVideoMetadata.HeadOrNone())
{
string artist = string.Empty;
foreach (ArtistMetadata artistMetadata in Optional(musicVideo.Artist?.ArtistMetadata).Flatten())
{
artist = artistMetadata.Title;
}
return new Dictionary<string, object>
{
["Resolution"] = resolution,
["Title"] = metadata.Title,
["Track"] = metadata.Track,
["Album"] = metadata.Album,
["Plot"] = metadata.Plot,
["ReleaseDate"] = metadata.ReleaseDate,
["Artists"] = (metadata.Artists ?? []).Map(a => a.Name).ToList(),
["Artist"] = artist,
["Studios"] = (metadata.Studios ?? []).Map(s => s.Name).ToList(),
["Directors"] = (metadata.Directors ?? []).Map(s => s.Name).ToList(),
["Duration"] = musicVideo.GetHeadVersion().Duration,
["StreamSeek"] = streamSeek
};
}
}
return Option<Dictionary<string, object>>.None;
}
}

28
ErsatzTV.Infrastructure/FFmpeg/MusicVideoCreditsGenerator.cs

@ -9,31 +9,23 @@ using Scriban; @@ -9,31 +9,23 @@ using Scriban;
namespace ErsatzTV.Infrastructure.FFmpeg;
public class MusicVideoCreditsGenerator : IMusicVideoCreditsGenerator
public class MusicVideoCreditsGenerator(ITempFilePool tempFilePool, ILogger<MusicVideoCreditsGenerator> logger)
: IMusicVideoCreditsGenerator
{
private readonly ILogger<MusicVideoCreditsGenerator> _logger;
private readonly ITempFilePool _tempFilePool;
public MusicVideoCreditsGenerator(ITempFilePool tempFilePool, ILogger<MusicVideoCreditsGenerator> logger)
{
_tempFilePool = tempFilePool;
_logger = logger;
}
public async Task<Option<Subtitle>> GenerateCreditsSubtitle(MusicVideo musicVideo, FFmpegProfile ffmpegProfile)
{
const int HORIZONTAL_MARGIN_PERCENT = 3;
const int VERTICAL_MARGIN_PERCENT = 5;
const int horizontalMarginPercent = 3;
const int verticalMarginPercent = 5;
var fontSize = (int)Math.Round(ffmpegProfile.Resolution.Height / 20.0);
int leftMarginPercent = HORIZONTAL_MARGIN_PERCENT;
int rightMarginPercent = HORIZONTAL_MARGIN_PERCENT;
int leftMarginPercent = horizontalMarginPercent;
int rightMarginPercent = horizontalMarginPercent;
var leftMargin = (int)Math.Round(leftMarginPercent / 100.0 * ffmpegProfile.Resolution.Width);
var rightMargin = (int)Math.Round(rightMarginPercent / 100.0 * ffmpegProfile.Resolution.Width);
var verticalMargin =
(int)Math.Round(VERTICAL_MARGIN_PERCENT / 100.0 * ffmpegProfile.Resolution.Height);
(int)Math.Round(verticalMarginPercent / 100.0 * ffmpegProfile.Resolution.Height);
foreach (MusicVideoMetadata metadata in musicVideo.MusicVideoMetadata)
{
@ -60,7 +52,7 @@ public class MusicVideoCreditsGenerator : IMusicVideoCreditsGenerator @@ -60,7 +52,7 @@ public class MusicVideoCreditsGenerator : IMusicVideoCreditsGenerator
sb.Append(CultureInfo.InvariantCulture, $"\\N{metadata.Album}");
}
string subtitles = await new SubtitleBuilder(_tempFilePool)
string subtitles = await new SubtitleBuilder(tempFilePool)
.WithResolution(ffmpegProfile.Resolution)
.WithFontName("OPTIKabel-Heavy")
.WithFontSize(fontSize)
@ -127,7 +119,7 @@ public class MusicVideoCreditsGenerator : IMusicVideoCreditsGenerator @@ -127,7 +119,7 @@ public class MusicVideoCreditsGenerator : IMusicVideoCreditsGenerator
StreamSeek = await settings.StreamSeek.IfNoneAsync(TimeSpan.Zero)
});
string fileName = _tempFilePool.GetNextTempFile(TempFileCategory.Subtitle);
string fileName = tempFilePool.GetNextTempFile(TempFileCategory.Subtitle);
await File.WriteAllTextAsync(fileName, result);
return new Subtitle
{
@ -143,7 +135,7 @@ public class MusicVideoCreditsGenerator : IMusicVideoCreditsGenerator @@ -143,7 +135,7 @@ public class MusicVideoCreditsGenerator : IMusicVideoCreditsGenerator
}
catch (Exception ex)
{
_logger.LogError(ex, "Error generating music video credits from template {Template}", templateFileName);
logger.LogError(ex, "Error generating music video credits from template {Template}", templateFileName);
}
return None;

28
ErsatzTV.Infrastructure/Streaming/GraphicsEngine.cs

@ -1,5 +1,7 @@ @@ -1,5 +1,7 @@
using System.IO.Pipelines;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Streaming;
using Microsoft.Extensions.Logging;
using SixLabors.ImageSharp;
@ -8,7 +10,7 @@ using SixLabors.ImageSharp.Processing; @@ -8,7 +10,7 @@ using SixLabors.ImageSharp.Processing;
namespace ErsatzTV.Infrastructure.Streaming;
public class GraphicsEngine(ILogger<GraphicsEngine> logger) : IGraphicsEngine
public class GraphicsEngine(ITemplateDataRepository templateDataRepository, ILogger<GraphicsEngine> logger) : IGraphicsEngine
{
public async Task Run(GraphicsEngineContext context, PipeWriter pipeWriter, CancellationToken cancellationToken)
{
@ -28,7 +30,29 @@ public class GraphicsEngine(ILogger<GraphicsEngine> logger) : IGraphicsEngine @@ -28,7 +30,29 @@ public class GraphicsEngine(ILogger<GraphicsEngine> logger) : IGraphicsEngine
break;
case TextElementContext textElementContext:
elements.Add(new TextElement(textElementContext.TextElement, textElementContext.Variables, logger));
var variables = new Dictionary<string, object>();
foreach (var variable in textElementContext.Variables)
{
variables.Add(variable.Key, variable.Value);
}
if (context.MediaItem is MusicVideo musicVideo)
{
var maybeTemplateData = await templateDataRepository.GetMusicVideoTemplateData(
context.FrameSize,
context.Seek,
musicVideo.Id);
foreach (var templateData in maybeTemplateData)
{
foreach (var variable in templateData)
{
variables.Add(variable.Key, variable.Value);
}
}
}
elements.Add(new TextElement(textElementContext.TextElement, variables, logger));
break;
}
}

9
ErsatzTV.Infrastructure/Streaming/TextElement.cs

@ -12,7 +12,8 @@ using Image=SixLabors.ImageSharp.Image; @@ -12,7 +12,8 @@ using Image=SixLabors.ImageSharp.Image;
namespace ErsatzTV.Infrastructure.Streaming;
public class TextElement(TextGraphicsElement textElement, Dictionary<string, string> variables, ILogger logger) : IGraphicsElement, IDisposable
public class TextElement(TextGraphicsElement textElement, Dictionary<string, object> variables, ILogger logger)
: IGraphicsElement, IDisposable
{
private Option<Expression> _maybeOpacityExpression;
private float _opacity;
@ -42,7 +43,8 @@ public class TextElement(TextGraphicsElement textElement, Dictionary<string, str @@ -42,7 +43,8 @@ public class TextElement(TextGraphicsElement textElement, Dictionary<string, str
string textToRender = await Template.Parse(textElement.Text).RenderAsync(variables);
var font = GraphicsEngineFonts.GetFont(textElement.FontFamily, textElement.FontSize ?? 48, FontStyle.Regular);
var font = GraphicsEngineFonts.GetFont(textElement.FontFamily, textElement.FontSize ?? 48,
FontStyle.Regular);
var fontColor = Color.White;
if (Color.TryParse(textElement.FontColor, out Color parsedColor) ||
Color.TryParseHex(textElement.FontColor, out parsedColor))
@ -67,7 +69,8 @@ public class TextElement(TextGraphicsElement textElement, Dictionary<string, str @@ -67,7 +69,8 @@ public class TextElement(TextGraphicsElement textElement, Dictionary<string, str
_image = new Image<Rgba32>((int)Math.Ceiling(textBounds.Width), (int)Math.Ceiling(textBounds.Height));
_image.Mutate(ctx => ctx.DrawText(textOptions, textToRender, fontColor));
int horizontalMargin = (int)Math.Round((textElement.HorizontalMarginPercent ?? 0) / 100.0 * frameSize.Width);
int horizontalMargin =
(int)Math.Round((textElement.HorizontalMarginPercent ?? 0) / 100.0 * frameSize.Width);
int verticalMargin = (int)Math.Round((textElement.VerticalMarginPercent ?? 0) / 100.0 * frameSize.Height);
_location = WatermarkElement.CalculatePosition(

1
ErsatzTV/Startup.cs

@ -720,6 +720,7 @@ public class Startup @@ -720,6 +720,7 @@ public class Startup
services.AddScoped<IChannelLogoGenerator, ChannelLogoGenerator>();
services.AddScoped<IGraphicsEngine, GraphicsEngine>();
services.AddScoped<IGraphicsElementRepository, GraphicsElementRepository>();
services.AddScoped<ITemplateDataRepository, TemplateDataRepository>();
services.AddScoped<IFFmpegProcessService, FFmpegLibraryProcessService>();
services.AddScoped<IPipelineBuilderFactory, PipelineBuilderFactory>();

Loading…
Cancel
Save