diff --git a/CHANGELOG.md b/CHANGELOG.md index bd81e3776..2f0085ddd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Fix watermarks and graphics elements on primary content split by mid-roll filler - Fix watermarks and graphics elements when `Scaling Behavior` is `Crop` - Fix hardware acceleration health check message on mobile +- Fix deco selection logic ### Changed - **BREAKING CHANGE**: change how `Scripted Schedule` system works diff --git a/ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs b/ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs index 459db121a..51fb9792b 100644 --- a/ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs +++ b/ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs @@ -37,6 +37,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler< private readonly IMediaCollectionRepository _mediaCollectionRepository; private readonly IMusicVideoCreditsGenerator _musicVideoCreditsGenerator; private readonly IWatermarkSelector _watermarkSelector; + private readonly IDecoSelector _decoSelector; private readonly IPlexPathReplacementService _plexPathReplacementService; private readonly ISongVideoGenerator _songVideoGenerator; private readonly ITelevisionRepository _televisionRepository; @@ -55,6 +56,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler< ISongVideoGenerator songVideoGenerator, IMusicVideoCreditsGenerator musicVideoCreditsGenerator, IWatermarkSelector watermarkSelector, + IDecoSelector decoSelector, ILogger logger) : base(dbContextFactory) { @@ -70,6 +72,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler< _songVideoGenerator = songVideoGenerator; _musicVideoCreditsGenerator = musicVideoCreditsGenerator; _watermarkSelector = watermarkSelector; + _decoSelector = decoSelector; _logger = logger; } @@ -768,7 +771,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler< private DeadAirFallbackResult GetDecoDeadAirFallback(Playout playout, DateTimeOffset now) { - DecoEntries decoEntries = DecoSelector.GetDecoEntries(playout, now); + DecoEntries decoEntries = _decoSelector.GetDecoEntries(playout, now); // first, check deco template / active deco foreach (Deco templateDeco in decoEntries.TemplateDeco) diff --git a/ErsatzTV.Core.Tests/FFmpeg/DecoSelectorTests.cs b/ErsatzTV.Core.Tests/FFmpeg/DecoSelectorTests.cs new file mode 100644 index 000000000..eba251b4b --- /dev/null +++ b/ErsatzTV.Core.Tests/FFmpeg/DecoSelectorTests.cs @@ -0,0 +1,126 @@ +using ErsatzTV.Core.Domain; +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 DecoSelectorTests +{ + private static readonly DecoSelector DecoSelector; + + static DecoSelectorTests() + { + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Debug() + .WriteTo.Console() + .CreateLogger(); + + var loggerFactory = new LoggerFactory().AddSerilog(Log.Logger); + + DecoSelector = new DecoSelector(loggerFactory.CreateLogger()); + } + + [Test] + public void GetDecoEntries_Should_Not_Select_Deco_Before_Start_Time() + { + var deco = new Deco { Id = 1, Name = "Test Deco" }; + + var decoTemplateItem = new DecoTemplateItem + { + Id = 1, + DecoId = 1, + Deco = deco, + StartTime = new TimeSpan(10, 0, 0), + EndTime = new TimeSpan(18, 0, 0) + }; + + var decoTemplate = new DecoTemplate + { + Id = 1, + Name = "Test Deco Template", + Items = new List { decoTemplateItem } + }; + + var playoutTemplate = new PlayoutTemplate + { + Id = 1, + Template = new Template { Id = 1, Name = "Test Template" }, + DecoTemplate = decoTemplate, + DaysOfWeek = PlayoutTemplate.AllDaysOfWeek(), + DaysOfMonth = PlayoutTemplate.AllDaysOfMonth(), + MonthsOfYear = PlayoutTemplate.AllMonthsOfYear() + }; + + var playout = new Playout + { + Id = 1, + Templates = new List { playoutTemplate } + }; + + var now = new DateTimeOffset(2025, 9, 9, 9, 0, 0, TimeSpan.FromHours(-5)); + + var result = DecoSelector.GetDecoEntries(playout, now); + + result.TemplateDeco.IsNone.ShouldBeTrue(); + } + + [Test] + public void GetDecoEntries_Should_Select_Correct_Deco_From_Multiple() + { + var deco1 = new Deco { Id = 1, Name = "Test Deco 1" }; + var deco2 = new Deco { Id = 2, Name = "Test Deco 2" }; + + var decoTemplateItem1 = new DecoTemplateItem + { + Id = 1, + DecoId = 1, + Deco = deco1, + StartTime = new TimeSpan(8, 0, 0), + EndTime = new TimeSpan(10, 0, 0) + }; + + var decoTemplateItem2 = new DecoTemplateItem + { + Id = 2, + DecoId = 2, + Deco = deco2, + StartTime = new TimeSpan(10, 0, 0), + EndTime = new TimeSpan(18, 0, 0) + }; + + var decoTemplate = new DecoTemplate + { + Id = 1, + Name = "Test Deco Template", + Items = new List { decoTemplateItem1, decoTemplateItem2 } + }; + + var playoutTemplate = new PlayoutTemplate + { + Id = 1, + Template = new Template { Id = 1, Name = "Test Template" }, + DecoTemplate = decoTemplate, + DaysOfWeek = PlayoutTemplate.AllDaysOfWeek(), + DaysOfMonth = PlayoutTemplate.AllDaysOfMonth(), + MonthsOfYear = PlayoutTemplate.AllMonthsOfYear() + }; + + var playout = new Playout + { + Id = 1, + Templates = new List { playoutTemplate } + }; + + var now = new DateTimeOffset(2025, 9, 9, 9, 0, 0, TimeSpan.FromHours(-5)); + + var result = DecoSelector.GetDecoEntries(playout, now); + + result.TemplateDeco.IsSome.ShouldBeTrue(); + result.TemplateDeco.IfSome(d => d.ShouldBe(deco1)); + } +} diff --git a/ErsatzTV.Core.Tests/FFmpeg/WatermarkSelectorTests.cs b/ErsatzTV.Core.Tests/FFmpeg/WatermarkSelectorTests.cs index 467a8d57b..53ea06b4f 100644 --- a/ErsatzTV.Core.Tests/FFmpeg/WatermarkSelectorTests.cs +++ b/ErsatzTV.Core.Tests/FFmpeg/WatermarkSelectorTests.cs @@ -66,6 +66,7 @@ public class WatermarkSelectorTests WatermarkSelector = new WatermarkSelector( Substitute.For(), + new DecoSelector(loggerFactory.CreateLogger()), loggerFactory.CreateLogger()); WatermarkNone = Option.None; diff --git a/ErsatzTV.Core/FFmpeg/DecoSelector.cs b/ErsatzTV.Core/FFmpeg/DecoSelector.cs index c4f1719be..abaef4aa2 100644 --- a/ErsatzTV.Core/FFmpeg/DecoSelector.cs +++ b/ErsatzTV.Core/FFmpeg/DecoSelector.cs @@ -1,13 +1,17 @@ using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain.Scheduling; +using ErsatzTV.Core.Interfaces.FFmpeg; using ErsatzTV.Core.Scheduling; +using Microsoft.Extensions.Logging; namespace ErsatzTV.Core.FFmpeg; -public static class DecoSelector +public class DecoSelector(ILogger logger) : IDecoSelector { - public static DecoEntries GetDecoEntries(Playout playout, DateTimeOffset now) + public DecoEntries GetDecoEntries(Playout playout, DateTimeOffset now) { + logger.LogDebug("Checking for deco at {Now}", now); + if (playout is null) { return new DecoEntries(Option.None, Option.None); @@ -23,9 +27,10 @@ public static class DecoSelector { Option maybeItem = Optional(activeTemplate.DecoTemplate) .SelectMany(dt => dt.Items) - .Find(i => i.StartTime <= now.TimeOfDay && i.EndTime == TimeSpan.Zero || i.EndTime > now.TimeOfDay); + .Find(i => i.StartTime <= now.TimeOfDay && (i.EndTime == TimeSpan.Zero || i.EndTime > now.TimeOfDay)); foreach (DecoTemplateItem item in maybeItem) { + logger.LogDebug("Selecting deco between {Start} and {End}", item.StartTime, item.EndTime); maybeTemplateDeco = Optional(item.Deco); } } @@ -33,5 +38,3 @@ public static class DecoSelector return new DecoEntries(maybeTemplateDeco, maybePlayoutDeco); } } - -public sealed record DecoEntries(Option TemplateDeco, Option PlayoutDeco); diff --git a/ErsatzTV.Core/FFmpeg/WatermarkSelector.cs b/ErsatzTV.Core/FFmpeg/WatermarkSelector.cs index f750fa114..10342a9f4 100644 --- a/ErsatzTV.Core/FFmpeg/WatermarkSelector.cs +++ b/ErsatzTV.Core/FFmpeg/WatermarkSelector.cs @@ -8,7 +8,7 @@ using Microsoft.Extensions.Logging; namespace ErsatzTV.Core.FFmpeg; -public class WatermarkSelector(IImageCache imageCache, ILogger logger) +public class WatermarkSelector(IImageCache imageCache, IDecoSelector decoSelector, ILogger logger) : IWatermarkSelector { public List SelectWatermarks( @@ -17,6 +17,8 @@ public class WatermarkSelector(IImageCache imageCache, ILogger(); if (channel.StreamingMode == StreamingMode.HttpLiveStreamingDirect) @@ -30,7 +32,7 @@ public class WatermarkSelector(IImageCache imageCache, ILogger TemplateDeco, Option PlayoutDeco); diff --git a/ErsatzTV.Core/Interfaces/FFmpeg/IDecoSelector.cs b/ErsatzTV.Core/Interfaces/FFmpeg/IDecoSelector.cs new file mode 100644 index 000000000..f181436f0 --- /dev/null +++ b/ErsatzTV.Core/Interfaces/FFmpeg/IDecoSelector.cs @@ -0,0 +1,8 @@ +using ErsatzTV.Core.Domain; + +namespace ErsatzTV.Core.Interfaces.FFmpeg; + +public interface IDecoSelector +{ + DecoEntries GetDecoEntries(Playout playout, DateTimeOffset now); +} diff --git a/ErsatzTV.Scanner.Tests/Core/FFmpeg/TranscodingTests.cs b/ErsatzTV.Scanner.Tests/Core/FFmpeg/TranscodingTests.cs index bf32e7ba3..4b8c5007d 100644 --- a/ErsatzTV.Scanner.Tests/Core/FFmpeg/TranscodingTests.cs +++ b/ErsatzTV.Scanner.Tests/Core/FFmpeg/TranscodingTests.cs @@ -348,6 +348,7 @@ public class TranscodingTests WatermarkSelector watermarkSelector = new WatermarkSelector( mockImageCache, + new DecoSelector(LoggerFactory.CreateLogger()), LoggerFactory.CreateLogger()); List watermarks = []; @@ -653,6 +654,7 @@ public class TranscodingTests WatermarkSelector watermarkSelector = new WatermarkSelector( mockImageCache, + new DecoSelector(LoggerFactory.CreateLogger()), LoggerFactory.CreateLogger()); List watermarks = []; diff --git a/ErsatzTV/Program.cs b/ErsatzTV/Program.cs index cbe0aa43d..1284fef8a 100644 --- a/ErsatzTV/Program.cs +++ b/ErsatzTV/Program.cs @@ -82,12 +82,7 @@ public class Program // streaming .MinimumLevel.Override("ErsatzTV.Application.Streaming", LoggingLevelSwitches.StreamingLevelSwitch) .MinimumLevel.Override("ErsatzTV.FFmpeg", LoggingLevelSwitches.StreamingLevelSwitch) - .MinimumLevel.Override( - "ErsatzTV.Core.FFmpeg.FFmpegLibraryProcessService", - LoggingLevelSwitches.StreamingLevelSwitch) - .MinimumLevel.Override( - "ErsatzTV.Core.FFmpeg.FFmpegStreamSelector", - LoggingLevelSwitches.StreamingLevelSwitch) + .MinimumLevel.Override("ErsatzTV.Core.FFmpeg", LoggingLevelSwitches.StreamingLevelSwitch) .MinimumLevel.Override("ErsatzTV.Controllers.IptvController", LoggingLevelSwitches.StreamingLevelSwitch) .MinimumLevel.Override("ErsatzTV.Controllers.InternalController", LoggingLevelSwitches.StreamingLevelSwitch) .MinimumLevel.Override( diff --git a/ErsatzTV/Startup.cs b/ErsatzTV/Startup.cs index d53ef953a..a331c6d17 100644 --- a/ErsatzTV/Startup.cs +++ b/ErsatzTV/Startup.cs @@ -776,6 +776,7 @@ public class Startup services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped();