From 741b00fd52b08a2c27b346f9451e97b5c17a2e2d Mon Sep 17 00:00:00 2001 From: Jason Dove Date: Sun, 17 Apr 2022 13:30:47 -0500 Subject: [PATCH] fix multiple filler scheduling bugs (#738) --- CHANGELOG.md | 2 + .../PlayoutModeSchedulerBaseTests.cs | 107 +++++++++++++++++- .../Scheduling/PlayoutModeSchedulerBase.cs | 48 ++++---- .../ViewModels/FillerPresetEditViewModel.cs | 7 +- 4 files changed, 138 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 914ce7090..c38c1fcb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Fix subtitles using software encoders, videotoolbox, VAAPI - Fix setting VAAPI driver name - Fix ffmpeg troubleshooting reports +- Fix bug where filler would behave as if it were configured to pad even though a different mode was selected +- Fix bug where mid-roll count filler would skip scheduling the final chapter in an episode ## [0.5.0-beta] - 2022-04-13 ### Fixed diff --git a/ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerBaseTests.cs b/ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerBaseTests.cs index 37c943598..f310b4f2e 100644 --- a/ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerBaseTests.cs +++ b/ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerBaseTests.cs @@ -3,8 +3,10 @@ using ErsatzTV.Core.Domain.Filler; using ErsatzTV.Core.Interfaces.Scheduling; using ErsatzTV.Core.Scheduling; using FluentAssertions; +using Microsoft.Extensions.Logging; using Moq; using NUnit.Framework; +using Serilog; namespace ErsatzTV.Core.Tests.Scheduling; @@ -222,7 +224,7 @@ public class PlayoutModeSchedulerBaseTests : SchedulerTestBase PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); - List playoutItems = PlayoutModeSchedulerBase + List playoutItems = Scheduler() .AddFiller( startState, CollectionEnumerators(scheduleItem, enumerator), @@ -273,7 +275,7 @@ public class PlayoutModeSchedulerBaseTests : SchedulerTestBase // too lazy to make another enumerator for the filler that we don't want enumerators.Add(CollectionKey.ForFillerPreset(scheduleItem.MidRollFiller), enumerator); - List playoutItems = PlayoutModeSchedulerBase + List playoutItems = Scheduler() .AddFiller( startState, enumerators, @@ -283,6 +285,77 @@ public class PlayoutModeSchedulerBaseTests : SchedulerTestBase playoutItems.Count.Should().Be(1); } + + [Test] + public void Should_Schedule_Mid_Roll_Count_Filler_Correctly() + { + Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromHours(1)); + Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(5)); + + var scheduleItem = new ProgramScheduleItemOne + { + Id = 1, + Index = 1, + Collection = collectionOne, + CollectionId = collectionOne.Id, + StartTime = null, + PlaybackOrder = PlaybackOrder.Chronological, + TailFiller = null, + FallbackFiller = null, + MidRollFiller = new FillerPreset + { + FillerKind = FillerKind.MidRoll, + FillerMode = FillerMode.Count, + PadToNearestMinute = 60, // this should be ignored + Count = 1 + } + }; + + var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator( + new List { scheduleItem }, + new CollectionEnumeratorState()); + + var enumerator = new ChronologicalMediaCollectionEnumerator( + collectionOne.MediaItems, + new CollectionEnumeratorState()); + + var fillerEnumerator = new ChronologicalMediaCollectionEnumerator( + collectionTwo.MediaItems, + new CollectionEnumeratorState()); + + PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); + + Dictionary enumerators = CollectionEnumerators( + scheduleItem, + enumerator); + + enumerators.Add(CollectionKey.ForFillerPreset(scheduleItem.MidRollFiller), fillerEnumerator); + + List playoutItems = Scheduler() + .AddFiller( + startState, + enumerators, + scheduleItem, + new PlayoutItem + { + MediaItemId = 1, + Start = startState.CurrentTime.UtcDateTime, + Finish = startState.CurrentTime.AddHours(1).UtcDateTime + }, + new List + { + new() { StartTime = TimeSpan.Zero, EndTime = TimeSpan.FromMinutes(6) }, + new() { StartTime = TimeSpan.FromMinutes(6), EndTime = TimeSpan.FromMinutes(60) } + }); + + playoutItems.Count.Should().Be(3); + playoutItems[0].MediaItemId.Should().Be(1); + playoutItems[0].StartOffset.Should().Be(startState.CurrentTime); + playoutItems[1].MediaItemId.Should().Be(3); + playoutItems[1].StartOffset.Should().Be(startState.CurrentTime + TimeSpan.FromMinutes(6)); + playoutItems[2].MediaItemId.Should().Be(1); + playoutItems[2].StartOffset.Should().Be(startState.CurrentTime + TimeSpan.FromMinutes(11)); + } } [TestFixture] @@ -324,4 +397,34 @@ public class PlayoutModeSchedulerBaseTests : SchedulerTestBase new() { Duration = duration } } }; + + private static PlayoutModeSchedulerBase Scheduler() => + new TestScheduler(); + + private class TestScheduler : PlayoutModeSchedulerBase + { + private static readonly ILoggerFactory LoggerFactory; + + static TestScheduler() + { + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Debug() + .WriteTo.Console() + .CreateLogger(); + + LoggerFactory = new LoggerFactory().AddSerilog(Log.Logger); + } + + public TestScheduler() : base(LoggerFactory.CreateLogger()) + { + } + + public override Tuple> Schedule( + PlayoutBuilderState playoutBuilderState, + Dictionary collectionEnumerators, + ProgramScheduleItem scheduleItem, + ProgramScheduleItem nextScheduleItem, + DateTimeOffset hardStop) => + throw new NotSupportedException(); + } } \ No newline at end of file diff --git a/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerBase.cs b/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerBase.cs index 14e9182bb..94a586ada 100644 --- a/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerBase.cs +++ b/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerBase.cs @@ -191,11 +191,9 @@ public abstract class PlayoutModeSchedulerBase : IPlayoutModeScheduler whe .Append(Optional(scheduleItem.PostRollFiller)) .ToList(); - if (allFiller.Count(f => f.PadToNearestMinute.HasValue) > 1) - // if (allFiller.Map(f => Optional(f.PadToNearestMinute)).Sequence().Flatten().Distinct().Count() > 1) + // multiple pad-to-nearest-minute values are invalid; use no filler + if (allFiller.Count(f => f.FillerMode == FillerMode.Pad && f.PadToNearestMinute.HasValue) > 1) { - // multiple pad-to-nearest-minute values are invalid; use no filler - // TODO: log error? return itemStartTime + itemDuration; } @@ -287,16 +285,17 @@ public abstract class PlayoutModeSchedulerBase : IPlayoutModeScheduler whe } } - foreach (FillerPreset padFiller in Optional(allFiller.FirstOrDefault(f => f.PadToNearestMinute.HasValue))) + foreach (FillerPreset padFiller in Optional( + allFiller.FirstOrDefault(f => f.FillerMode == FillerMode.Pad && f.PadToNearestMinute.HasValue))) { int currentMinute = (itemStartTime + totalDuration).Minute; // ReSharper disable once PossibleInvalidOperationException int targetMinute = (currentMinute + padFiller.PadToNearestMinute.Value - 1) / padFiller.PadToNearestMinute.Value * padFiller.PadToNearestMinute.Value; - + DateTimeOffset targetTime = itemStartTime + totalDuration - TimeSpan.FromMinutes(currentMinute) + TimeSpan.FromMinutes(targetMinute); - + return new DateTimeOffset( targetTime.Year, targetTime.Month, @@ -310,7 +309,7 @@ public abstract class PlayoutModeSchedulerBase : IPlayoutModeScheduler whe return itemStartTime + totalDuration; } - internal static List AddFiller( + internal List AddFiller( PlayoutBuilderState playoutBuilderState, Dictionary enumerators, ProgramScheduleItem scheduleItem, @@ -324,11 +323,10 @@ public abstract class PlayoutModeSchedulerBase : IPlayoutModeScheduler whe .Append(Optional(scheduleItem.PostRollFiller)) .ToList(); - if (allFiller.Count(f => f.PadToNearestMinute.HasValue) > 1) - // if (allFiller.Map(f => Optional(f.PadToNearestMinute)).Sequence().Flatten().Distinct().Count() > 1) + // multiple pad-to-nearest-minute values are invalid; use no filler + if (allFiller.Count(f => f.FillerMode == FillerMode.Pad && f.PadToNearestMinute.HasValue) > 1) { - // multiple pad-to-nearest-minute values are invalid; use no filler - // TODO: log error? + _logger.LogError("Multiple pad-to-nearest-minute values are invalid; no filler will be used"); return new List { playoutItem }; } @@ -386,15 +384,18 @@ public abstract class PlayoutModeSchedulerBase : IPlayoutModeScheduler whe break; case FillerMode.Count when filler.Count.HasValue: IMediaCollectionEnumerator e2 = enumerators[CollectionKey.ForFillerPreset(filler)]; - for (var i = 0; i < effectiveChapters.Count - 1; i++) + for (var i = 0; i < effectiveChapters.Count; i++) { result.Add(playoutItem.ForChapter(effectiveChapters[i])); - result.AddRange( - AddCountFiller( - playoutBuilderState, - e2, - filler.Count.Value, - FillerKind.MidRoll)); + if (i < effectiveChapters.Count - 1) + { + result.AddRange( + AddCountFiller( + playoutBuilderState, + e2, + filler.Count.Value, + FillerKind.MidRoll)); + } } break; @@ -421,13 +422,14 @@ public abstract class PlayoutModeSchedulerBase : IPlayoutModeScheduler whe } // after all non-padded filler has been added, figure out padding - foreach (FillerPreset padFiller in Optional(allFiller.FirstOrDefault(f => f.PadToNearestMinute.HasValue))) + foreach (FillerPreset padFiller in Optional( + allFiller.FirstOrDefault(f => f.FillerMode == FillerMode.Pad && f.PadToNearestMinute.HasValue))) { var totalDuration = TimeSpan.FromMilliseconds( result.Sum(pi => (pi.Finish - pi.Start).TotalMilliseconds) + effectiveChapters.Sum(c => (c.EndTime - c.StartTime).TotalMilliseconds)); - + int currentMinute = (playoutItem.StartOffset + totalDuration).Minute; // ReSharper disable once PossibleInvalidOperationException int targetMinute = (currentMinute + padFiller.PadToNearestMinute.Value - 1) / @@ -437,7 +439,7 @@ public abstract class PlayoutModeSchedulerBase : IPlayoutModeScheduler whe TimeSpan.FromMinutes(currentMinute) + TimeSpan.FromMinutes(targetMinute); - + var targetTime = new DateTimeOffset( almostTargetTime.Year, almostTargetTime.Month, @@ -532,7 +534,7 @@ public abstract class PlayoutModeSchedulerBase : IPlayoutModeScheduler whe } } } - + break; case FillerKind.PostRoll: IMediaCollectionEnumerator post1 = enumerators[CollectionKey.ForFillerPreset(padFiller)]; diff --git a/ErsatzTV/ViewModels/FillerPresetEditViewModel.cs b/ErsatzTV/ViewModels/FillerPresetEditViewModel.cs index a7796d960..1771537f1 100644 --- a/ErsatzTV/ViewModels/FillerPresetEditViewModel.cs +++ b/ErsatzTV/ViewModels/FillerPresetEditViewModel.cs @@ -15,6 +15,7 @@ public class FillerPresetEditViewModel private FillerMode _fillerMode; private int? _count; private ProgramScheduleItemCollectionType _collectionType; + private int? _padToNearestMinute; public int Id { get; set; } public string Name { get; set; } @@ -50,7 +51,11 @@ public class FillerPresetEditViewModel set => _count = value; } - public int? PadToNearestMinute { get; set; } + public int? PadToNearestMinute + { + get => FillerMode == FillerMode.Pad ? _padToNearestMinute : null; + set => _padToNearestMinute = value; + } public ProgramScheduleItemCollectionType CollectionType {