From 555b156154205e7403b5d64e7677ddf6c957057b Mon Sep 17 00:00:00 2001 From: Jason Dove Date: Fri, 7 Oct 2022 09:21:19 -0500 Subject: [PATCH] fix tail and fallback filler scheduling (#981) --- CHANGELOG.md | 3 + .../PlayoutModeSchedulerFloodTests.cs | 210 ++++++++++++++++++ .../Scheduling/PlayoutModeSchedulerBase.cs | 19 +- .../PlayoutModeSchedulerDuration.cs | 2 +- .../Scheduling/PlayoutModeSchedulerFlood.cs | 2 +- .../PlayoutModeSchedulerMultiple.cs | 2 +- .../Scheduling/PlayoutModeSchedulerOne.cs | 7 +- 7 files changed, 235 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6acdc33a..26e36bdd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [Unreleased] +### Fixed +- Fix bug where tail or fallback filler would sometimes schedule much longer than expected + - This only happened with fixed start schedule items following a schedule item with tail or fallback filler ## [0.6.8-beta] - 2022-10-05 ### Fixed diff --git a/ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerFloodTests.cs b/ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerFloodTests.cs index 8bd3bf90..445bbde3 100644 --- a/ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerFloodTests.cs +++ b/ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerFloodTests.cs @@ -823,6 +823,216 @@ public class PlayoutModeSchedulerFloodTests : SchedulerTestBase playoutItems[6].GuideGroup.Should().Be(3); playoutItems[6].FillerKind.Should().Be(FillerKind.Fallback); } + + [Test] + public void Should_Not_Schedule_Fallback_Filler_Incomplete_Flood() + { + Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(20)); + Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(1)); + + var scheduleItem = new ProgramScheduleItemFlood + { + Id = 1, + Index = 1, + Collection = collectionOne, + CollectionId = collectionOne.Id, + StartTime = null, + PlaybackOrder = PlaybackOrder.Chronological, + TailFiller = null, + FallbackFiller = new FillerPreset + { + FillerKind = FillerKind.Fallback, + Collection = collectionTwo, + CollectionId = collectionTwo.Id + } + }; + + var enumerator1 = new ChronologicalMediaCollectionEnumerator( + collectionOne.MediaItems, + new CollectionEnumeratorState()); + + var enumerator2 = new ChronologicalMediaCollectionEnumerator( + collectionTwo.MediaItems, + new CollectionEnumeratorState()); + + var sortedScheduleItems = new List + { + scheduleItem, + NextScheduleItem + }; + + var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator( + sortedScheduleItems, + new CollectionEnumeratorState()); + + PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); + + var scheduler = new PlayoutModeSchedulerFlood(new Mock().Object); + + // hard stop at 2, an hour before the "next schedule item" at 3 + DateTimeOffset hardStop = StartState(scheduleItemsEnumerator).CurrentTime.AddHours(2); + + (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( + startState, + CollectionEnumerators( + scheduleItem, + enumerator1, + scheduleItem.FallbackFiller, + enumerator2), + scheduleItem, + NextScheduleItem, + hardStop); + + playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(2)); + playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); + + playoutBuilderState.NextGuideGroup.Should().Be(7); + playoutBuilderState.DurationFinish.IsNone.Should().BeTrue(); + playoutBuilderState.InFlood.Should().BeTrue(); + playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); + playoutBuilderState.InDurationFiller.Should().BeFalse(); + playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0); + + enumerator1.State.Index.Should().Be(0); + enumerator2.State.Index.Should().Be(0); + + playoutItems.Count.Should().Be(6); + + playoutItems[0].MediaItemId.Should().Be(1); + playoutItems[0].StartOffset.Should().Be(startState.CurrentTime); + playoutItems[0].GuideGroup.Should().Be(1); + playoutItems[0].FillerKind.Should().Be(FillerKind.None); + + playoutItems[1].MediaItemId.Should().Be(2); + playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddMinutes(20)); + playoutItems[1].GuideGroup.Should().Be(2); + playoutItems[1].FillerKind.Should().Be(FillerKind.None); + + playoutItems[2].MediaItemId.Should().Be(1); + playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.AddMinutes(40)); + playoutItems[2].GuideGroup.Should().Be(3); + playoutItems[2].FillerKind.Should().Be(FillerKind.None); + + playoutItems[3].MediaItemId.Should().Be(2); + playoutItems[3].StartOffset.Should().Be(startState.CurrentTime.AddMinutes(60)); + playoutItems[3].GuideGroup.Should().Be(4); + playoutItems[3].FillerKind.Should().Be(FillerKind.None); + + playoutItems[4].MediaItemId.Should().Be(1); + playoutItems[4].StartOffset.Should().Be(startState.CurrentTime.AddMinutes(80)); + playoutItems[4].GuideGroup.Should().Be(5); + playoutItems[4].FillerKind.Should().Be(FillerKind.None); + + playoutItems[5].MediaItemId.Should().Be(2); + playoutItems[5].StartOffset.Should().Be(startState.CurrentTime.AddMinutes(100)); + playoutItems[5].GuideGroup.Should().Be(6); + playoutItems[5].FillerKind.Should().Be(FillerKind.None); + } + + [Test] + public void Should_Not_Schedule_Tail_Filler_Incomplete_Flood() + { + Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(20)); + Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(1)); + + var scheduleItem = new ProgramScheduleItemFlood + { + Id = 1, + Index = 1, + Collection = collectionOne, + CollectionId = collectionOne.Id, + StartTime = null, + PlaybackOrder = PlaybackOrder.Chronological, + TailFiller = new FillerPreset + { + FillerKind = FillerKind.Tail, + Collection = collectionTwo, + CollectionId = collectionTwo.Id + }, + FallbackFiller = null + }; + + var enumerator1 = new ChronologicalMediaCollectionEnumerator( + collectionOne.MediaItems, + new CollectionEnumeratorState()); + + var enumerator2 = new ChronologicalMediaCollectionEnumerator( + collectionTwo.MediaItems, + new CollectionEnumeratorState()); + + var sortedScheduleItems = new List + { + scheduleItem, + NextScheduleItem + }; + + var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator( + sortedScheduleItems, + new CollectionEnumeratorState()); + + PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); + + var scheduler = new PlayoutModeSchedulerFlood(new Mock().Object); + + // hard stop at 2, an hour before the "next schedule item" at 3 + DateTimeOffset hardStop = StartState(scheduleItemsEnumerator).CurrentTime.AddHours(2); + + (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( + startState, + CollectionEnumerators( + scheduleItem, + enumerator1, + scheduleItem.TailFiller, + enumerator2), + scheduleItem, + NextScheduleItem, + hardStop); + + playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(2)); + playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); + + playoutBuilderState.NextGuideGroup.Should().Be(7); + playoutBuilderState.DurationFinish.IsNone.Should().BeTrue(); + playoutBuilderState.InFlood.Should().BeTrue(); + playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); + playoutBuilderState.InDurationFiller.Should().BeFalse(); + playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0); + + enumerator1.State.Index.Should().Be(0); + enumerator2.State.Index.Should().Be(0); + + playoutItems.Count.Should().Be(6); + + playoutItems[0].MediaItemId.Should().Be(1); + playoutItems[0].StartOffset.Should().Be(startState.CurrentTime); + playoutItems[0].GuideGroup.Should().Be(1); + playoutItems[0].FillerKind.Should().Be(FillerKind.None); + + playoutItems[1].MediaItemId.Should().Be(2); + playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddMinutes(20)); + playoutItems[1].GuideGroup.Should().Be(2); + playoutItems[1].FillerKind.Should().Be(FillerKind.None); + + playoutItems[2].MediaItemId.Should().Be(1); + playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.AddMinutes(40)); + playoutItems[2].GuideGroup.Should().Be(3); + playoutItems[2].FillerKind.Should().Be(FillerKind.None); + + playoutItems[3].MediaItemId.Should().Be(2); + playoutItems[3].StartOffset.Should().Be(startState.CurrentTime.AddMinutes(60)); + playoutItems[3].GuideGroup.Should().Be(4); + playoutItems[3].FillerKind.Should().Be(FillerKind.None); + + playoutItems[4].MediaItemId.Should().Be(1); + playoutItems[4].StartOffset.Should().Be(startState.CurrentTime.AddMinutes(80)); + playoutItems[4].GuideGroup.Should().Be(5); + playoutItems[4].FillerKind.Should().Be(FillerKind.None); + + playoutItems[5].MediaItemId.Should().Be(2); + playoutItems[5].StartOffset.Should().Be(startState.CurrentTime.AddMinutes(100)); + playoutItems[5].GuideGroup.Should().Be(6); + playoutItems[5].FillerKind.Should().Be(FillerKind.None); + } [Test] public void Should_Not_Have_Gap_With_Unused_Tail_And_Unused_Fallback() diff --git a/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerBase.cs b/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerBase.cs index 858afe44..3db67675 100644 --- a/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerBase.cs +++ b/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerBase.cs @@ -20,9 +20,24 @@ public abstract class PlayoutModeSchedulerBase : IPlayoutModeScheduler whe ProgramScheduleItem nextScheduleItem, DateTimeOffset hardStop); - public static DateTimeOffset GetStartTimeAfter( + public static DateTimeOffset GetFillerStartTimeAfter( PlayoutBuilderState state, - ProgramScheduleItem scheduleItem) + ProgramScheduleItem scheduleItem, + DateTimeOffset hardStop + ) + { + DateTimeOffset startTime = GetStartTimeAfter(state, scheduleItem); + + // filler should always stop at the hard stop + if (hardStop < startTime) + { + startTime = hardStop; + } + + return startTime; + } + + public static DateTimeOffset GetStartTimeAfter(PlayoutBuilderState state, ProgramScheduleItem scheduleItem) { DateTimeOffset startTime = state.CurrentTime.ToLocalTime(); diff --git a/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerDuration.cs b/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerDuration.cs index 0bf7e2be..60d7becc 100644 --- a/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerDuration.cs +++ b/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerDuration.cs @@ -196,7 +196,7 @@ public class PlayoutModeSchedulerDuration : PlayoutModeSchedulerBase pi.FillerKind == FillerKind.None).ToList(); - PlayoutItem last = all.OrderBy(pi => pi.FinishOffset).LastOrDefault(); + PlayoutItem last = all.MaxBy(pi => pi.FinishOffset); foreach (PlayoutItem item in all.Filter(pi => pi != last)) { item.GuideFinish = null; diff --git a/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerFlood.cs b/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerFlood.cs index 4a1b7bc8..1d765420 100644 --- a/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerFlood.cs +++ b/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerFlood.cs @@ -138,7 +138,7 @@ public class PlayoutModeSchedulerFlood : PlayoutModeSchedulerBase= hardStop) { playoutBuilderState = playoutBuilderState with { CurrentTime = hardStop }; @@ -84,7 +81,7 @@ public class PlayoutModeSchedulerOne : PlayoutModeSchedulerBase