Browse Source

add graphics elements to decos (#2413)

* add deco graphics elements, selector and tests

* add migrations

* edit deco graphics elements from ui

* update changelog
pull/2414/head
Jason Dove 8 months ago committed by GitHub
parent
commit
bc721755f5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 3
      ErsatzTV.Application/Scheduling/Commands/CreateDecoHandler.cs
  3. 3
      ErsatzTV.Application/Scheduling/Commands/UpdateDeco.cs
  4. 32
      ErsatzTV.Application/Scheduling/Commands/UpdateDecoHandler.cs
  5. 4
      ErsatzTV.Application/Scheduling/DecoViewModel.cs
  6. 3
      ErsatzTV.Application/Scheduling/Mapper.cs
  7. 2
      ErsatzTV.Application/Scheduling/Queries/GetDecoByIdHandler.cs
  8. 3
      ErsatzTV.Application/Scheduling/Queries/GetDecoByPlayoutIdHandler.cs
  9. 2
      ErsatzTV.Application/Scheduling/Queries/GetDecosByDecoGroupIdHandler.cs
  10. 24
      ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs
  11. 374
      ErsatzTV.Core.Tests/FFmpeg/GraphicsElementSelectorTests.cs
  12. 11
      ErsatzTV.Core/Domain/DecoGraphicsElement.cs
  13. 7
      ErsatzTV.Core/Domain/GraphicsElement.cs
  14. 6
      ErsatzTV.Core/Domain/Scheduling/Deco.cs
  15. 140
      ErsatzTV.Core/FFmpeg/GraphicsElementSelector.cs
  16. 11
      ErsatzTV.Core/Interfaces/FFmpeg/IGraphicsElementSelector.cs
  17. 6483
      ErsatzTV.Infrastructure.MySql/Migrations/20250913143555_Add_DecoGraphicsElements.Designer.cs
  18. 73
      ErsatzTV.Infrastructure.MySql/Migrations/20250913143555_Add_DecoGraphicsElements.cs
  19. 44
      ErsatzTV.Infrastructure.MySql/Migrations/TvContextModelSnapshot.cs
  20. 6316
      ErsatzTV.Infrastructure.Sqlite/Migrations/20250913143705_Add_DecoGraphicsElements.Designer.cs
  21. 72
      ErsatzTV.Infrastructure.Sqlite/Migrations/20250913143705_Add_DecoGraphicsElements.cs
  22. 44
      ErsatzTV.Infrastructure.Sqlite/Migrations/TvContextModelSnapshot.cs
  23. 13
      ErsatzTV.Infrastructure/Data/Configurations/Scheduling/DecoConfiguration.cs
  24. 48
      ErsatzTV/Pages/DecoEditor.razor
  25. 1
      ErsatzTV/Startup.cs
  26. 5
      ErsatzTV/ViewModels/DecoEditViewModel.cs
  27. 4
      ErsatzTV/wwwroot/css/site.css

1
CHANGELOG.md

@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased]
### Added
- Classic schedules: allow selecting multiple graphics elements on schedule items
- Block schedules: allow selecting multiple graphics elements on decos
- Add channel `Playout Source` setting
- `Generated`: default/existing behavior where channel must have its own playout
- `Mirror`: channel will play content from the specified `Mirror Source Channel`'s playout

3
ErsatzTV.Application/Scheduling/Commands/CreateDecoHandler.cs

@ -30,7 +30,8 @@ public class CreateDecoHandler(IDbContextFactory<TvContext> dbContextFactory) @@ -30,7 +30,8 @@ public class CreateDecoHandler(IDbContextFactory<TvContext> dbContextFactory)
{
DecoGroupId = request.DecoGroupId,
Name = name,
DecoWatermarks = []
DecoWatermarks = [],
DecoGraphicsElements = []
});
private static async Task<Validation<BaseError, string>> ValidateDecoName(

3
ErsatzTV.Application/Scheduling/Commands/UpdateDeco.cs

@ -11,6 +11,9 @@ public record UpdateDeco( @@ -11,6 +11,9 @@ public record UpdateDeco(
DecoMode WatermarkMode,
List<int> WatermarkIds,
bool UseWatermarkDuringFiller,
DecoMode GraphicsElementsMode,
List<int> GraphicsElementIds,
bool UseGraphicsElementsDuringFiller,
DecoMode DefaultFillerMode,
ProgramScheduleItemCollectionType DefaultFillerCollectionType,
int? DefaultFillerCollectionId,

32
ErsatzTV.Application/Scheduling/Commands/UpdateDecoHandler.cs

@ -25,9 +25,8 @@ public class UpdateDecoHandler(IDbContextFactory<TvContext> dbContextFactory) @@ -25,9 +25,8 @@ public class UpdateDecoHandler(IDbContextFactory<TvContext> dbContextFactory)
{
existing.Name = request.Name;
bool hasWatermark = request.WatermarkMode is (DecoMode.Override or DecoMode.Merge);
// watermark
bool hasWatermark = request.WatermarkMode is (DecoMode.Override or DecoMode.Merge);
existing.WatermarkMode = request.WatermarkMode;
existing.UseWatermarkDuringFiller = hasWatermark && request.UseWatermarkDuringFiller;
@ -54,6 +53,34 @@ public class UpdateDecoHandler(IDbContextFactory<TvContext> dbContextFactory) @@ -54,6 +53,34 @@ public class UpdateDecoHandler(IDbContextFactory<TvContext> dbContextFactory)
existing.DecoWatermarks.Clear();
}
// graphics elements
bool hasGraphicsElements = request.GraphicsElementsMode is (DecoMode.Override or DecoMode.Merge);
existing.GraphicsElementsMode = request.GraphicsElementsMode;
existing.UseGraphicsElementsDuringFiller = hasGraphicsElements && request.UseGraphicsElementsDuringFiller;
if (hasGraphicsElements)
{
// this is different than schedule item/playout item because we have to merge graphics element ids
IEnumerable<int> toAdd =
request.GraphicsElementIds.Where(id => existing.DecoGraphicsElements.All(ge => ge.GraphicsElementId != id));
IEnumerable<DecoGraphicsElement> toRemove =
existing.DecoGraphicsElements.Where(ge => !request.GraphicsElementIds.Contains(ge.GraphicsElementId));
existing.DecoGraphicsElements.RemoveAll(toRemove.Contains);
foreach (int graphicsElementId in toAdd)
{
existing.DecoGraphicsElements.Add(
new DecoGraphicsElement
{
DecoId = existing.Id,
GraphicsElementId = graphicsElementId
});
}
}
else
{
existing.DecoGraphicsElements.Clear();
}
// default filler
existing.DefaultFillerMode = request.DefaultFillerMode;
existing.DefaultFillerCollectionType = request.DefaultFillerCollectionType;
@ -126,6 +153,7 @@ public class UpdateDecoHandler(IDbContextFactory<TvContext> dbContextFactory) @@ -126,6 +153,7 @@ public class UpdateDecoHandler(IDbContextFactory<TvContext> dbContextFactory)
CancellationToken cancellationToken) =>
dbContext.Decos
.Include(d => d.DecoWatermarks)
.Include(d => d.DecoGraphicsElements)
.SelectOneAsync(d => d.Id, d => d.Id == request.DecoId, cancellationToken)
.Map(o => o.ToValidation<BaseError>("Deco does not exist"));

4
ErsatzTV.Application/Scheduling/DecoViewModel.cs

@ -1,3 +1,4 @@ @@ -1,3 +1,4 @@
using ErsatzTV.Application.Graphics;
using ErsatzTV.Application.Watermarks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Scheduling;
@ -12,6 +13,9 @@ public record DecoViewModel( @@ -12,6 +13,9 @@ public record DecoViewModel(
DecoMode WatermarkMode,
List<WatermarkViewModel> Watermarks,
bool UseWatermarkDuringFiller,
DecoMode GraphicsElementsMode,
List<GraphicsElementViewModel> GraphicsElements,
bool UseGraphicsElementsDuringFiller,
DecoMode DefaultFillerMode,
ProgramScheduleItemCollectionType DefaultFillerCollectionType,
int? DefaultFillerCollectionId,

3
ErsatzTV.Application/Scheduling/Mapper.cs

@ -91,6 +91,9 @@ internal static class Mapper @@ -91,6 +91,9 @@ internal static class Mapper
deco.WatermarkMode,
deco.DecoWatermarks.Map(wm => Watermarks.Mapper.ProjectToViewModel(wm.Watermark)).ToList(),
deco.UseWatermarkDuringFiller,
deco.GraphicsElementsMode,
deco.DecoGraphicsElements.Map(ge => Graphics.Mapper.ProjectToViewModel(ge.GraphicsElement)).ToList(),
deco.UseGraphicsElementsDuringFiller,
deco.DefaultFillerMode,
deco.DefaultFillerCollectionType,
deco.DefaultFillerCollectionId,

2
ErsatzTV.Application/Scheduling/Queries/GetDecoByIdHandler.cs

@ -15,6 +15,8 @@ public class GetDecoByIdHandler(IDbContextFactory<TvContext> dbContextFactory) @@ -15,6 +15,8 @@ public class GetDecoByIdHandler(IDbContextFactory<TvContext> dbContextFactory)
.Include(d => d.DecoGroup)
.Include(d => d.DecoWatermarks)
.ThenInclude(d => d.Watermark)
.Include(d => d.DecoGraphicsElements)
.ThenInclude(d => d.GraphicsElement)
.SelectOneAsync(b => b.Id, b => b.Id == request.DecoId, cancellationToken)
.MapT(Mapper.ProjectToViewModel);
}

3
ErsatzTV.Application/Scheduling/Queries/GetDecoByPlayoutIdHandler.cs

@ -17,6 +17,9 @@ public class GetDecoByPlayoutIdHandler(IDbContextFactory<TvContext> dbContextFac @@ -17,6 +17,9 @@ public class GetDecoByPlayoutIdHandler(IDbContextFactory<TvContext> dbContextFac
.Include(p => p.Deco)
.ThenInclude(d => d.DecoWatermarks)
.ThenInclude(d => d.Watermark)
.Include(p => p.Deco)
.ThenInclude(d => d.DecoGraphicsElements)
.ThenInclude(d => d.GraphicsElement)
.SelectOneAsync(p => p.Id, p => p.Id == request.PlayoutId && p.DecoId != null, cancellationToken)
.MapT(p => Mapper.ProjectToViewModel(p.Deco));
}

2
ErsatzTV.Application/Scheduling/Queries/GetDecosByDecoGroupIdHandler.cs

@ -16,6 +16,8 @@ public class GetDecosByDecoGroupIdHandler(IDbContextFactory<TvContext> dbContext @@ -16,6 +16,8 @@ public class GetDecosByDecoGroupIdHandler(IDbContextFactory<TvContext> dbContext
.Include(d => d.DecoGroup)
.Include(d => d.DecoWatermarks)
.ThenInclude(d => d.Watermark)
.Include(d => d.DecoGraphicsElements)
.ThenInclude(d => d.GraphicsElement)
.Filter(b => b.DecoGroupId == request.DecoGroupId)
.ToListAsync(cancellationToken);

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

@ -37,6 +37,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler< @@ -37,6 +37,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
private readonly IMediaCollectionRepository _mediaCollectionRepository;
private readonly IMusicVideoCreditsGenerator _musicVideoCreditsGenerator;
private readonly IWatermarkSelector _watermarkSelector;
private readonly IGraphicsElementSelector _graphicsElementSelector;
private readonly IDecoSelector _decoSelector;
private readonly IPlexPathReplacementService _plexPathReplacementService;
private readonly ISongVideoGenerator _songVideoGenerator;
@ -56,6 +57,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler< @@ -56,6 +57,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
ISongVideoGenerator songVideoGenerator,
IMusicVideoCreditsGenerator musicVideoCreditsGenerator,
IWatermarkSelector watermarkSelector,
IGraphicsElementSelector graphicsElementSelector,
IDecoSelector decoSelector,
ILogger<GetPlayoutItemProcessByChannelNumberHandler> logger)
: base(dbContextFactory)
@ -72,6 +74,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler< @@ -72,6 +74,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
_songVideoGenerator = songVideoGenerator;
_musicVideoCreditsGenerator = musicVideoCreditsGenerator;
_watermarkSelector = watermarkSelector;
_graphicsElementSelector = graphicsElementSelector;
_decoSelector = decoSelector;
_logger = logger;
}
@ -94,6 +97,10 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler< @@ -94,6 +97,10 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
.ThenInclude(p => p.Deco)
.ThenInclude(d => d.DecoWatermarks)
.ThenInclude(d => d.Watermark)
.Include(i => i.Playout)
.ThenInclude(p => p.Deco)
.ThenInclude(d => d.DecoGraphicsElements)
.ThenInclude(d => d.GraphicsElement)
// get graphics elements
.Include(i => i.PlayoutItemGraphicsElements)
@ -107,6 +114,13 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler< @@ -107,6 +114,13 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
.ThenInclude(i => i.Deco)
.ThenInclude(d => d.DecoWatermarks)
.ThenInclude(d => d.Watermark)
.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 as Episode).EpisodeMetadata)
.ThenInclude(em => em.Subtitles)
@ -208,6 +222,9 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler< @@ -208,6 +222,9 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
.Include(p => p.Deco)
.ThenInclude(d => d.DecoWatermarks)
.ThenInclude(d => d.Watermark)
.Include(p => p.Deco)
.ThenInclude(d => d.DecoGraphicsElements)
.ThenInclude(d => d.GraphicsElement)
// get playout templates (and deco templates/decos)
.Include(p => p.Templates)
@ -306,6 +323,11 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler< @@ -306,6 +323,11 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
}
}
List<PlayoutItemGraphicsElement> graphicsElements = _graphicsElementSelector.SelectGraphicsElements(
channel,
playoutItemWithPath.PlayoutItem,
now);
if (playoutItemWithPath.PlayoutItem.MediaItem is Image)
{
audioPath = string.Empty;
@ -349,7 +371,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler< @@ -349,7 +371,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
finish,
effectiveNow,
watermarks,
playoutItemWithPath.PlayoutItem.PlayoutItemGraphicsElements,
graphicsElements,
channel.FFmpegProfile.VaapiDisplay,
channel.FFmpegProfile.VaapiDriver,
channel.FFmpegProfile.VaapiDevice,

374
ErsatzTV.Core.Tests/FFmpeg/GraphicsElementSelectorTests.cs

@ -0,0 +1,374 @@ @@ -0,0 +1,374 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Filler;
using ErsatzTV.Core.Domain.Scheduling;
using ErsatzTV.Core.FFmpeg;
using Microsoft.Extensions.Logging;
using NUnit.Framework;
using Serilog;
using Shouldly;
namespace ErsatzTV.Core.Tests.FFmpeg;
[TestFixture]
public class GraphicsElementSelectorTests
{
private static readonly GraphicsElementSelector GraphicsElementSelector;
private static readonly DateTimeOffset Now = new(2025, 08, 17, 12, 0, 0, TimeSpan.FromHours(-5));
private static readonly GraphicsElement GraphicsElementTemplateDeco;
private static readonly GraphicsElement GraphicsElementDefaultDeco;
private static readonly GraphicsElement GraphicsElementPlayoutItem;
private static readonly Channel Channel;
private static readonly Channel ChannelHlsDirect;
private static readonly PlayoutItem PlayoutItemWithNoGraphics;
private static readonly PlayoutItem PlayoutItemWithGraphics;
private static readonly PlayoutItem PlayoutItemWithGraphicsAsFiller;
private static readonly PlayoutItem TemplateDecoInherit;
private static readonly PlayoutItem TemplateDecoDisable;
private static readonly PlayoutItem TemplateDecoOverride;
private static readonly PlayoutItem TemplateDecoMerge;
private static readonly PlayoutItem TemplateDecoMergeFillerDisabled;
private static readonly PlayoutItem DefaultDecoInherit;
private static readonly PlayoutItem DefaultDecoDisable;
private static readonly PlayoutItem DefaultDecoOverride;
private static readonly PlayoutItem DefaultDecoMerge;
private static readonly PlayoutItem TemplateDecoInheritDefaultDecoMerge;
private static readonly PlayoutItem TemplateDecoMergeDefaultDecoInherit;
private static readonly PlayoutItem TemplateDecoMergeDefaultDecoMerge;
private static readonly PlayoutItem TemplateDecoOverrideDefaultDecoMerge;
static GraphicsElementSelectorTests()
{
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.Console()
.CreateLogger();
var loggerFactory = new LoggerFactory().AddSerilog(Log.Logger);
GraphicsElementSelector = new GraphicsElementSelector(
new DecoSelector(loggerFactory.CreateLogger<DecoSelector>()),
loggerFactory.CreateLogger<GraphicsElementSelector>());
GraphicsElementTemplateDeco = new GraphicsElement { Id = 1, Path = "Template Deco GE" };
GraphicsElementDefaultDeco = new GraphicsElement { Id = 2, Path = "Default Deco GE" };
GraphicsElementPlayoutItem = new GraphicsElement { Id = 3, Path = "Playout Item GE" };
Channel = new Channel(Guid.NewGuid());
ChannelHlsDirect = new Channel(Guid.NewGuid()) { StreamingMode = StreamingMode.HttpLiveStreamingDirect };
PlayoutItemWithNoGraphics = new PlayoutItem { PlayoutItemGraphicsElements = [] };
PlayoutItemWithGraphics = new PlayoutItem
{
PlayoutItemGraphicsElements =
[
new PlayoutItemGraphicsElement { GraphicsElement = GraphicsElementPlayoutItem }
]
};
PlayoutItemWithGraphicsAsFiller = new PlayoutItem
{
PlayoutItemGraphicsElements =
[
new PlayoutItemGraphicsElement { GraphicsElement = GraphicsElementPlayoutItem }
],
FillerKind = FillerKind.Tail
};
var decoWithInherit = new DecoTemplate
{ Items = [new DecoTemplateItem { Deco = new Deco { GraphicsElementsMode = DecoMode.Inherit } }] };
var decoWithDisable = new DecoTemplate
{ Items = [new DecoTemplateItem { Deco = new Deco { GraphicsElementsMode = DecoMode.Disable } }] };
var decoWithOverride = new DecoTemplate
{
Items =
[
new DecoTemplateItem
{
Deco = new Deco
{
GraphicsElementsMode = DecoMode.Override,
DecoGraphicsElements =
[new DecoGraphicsElement { GraphicsElement = GraphicsElementTemplateDeco }]
}
}
]
};
var decoWithMerge = new DecoTemplate
{
Items =
[
new DecoTemplateItem
{
Deco = new Deco
{
GraphicsElementsMode = DecoMode.Merge,
DecoGraphicsElements =
[new DecoGraphicsElement { GraphicsElement = GraphicsElementTemplateDeco }],
UseGraphicsElementsDuringFiller = true
}
}
]
};
var decoWithMergeFillerDisabled = new DecoTemplate
{
Items =
[
new DecoTemplateItem
{
Deco = new Deco
{
GraphicsElementsMode = DecoMode.Merge,
DecoGraphicsElements =
[new DecoGraphicsElement { GraphicsElement = GraphicsElementTemplateDeco }],
UseGraphicsElementsDuringFiller = false
}
}
]
};
var playoutWithTemplateDecoInherit = new Playout
{
Templates =
[
new PlayoutTemplate
{
DaysOfWeek = PlayoutTemplate.AllDaysOfWeek(),
DaysOfMonth = PlayoutTemplate.AllDaysOfMonth(),
MonthsOfYear = PlayoutTemplate.AllMonthsOfYear(),
DecoTemplate = decoWithInherit
}
]
};
var playoutWithTemplateDecoDisable = new Playout
{
Templates =
[
new PlayoutTemplate
{
DaysOfWeek = PlayoutTemplate.AllDaysOfWeek(),
DaysOfMonth = PlayoutTemplate.AllDaysOfMonth(),
MonthsOfYear = PlayoutTemplate.AllMonthsOfYear(),
DecoTemplate = decoWithDisable
}
]
};
var playoutWithTemplateDecoOverride = new Playout
{
Templates =
[
new PlayoutTemplate
{
DaysOfWeek = PlayoutTemplate.AllDaysOfWeek(),
DaysOfMonth = PlayoutTemplate.AllDaysOfMonth(),
MonthsOfYear = PlayoutTemplate.AllMonthsOfYear(),
DecoTemplate = decoWithOverride
}
]
};
var playoutWithTemplateDecoMerge = new Playout
{
Templates =
[
new PlayoutTemplate
{
DaysOfWeek = PlayoutTemplate.AllDaysOfWeek(),
DaysOfMonth = PlayoutTemplate.AllDaysOfMonth(),
MonthsOfYear = PlayoutTemplate.AllMonthsOfYear(),
DecoTemplate = decoWithMerge
}
]
};
var playoutWithTemplateDecoMergeFillerDisabled = new Playout
{
Templates =
[
new PlayoutTemplate
{
DaysOfWeek = PlayoutTemplate.AllDaysOfWeek(),
DaysOfMonth = PlayoutTemplate.AllDaysOfMonth(),
MonthsOfYear = PlayoutTemplate.AllMonthsOfYear(),
DecoTemplate = decoWithMergeFillerDisabled
}
]
};
TemplateDecoInherit = new PlayoutItem
{ Playout = playoutWithTemplateDecoInherit, PlayoutItemGraphicsElements = [] };
TemplateDecoDisable = new PlayoutItem
{ Playout = playoutWithTemplateDecoDisable, PlayoutItemGraphicsElements = [] };
TemplateDecoOverride = new PlayoutItem
{ Playout = playoutWithTemplateDecoOverride, PlayoutItemGraphicsElements = [] };
TemplateDecoMerge = new PlayoutItem
{ Playout = playoutWithTemplateDecoMerge, PlayoutItemGraphicsElements = [] };
TemplateDecoMergeFillerDisabled = new PlayoutItem
{ Playout = playoutWithTemplateDecoMergeFillerDisabled, FillerKind = FillerKind.Tail };
var playoutWithDecoInherit = new Playout
{ Deco = new Deco { GraphicsElementsMode = DecoMode.Inherit }, Templates = [] };
var playoutWithDecoDisable = new Playout
{ Deco = new Deco { GraphicsElementsMode = DecoMode.Disable }, Templates = [] };
var playoutWithDecoOverride = new Playout
{
Deco = new Deco
{
GraphicsElementsMode = DecoMode.Override,
DecoGraphicsElements = [new DecoGraphicsElement { GraphicsElement = GraphicsElementDefaultDeco }]
},
Templates = []
};
var playoutWithDecoMerge = new Playout
{
Deco = new Deco
{
GraphicsElementsMode = DecoMode.Merge,
DecoGraphicsElements = [new DecoGraphicsElement { GraphicsElement = GraphicsElementDefaultDeco }]
},
Templates = []
};
DefaultDecoInherit = new PlayoutItem { Playout = playoutWithDecoInherit, PlayoutItemGraphicsElements = [] };
DefaultDecoDisable = new PlayoutItem { Playout = playoutWithDecoDisable, PlayoutItemGraphicsElements = [] };
DefaultDecoOverride = new PlayoutItem { Playout = playoutWithDecoOverride, PlayoutItemGraphicsElements = [] };
DefaultDecoMerge = new PlayoutItem { Playout = playoutWithDecoMerge, PlayoutItemGraphicsElements = [] };
var playoutWithTemplateInheritDefaultMerge = new Playout
{
Templates =
[
new PlayoutTemplate
{
DaysOfWeek = PlayoutTemplate.AllDaysOfWeek(),
DaysOfMonth = PlayoutTemplate.AllDaysOfMonth(),
MonthsOfYear = PlayoutTemplate.AllMonthsOfYear(),
DecoTemplate = decoWithInherit
}
],
Deco = playoutWithDecoMerge.Deco
};
TemplateDecoInheritDefaultDecoMerge = new PlayoutItem
{ Playout = playoutWithTemplateInheritDefaultMerge, PlayoutItemGraphicsElements = [] };
var playoutWithTemplateMergeDefaultInherit = new Playout
{
Templates =
[
new PlayoutTemplate
{
DaysOfWeek = PlayoutTemplate.AllDaysOfWeek(),
DaysOfMonth = PlayoutTemplate.AllDaysOfMonth(),
MonthsOfYear = PlayoutTemplate.AllMonthsOfYear(),
DecoTemplate = decoWithMerge
}
],
Deco = playoutWithDecoInherit.Deco
};
TemplateDecoMergeDefaultDecoInherit = new PlayoutItem
{ Playout = playoutWithTemplateMergeDefaultInherit, PlayoutItemGraphicsElements = [] };
var playoutWithTemplateMergeDefaultMerge = new Playout
{
Templates =
[
new PlayoutTemplate
{
DaysOfWeek = PlayoutTemplate.AllDaysOfWeek(),
DaysOfMonth = PlayoutTemplate.AllDaysOfMonth(),
MonthsOfYear = PlayoutTemplate.AllMonthsOfYear(),
DecoTemplate = decoWithMerge
}
],
Deco = playoutWithDecoMerge.Deco
};
TemplateDecoMergeDefaultDecoMerge = new PlayoutItem
{ Playout = playoutWithTemplateMergeDefaultMerge, PlayoutItemGraphicsElements = [] };
var playoutWithTemplateOverrideDefaultMerge = new Playout
{
Templates =
[
new PlayoutTemplate
{
DaysOfWeek = PlayoutTemplate.AllDaysOfWeek(),
DaysOfMonth = PlayoutTemplate.AllDaysOfMonth(),
MonthsOfYear = PlayoutTemplate.AllMonthsOfYear(),
DecoTemplate = decoWithOverride
}
],
Deco = playoutWithDecoMerge.Deco
};
TemplateDecoOverrideDefaultDecoMerge = new PlayoutItem
{ Playout = playoutWithTemplateOverrideDefaultMerge, PlayoutItemGraphicsElements = [] };
}
private static IEnumerable<(Channel channel, PlayoutItem playoutItem, List<GraphicsElement> expected)>
SelectGraphicsElementsTestCases()
{
// HLS direct streaming mode disables graphics
yield return (ChannelHlsDirect, PlayoutItemWithGraphics, []);
// no graphics configured
yield return (Channel, PlayoutItemWithNoGraphics, []);
// only playout item graphics
yield return (Channel, PlayoutItemWithGraphics, [GraphicsElementPlayoutItem]);
// template deco disable
yield return (Channel, new PlayoutItem { Playout = TemplateDecoDisable.Playout, PlayoutItemGraphicsElements = PlayoutItemWithGraphics.PlayoutItemGraphicsElements }, []);
// template deco override
yield return (Channel, new PlayoutItem { Playout = TemplateDecoOverride.Playout, PlayoutItemGraphicsElements = PlayoutItemWithGraphics.PlayoutItemGraphicsElements }, [GraphicsElementTemplateDeco]);
// template deco merge
yield return (Channel, new PlayoutItem { Playout = TemplateDecoMerge.Playout, PlayoutItemGraphicsElements = PlayoutItemWithGraphics.PlayoutItemGraphicsElements }, [GraphicsElementTemplateDeco, GraphicsElementPlayoutItem]);
// template deco inherit, default deco disable
yield return (Channel, new PlayoutItem { Playout = new Playout { Templates = TemplateDecoInherit.Playout.Templates, Deco = DefaultDecoDisable.Playout.Deco }, PlayoutItemGraphicsElements = PlayoutItemWithGraphics.PlayoutItemGraphicsElements }, []);
// template deco inherit, default deco override
yield return (Channel, new PlayoutItem { Playout = new Playout { Templates = TemplateDecoInherit.Playout.Templates, Deco = DefaultDecoOverride.Playout.Deco }, PlayoutItemGraphicsElements = PlayoutItemWithGraphics.PlayoutItemGraphicsElements }, [GraphicsElementDefaultDeco]);
// template deco inherit, default deco merge
yield return (Channel, new PlayoutItem { Playout = new Playout { Templates = TemplateDecoInherit.Playout.Templates, Deco = DefaultDecoMerge.Playout.Deco }, PlayoutItemGraphicsElements = PlayoutItemWithGraphics.PlayoutItemGraphicsElements }, [GraphicsElementDefaultDeco, GraphicsElementPlayoutItem]);
// template deco merge, default deco merge
yield return (Channel, new PlayoutItem { Playout = TemplateDecoMergeDefaultDecoMerge.Playout, PlayoutItemGraphicsElements = PlayoutItemWithGraphics.PlayoutItemGraphicsElements }, [GraphicsElementTemplateDeco, GraphicsElementDefaultDeco, GraphicsElementPlayoutItem]);
// template deco merge, default deco inherit
yield return (Channel, new PlayoutItem { Playout = TemplateDecoMergeDefaultDecoInherit.Playout, PlayoutItemGraphicsElements = PlayoutItemWithGraphics.PlayoutItemGraphicsElements }, [GraphicsElementTemplateDeco, GraphicsElementPlayoutItem]);
// template deco override, default deco merge (template override should win)
yield return (Channel, new PlayoutItem { Playout = TemplateDecoOverrideDefaultDecoMerge.Playout, PlayoutItemGraphicsElements = PlayoutItemWithGraphics.PlayoutItemGraphicsElements }, [GraphicsElementTemplateDeco]);
// filler item with template deco merge, filler disabled
yield return (Channel, new PlayoutItem { Playout = TemplateDecoMergeFillerDisabled.Playout, FillerKind = FillerKind.Tail, PlayoutItemGraphicsElements = PlayoutItemWithGraphics.PlayoutItemGraphicsElements }, []);
// filler item with template deco merge, filler enabled
yield return (Channel, new PlayoutItem { Playout = TemplateDecoMerge.Playout, FillerKind = FillerKind.Tail, PlayoutItemGraphicsElements = PlayoutItemWithGraphics.PlayoutItemGraphicsElements }, [GraphicsElementTemplateDeco, GraphicsElementPlayoutItem]);
}
[TestCaseSource(nameof(SelectGraphicsElementsTestCases))]
public void Should_Select_Appropriate_Graphics_Elements(
(Channel channel, PlayoutItem playoutItem, List<GraphicsElement> expected) testCase)
{
List<PlayoutItemGraphicsElement> result = GraphicsElementSelector.SelectGraphicsElements(
testCase.channel,
testCase.playoutItem,
Now);
result.Map(pige => pige.GraphicsElement).ShouldBe(testCase.expected);
}
}

11
ErsatzTV.Core/Domain/DecoGraphicsElement.cs

@ -0,0 +1,11 @@ @@ -0,0 +1,11 @@
using ErsatzTV.Core.Domain.Scheduling;
namespace ErsatzTV.Core.Domain;
public class DecoGraphicsElement
{
public int DecoId { get; set; }
public Deco Deco { get; set; }
public int GraphicsElementId { get; set; }
public GraphicsElement GraphicsElement { get; set; }
}

7
ErsatzTV.Core/Domain/GraphicsElement.cs

@ -1,3 +1,5 @@ @@ -1,3 +1,5 @@
using ErsatzTV.Core.Domain.Scheduling;
namespace ErsatzTV.Core.Domain;
public class GraphicsElement
@ -9,4 +11,9 @@ public class GraphicsElement @@ -9,4 +11,9 @@ public class GraphicsElement
public List<PlayoutItemGraphicsElement> PlayoutItemGraphicsElements { get; set; }
public List<ProgramScheduleItem> ProgramScheduleItems { get; set; }
public List<ProgramScheduleItemGraphicsElement> ProgramScheduleItemGraphicsElements { get; set; }
public List<Deco> Decos { get; set; }
public List<DecoGraphicsElement> DecoGraphicsElements { get; set; }
// for unit testing
public override string ToString() => Path;
}

6
ErsatzTV.Core/Domain/Scheduling/Deco.cs

@ -14,6 +14,12 @@ public class Deco @@ -14,6 +14,12 @@ public class Deco
public List<DecoWatermark> DecoWatermarks { get; set; }
public bool UseWatermarkDuringFiller { get; set; }
// graphics elements
public DecoMode GraphicsElementsMode { get; set; }
public List<GraphicsElement> GraphicsElements { get; set; }
public List<DecoGraphicsElement> DecoGraphicsElements { get; set; }
public bool UseGraphicsElementsDuringFiller { get; set; }
// default filler
public DecoMode DefaultFillerMode { get; set; }
public ProgramScheduleItemCollectionType DefaultFillerCollectionType { get; set; }

140
ErsatzTV.Core/FFmpeg/GraphicsElementSelector.cs

@ -0,0 +1,140 @@ @@ -0,0 +1,140 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Filler;
using ErsatzTV.Core.Domain.Scheduling;
using ErsatzTV.Core.Interfaces.FFmpeg;
using Microsoft.Extensions.Logging;
namespace ErsatzTV.Core.FFmpeg;
public class GraphicsElementSelector(IDecoSelector decoSelector, ILogger<GraphicsElementSelector> logger)
: IGraphicsElementSelector
{
public List<PlayoutItemGraphicsElement> SelectGraphicsElements(
Channel channel,
PlayoutItem playoutItem,
DateTimeOffset now)
{
logger.LogDebug("Checking for graphics elements at {Now}", now);
var result = new List<PlayoutItemGraphicsElement>();
if (channel.StreamingMode == StreamingMode.HttpLiveStreamingDirect)
{
return result;
}
// if (playoutItem.DisableWatermarks)
// {
// logger.LogDebug("Graphics elements are disabled by playout item");
// return result;
// }
DecoEntries decoEntries = decoSelector.GetDecoEntries(playoutItem.Playout, now);
// first, check deco template / active deco
foreach (Deco templateDeco in decoEntries.TemplateDeco)
{
var done = false;
switch (templateDeco.GraphicsElementsMode)
{
case DecoMode.Merge:
if (playoutItem.FillerKind is FillerKind.None || templateDeco.UseGraphicsElementsDuringFiller)
{
logger.LogDebug("Graphics elements will come from template deco (merge)");
result.AddRange(
templateDeco.DecoGraphicsElements.Map(dge => dge.GraphicsElement).Map(ge =>
new PlayoutItemGraphicsElement { PlayoutItem = playoutItem, GraphicsElement = ge }));
break;
}
logger.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)");
result.AddRange(
templateDeco.DecoGraphicsElements.Map(dge => dge.GraphicsElement).Map(ge =>
new PlayoutItemGraphicsElement { PlayoutItem = playoutItem, GraphicsElement = ge }));
done = true;
break;
}
logger.LogDebug("Graphics elements are disabled by template deco during filler");
result.Clear();
done = true;
break;
case DecoMode.Disable:
logger.LogDebug("Graphics elements are disabled by template deco");
done = true;
break;
case DecoMode.Inherit:
logger.LogDebug("Graphics elements will inherit from playout deco");
break;
}
if (done)
{
return result;
}
}
// second, check playout deco
foreach (Deco playoutDeco in decoEntries.PlayoutDeco)
{
var done = false;
switch (playoutDeco.GraphicsElementsMode)
{
case DecoMode.Merge:
if (playoutItem.FillerKind is FillerKind.None || playoutDeco.UseGraphicsElementsDuringFiller)
{
logger.LogDebug("Graphics elements will come from playout deco (merge)");
result.AddRange(
playoutDeco.DecoGraphicsElements.Map(dge => dge.GraphicsElement).Map(ge =>
new PlayoutItemGraphicsElement { PlayoutItem = playoutItem, GraphicsElement = ge }));
break;
}
logger.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)");
result.AddRange(
playoutDeco.DecoGraphicsElements.Map(dge => dge.GraphicsElement).Map(ge =>
new PlayoutItemGraphicsElement { PlayoutItem = playoutItem, GraphicsElement = ge }));
done = true;
break;
}
logger.LogDebug("Graphics elements are disabled by playout deco during filler");
result.Clear();
done = true;
break;
case DecoMode.Disable:
logger.LogDebug("Graphics elements are disabled by playout deco");
done = true;
break;
case DecoMode.Inherit:
logger.LogDebug("Graphics elements will inherit from channel and/or global setting");
break;
}
if (done)
{
return result;
}
}
result.AddRange(playoutItem.PlayoutItemGraphicsElements);
return result;
}
}

11
ErsatzTV.Core/Interfaces/FFmpeg/IGraphicsElementSelector.cs

@ -0,0 +1,11 @@ @@ -0,0 +1,11 @@
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Core.Interfaces.FFmpeg;
public interface IGraphicsElementSelector
{
List<PlayoutItemGraphicsElement> SelectGraphicsElements(
Channel channel,
PlayoutItem playoutItem,
DateTimeOffset now);
}

6483
ErsatzTV.Infrastructure.MySql/Migrations/20250913143555_Add_DecoGraphicsElements.Designer.cs generated

File diff suppressed because it is too large Load Diff

73
ErsatzTV.Infrastructure.MySql/Migrations/20250913143555_Add_DecoGraphicsElements.cs

@ -0,0 +1,73 @@ @@ -0,0 +1,73 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.MySql.Migrations
{
/// <inheritdoc />
public partial class Add_DecoGraphicsElements : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "GraphicsElementsMode",
table: "Deco",
type: "int",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<bool>(
name: "UseGraphicsElementsDuringFiller",
table: "Deco",
type: "tinyint(1)",
nullable: false,
defaultValue: false);
migrationBuilder.CreateTable(
name: "DecoGraphicsElement",
columns: table => new
{
DecoId = table.Column<int>(type: "int", nullable: false),
GraphicsElementId = table.Column<int>(type: "int", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_DecoGraphicsElement", x => new { x.DecoId, x.GraphicsElementId });
table.ForeignKey(
name: "FK_DecoGraphicsElement_Deco_DecoId",
column: x => x.DecoId,
principalTable: "Deco",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_DecoGraphicsElement_GraphicsElement_GraphicsElementId",
column: x => x.GraphicsElementId,
principalTable: "GraphicsElement",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateIndex(
name: "IX_DecoGraphicsElement_GraphicsElementId",
table: "DecoGraphicsElement",
column: "GraphicsElementId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "DecoGraphicsElement");
migrationBuilder.DropColumn(
name: "GraphicsElementsMode",
table: "Deco");
migrationBuilder.DropColumn(
name: "UseGraphicsElementsDuringFiller",
table: "Deco");
}
}
}

44
ErsatzTV.Infrastructure.MySql/Migrations/TvContextModelSnapshot.cs

@ -475,6 +475,21 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations @@ -475,6 +475,21 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
b.ToTable("ConfigElement", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.DecoGraphicsElement", b =>
{
b.Property<int>("DecoId")
.HasColumnType("int");
b.Property<int>("GraphicsElementId")
.HasColumnType("int");
b.HasKey("DecoId", "GraphicsElementId");
b.HasIndex("GraphicsElementId");
b.ToTable("DecoGraphicsElement");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.DecoWatermark", b =>
{
b.Property<int>("DecoId")
@ -2589,9 +2604,15 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations @@ -2589,9 +2604,15 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
b.Property<bool>("DefaultFillerTrimToFit")
.HasColumnType("tinyint(1)");
b.Property<int>("GraphicsElementsMode")
.HasColumnType("int");
b.Property<string>("Name")
.HasColumnType("varchar(255)");
b.Property<bool>("UseGraphicsElementsDuringFiller")
.HasColumnType("tinyint(1)");
b.Property<bool>("UseWatermarkDuringFiller")
.HasColumnType("tinyint(1)");
@ -4139,6 +4160,25 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations @@ -4139,6 +4160,25 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
b.Navigation("MediaItem");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.DecoGraphicsElement", b =>
{
b.HasOne("ErsatzTV.Core.Domain.Scheduling.Deco", "Deco")
.WithMany("DecoGraphicsElements")
.HasForeignKey("DecoId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ErsatzTV.Core.Domain.GraphicsElement", "GraphicsElement")
.WithMany("DecoGraphicsElements")
.HasForeignKey("GraphicsElementId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Deco");
b.Navigation("GraphicsElement");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.DecoWatermark", b =>
{
b.HasOne("ErsatzTV.Core.Domain.Scheduling.Deco", "Deco")
@ -6013,6 +6053,8 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations @@ -6013,6 +6053,8 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
modelBuilder.Entity("ErsatzTV.Core.Domain.GraphicsElement", b =>
{
b.Navigation("DecoGraphicsElements");
b.Navigation("PlayoutItemGraphicsElements");
b.Navigation("ProgramScheduleItemGraphicsElements");
@ -6230,6 +6272,8 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations @@ -6230,6 +6272,8 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.Deco", b =>
{
b.Navigation("DecoGraphicsElements");
b.Navigation("DecoWatermarks");
b.Navigation("Playouts");

6316
ErsatzTV.Infrastructure.Sqlite/Migrations/20250913143705_Add_DecoGraphicsElements.Designer.cs generated

File diff suppressed because it is too large Load Diff

72
ErsatzTV.Infrastructure.Sqlite/Migrations/20250913143705_Add_DecoGraphicsElements.cs

@ -0,0 +1,72 @@ @@ -0,0 +1,72 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.Sqlite.Migrations
{
/// <inheritdoc />
public partial class Add_DecoGraphicsElements : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "GraphicsElementsMode",
table: "Deco",
type: "INTEGER",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<bool>(
name: "UseGraphicsElementsDuringFiller",
table: "Deco",
type: "INTEGER",
nullable: false,
defaultValue: false);
migrationBuilder.CreateTable(
name: "DecoGraphicsElement",
columns: table => new
{
DecoId = table.Column<int>(type: "INTEGER", nullable: false),
GraphicsElementId = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_DecoGraphicsElement", x => new { x.DecoId, x.GraphicsElementId });
table.ForeignKey(
name: "FK_DecoGraphicsElement_Deco_DecoId",
column: x => x.DecoId,
principalTable: "Deco",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_DecoGraphicsElement_GraphicsElement_GraphicsElementId",
column: x => x.GraphicsElementId,
principalTable: "GraphicsElement",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_DecoGraphicsElement_GraphicsElementId",
table: "DecoGraphicsElement",
column: "GraphicsElementId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "DecoGraphicsElement");
migrationBuilder.DropColumn(
name: "GraphicsElementsMode",
table: "Deco");
migrationBuilder.DropColumn(
name: "UseGraphicsElementsDuringFiller",
table: "Deco");
}
}
}

44
ErsatzTV.Infrastructure.Sqlite/Migrations/TvContextModelSnapshot.cs

@ -456,6 +456,21 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -456,6 +456,21 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.ToTable("ConfigElement", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.DecoGraphicsElement", b =>
{
b.Property<int>("DecoId")
.HasColumnType("INTEGER");
b.Property<int>("GraphicsElementId")
.HasColumnType("INTEGER");
b.HasKey("DecoId", "GraphicsElementId");
b.HasIndex("GraphicsElementId");
b.ToTable("DecoGraphicsElement");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.DecoWatermark", b =>
{
b.Property<int>("DecoId")
@ -2468,9 +2483,15 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -2468,9 +2483,15 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.Property<bool>("DefaultFillerTrimToFit")
.HasColumnType("INTEGER");
b.Property<int>("GraphicsElementsMode")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<bool>("UseGraphicsElementsDuringFiller")
.HasColumnType("INTEGER");
b.Property<bool>("UseWatermarkDuringFiller")
.HasColumnType("INTEGER");
@ -3972,6 +3993,25 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -3972,6 +3993,25 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.Navigation("MediaItem");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.DecoGraphicsElement", b =>
{
b.HasOne("ErsatzTV.Core.Domain.Scheduling.Deco", "Deco")
.WithMany("DecoGraphicsElements")
.HasForeignKey("DecoId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ErsatzTV.Core.Domain.GraphicsElement", "GraphicsElement")
.WithMany("DecoGraphicsElements")
.HasForeignKey("GraphicsElementId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Deco");
b.Navigation("GraphicsElement");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.DecoWatermark", b =>
{
b.HasOne("ErsatzTV.Core.Domain.Scheduling.Deco", "Deco")
@ -5846,6 +5886,8 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -5846,6 +5886,8 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
modelBuilder.Entity("ErsatzTV.Core.Domain.GraphicsElement", b =>
{
b.Navigation("DecoGraphicsElements");
b.Navigation("PlayoutItemGraphicsElements");
b.Navigation("ProgramScheduleItemGraphicsElements");
@ -6063,6 +6105,8 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -6063,6 +6105,8 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.Deco", b =>
{
b.Navigation("DecoGraphicsElements");
b.Navigation("DecoWatermarks");
b.Navigation("Playouts");

13
ErsatzTV.Infrastructure/Data/Configurations/Scheduling/DecoConfiguration.cs

@ -56,5 +56,18 @@ public class DecoConfiguration : IEntityTypeConfiguration<Deco> @@ -56,5 +56,18 @@ public class DecoConfiguration : IEntityTypeConfiguration<Deco>
.HasForeignKey(ci => ci.DecoId)
.OnDelete(DeleteBehavior.Cascade),
j => j.HasKey(ci => new { ci.DecoId, ci.WatermarkId }));
builder.HasMany(c => c.GraphicsElements)
.WithMany(m => m.Decos)
.UsingEntity<DecoGraphicsElement>(
j => j.HasOne(ci => ci.GraphicsElement)
.WithMany(mi => mi.DecoGraphicsElements)
.HasForeignKey(ci => ci.GraphicsElementId)
.OnDelete(DeleteBehavior.Cascade),
j => j.HasOne(ci => ci.Deco)
.WithMany(c => c.DecoGraphicsElements)
.HasForeignKey(ci => ci.DecoId)
.OnDelete(DeleteBehavior.Cascade),
j => j.HasKey(ci => new { ci.DecoId, ci.GraphicsElementId }));
}
}

48
ErsatzTV/Pages/DecoEditor.razor

@ -1,5 +1,6 @@ @@ -1,5 +1,6 @@
@page "/decos/{Id:int}"
@using ErsatzTV.Application.Artists
@using ErsatzTV.Application.Graphics
@using ErsatzTV.Application.MediaCollections
@using ErsatzTV.Application.MediaItems
@using ErsatzTV.Application.Scheduling
@ -72,6 +73,44 @@ @@ -72,6 +73,44 @@
@bind-Value="_deco.UseWatermarkDuringFiller"
Dense="true"/>
</MudStack>
<MudText Typo="Typo.h5" Class="mt-10 mb-2">Graphics Elements</MudText>
<MudDivider Class="mb-6"/>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Graphics Elements Mode</MudText>
</div>
<MudSelect @bind-Value="_deco.GraphicsElementsMode" For="@(() => _deco.GraphicsElementsMode)">
<MudSelectItem Value="DecoMode.Inherit">Inherit</MudSelectItem>
<MudSelectItem Value="DecoMode.Disable">Disable</MudSelectItem>
<MudSelectItem Value="DecoMode.Override">Replace</MudSelectItem>
<MudSelectItem Value="DecoMode.Merge">Merge</MudSelectItem>
</MudSelect>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Graphics Elements</MudText>
</div>
<MudSelect T="GraphicsElementViewModel"
@bind-SelectedValues="_deco.GraphicsElements"
Disabled="@(_deco.GraphicsElementsMode is not (DecoMode.Override or DecoMode.Merge))"
ToStringFunc="@(ge => ge?.Name)"
Clearable="true"
MultiSelection="true">
@foreach (GraphicsElementViewModel graphicsElement in _graphicsElements)
{
<MudSelectItem Value="@graphicsElement">@graphicsElement.Name</MudSelectItem>
}
</MudSelect>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Use Graphics Elements During Filler</MudText>
</div>
<MudCheckBox T="bool"
Disabled="@(_deco.GraphicsElementsMode != DecoMode.Override)"
@bind-Value="_deco.UseGraphicsElementsDuringFiller"
Dense="true"/>
</MudStack>
<MudText Typo="Typo.h5" Class="mt-10 mb-2">Default Filler</MudText>
<MudDivider Class="mb-6"/>
<MudText Typo="Typo.body2" Class="mb-6">After all blocks have been scheduled, a second pass will be made to fill unscheduled time using random items from this collection.</MudText>
@ -344,6 +383,7 @@ @@ -344,6 +383,7 @@
private DecoEditViewModel _deco = new();
private List<WatermarkViewModel> _watermarks = [];
private List<GraphicsElementViewModel> _graphicsElements = [];
private List<MediaCollectionViewModel> _mediaCollections = [];
private List<MultiCollectionViewModel> _multiCollections = [];
@ -369,6 +409,7 @@ @@ -369,6 +409,7 @@
try
{
_watermarks = await Mediator.Send(new GetAllWatermarks(), token);
_graphicsElements = await Mediator.Send(new GetAllGraphicsElements(), token);
_mediaCollections = await Mediator.Send(new GetAllCollections(), token)
.Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList());
@ -401,6 +442,10 @@ @@ -401,6 +442,10 @@
Watermarks = deco.Watermarks,
UseWatermarkDuringFiller = deco.UseWatermarkDuringFiller,
GraphicsElementsMode = deco.GraphicsElementsMode,
GraphicsElements = deco.GraphicsElements,
UseGraphicsElementsDuringFiller = deco.UseGraphicsElementsDuringFiller,
DefaultFillerMode = deco.DefaultFillerMode,
DefaultFillerCollectionType = deco.DefaultFillerCollectionType,
DefaultFillerCollection = deco.DefaultFillerCollectionId.HasValue
@ -451,6 +496,9 @@ @@ -451,6 +496,9 @@
_deco.WatermarkMode,
_deco.Watermarks.Map(wm => wm.Id).ToList(),
_deco.UseWatermarkDuringFiller,
_deco.GraphicsElementsMode,
_deco.GraphicsElements.Map(ge => ge.Id).ToList(),
_deco.UseGraphicsElementsDuringFiller,
_deco.DefaultFillerMode,
_deco.DefaultFillerCollectionType,
_deco.DefaultFillerCollection?.Id,

1
ErsatzTV/Startup.cs

@ -787,6 +787,7 @@ public class Startup @@ -787,6 +787,7 @@ public class Startup
services.AddScoped<TemplateFunctions>();
services.AddScoped<IDecoSelector, DecoSelector>();
services.AddScoped<IWatermarkSelector, WatermarkSelector>();
services.AddScoped<IGraphicsElementSelector, GraphicsElementSelector>();
services.AddScoped<IFFmpegProcessService, FFmpegLibraryProcessService>();
services.AddScoped<IPipelineBuilderFactory, PipelineBuilderFactory>();

5
ErsatzTV/ViewModels/DecoEditViewModel.cs

@ -1,3 +1,4 @@ @@ -1,3 +1,4 @@
using ErsatzTV.Application.Graphics;
using ErsatzTV.Application.MediaCollections;
using ErsatzTV.Application.MediaItems;
using ErsatzTV.Application.Watermarks;
@ -15,6 +16,10 @@ public class DecoEditViewModel @@ -15,6 +16,10 @@ public class DecoEditViewModel
public IEnumerable<WatermarkViewModel> Watermarks { get; set; }
public bool UseWatermarkDuringFiller { get; set; }
public DecoMode GraphicsElementsMode { get; set; }
public IEnumerable<GraphicsElementViewModel> GraphicsElements { get; set; }
public bool UseGraphicsElementsDuringFiller { get; set; }
public DecoMode DefaultFillerMode { get; set; }
public ProgramScheduleItemCollectionType DefaultFillerCollectionType { get; set; }
public MediaCollectionViewModel DefaultFillerCollection { get; set; }

4
ErsatzTV/wwwroot/css/site.css

@ -202,7 +202,7 @@ div.ersatztv-light { @@ -202,7 +202,7 @@ div.ersatztv-light {
background-color: #002e00;
}
.playout-filler-preroll, .playout-filler-midroll, .playout-filler-postroll {
.playout-filler-preroll, .playout-filler-midroll, .playout-filler-postroll, .playout-filler-decodefault {
background-color: #27272f
}
@ -216,4 +216,4 @@ div.ersatztv-light { @@ -216,4 +216,4 @@ div.ersatztv-light {
.playout-filler-unscheduled {
background-color: #52040d;
}
}

Loading…
Cancel
Save