diff --git a/CHANGELOG.md b/CHANGELOG.md index 648fedef..7c23b26c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Fixed - Fix error display with `HLS Segmenter` and `MPEG-TS` streaming modes - Remove erroneous log messages about normalizing framerate on channels where framerate normalization is disabled +- Fix unscheduled filler gaps that sometimes happen as playouts are automatically extended each hour ### Added - Clean transcode cache folder on startup and after `HLS Segmenter` session terminates for any reason diff --git a/ErsatzTV.Core.Tests/Scheduling/PlayoutBuilderTests.cs b/ErsatzTV.Core.Tests/Scheduling/PlayoutBuilderTests.cs index 32af4d16..5b0912db 100644 --- a/ErsatzTV.Core.Tests/Scheduling/PlayoutBuilderTests.cs +++ b/ErsatzTV.Core.Tests/Scheduling/PlayoutBuilderTests.cs @@ -68,6 +68,8 @@ public class PlayoutBuilderTests result.Items.Head().MediaItemId.Should().Be(2); result.Items.Head().StartOffset.TimeOfDay.Should().Be(TimeSpan.Zero); result.Items.Head().FinishOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(6)); + + result.Anchor.NextStartOffset.Should().Be(HoursAfterMidnight(6)); } [Test] @@ -127,6 +129,8 @@ public class PlayoutBuilderTests result.Items.Head().MediaItemId.Should().Be(2); result.Items.Head().StartOffset.TimeOfDay.Should().Be(TimeSpan.Zero); result.Items.Head().FinishOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(6)); + + result.Anchor.NextStartOffset.Should().Be(HoursAfterMidnight(6)); } [Test] @@ -186,6 +190,8 @@ public class PlayoutBuilderTests result.Items.Head().MediaItemId.Should().Be(2); result.Items.Head().StartOffset.TimeOfDay.Should().Be(TimeSpan.Zero); result.Items.Head().FinishOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(6)); + + result.Anchor.NextStartOffset.Should().Be(HoursAfterMidnight(6)); } [Test] @@ -214,6 +220,8 @@ public class PlayoutBuilderTests result.Items.Count.Should().Be(1); result.Items.Head().StartOffset.TimeOfDay.Should().Be(TimeSpan.Zero); result.Items.Head().FinishOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(6)); + + result.Anchor.NextStartOffset.Should().Be(HoursAfterMidnight(6)); } [Test] @@ -242,6 +250,8 @@ public class PlayoutBuilderTests result.Items.Count.Should().Be(1); result.Items.Head().StartOffset.TimeOfDay.Should().Be(TimeSpan.Zero); result.Items.Head().FinishOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(6)); + + result.Anchor.NextStartOffset.Should().Be(HoursAfterMidnight(6)); } [Test] @@ -261,6 +271,8 @@ public class PlayoutBuilderTests result.Items.Count.Should().Be(1); result.Items.Head().StartOffset.TimeOfDay.Should().Be(TimeSpan.Zero); result.Items.Head().FinishOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(6)); + + result.Anchor.NextStartOffset.Should().Be(HoursAfterMidnight(6)); } [Test] @@ -281,6 +293,8 @@ public class PlayoutBuilderTests result.Items[0].StartOffset.TimeOfDay.Should().Be(TimeSpan.Zero); result.Items[1].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(6)); result.Items[1].FinishOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(12)); + + result.Anchor.NextStartOffset.Should().Be(HoursAfterMidnight(12)); } [Test] @@ -308,6 +322,8 @@ public class PlayoutBuilderTests result.Items[2].MediaItemId.Should().Be(1); result.Items[3].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(3)); result.Items[3].MediaItemId.Should().Be(2); + + result.Anchor.NextStartOffset.Should().Be(HoursAfterMidnight(4)); } [Test] @@ -329,7 +345,7 @@ public class PlayoutBuilderTests result.Items.Count.Should().Be(1); result.Items.Head().MediaItemId.Should().Be(1); - result.Anchor.NextStartOffset.Should().Be(DateTime.Today.AddHours(6)); + result.Anchor.NextStartOffset.Should().Be(HoursAfterMidnight(6)); result.ProgramScheduleAnchors.Count.Should().Be(1); result.ProgramScheduleAnchors.Head().EnumeratorState.Index.Should().Be(1); @@ -343,7 +359,7 @@ public class PlayoutBuilderTests result2.Items.Last().StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(6)); result2.Items.Last().MediaItemId.Should().Be(2); - result2.Anchor.NextStartOffset.Should().Be(DateTime.Today.AddHours(12)); + result2.Anchor.NextStartOffset.Should().Be(HoursAfterMidnight(12)); result2.ProgramScheduleAnchors.Count.Should().Be(1); result2.ProgramScheduleAnchors.Head().EnumeratorState.Index.Should().Be(0); } @@ -452,6 +468,8 @@ public class PlayoutBuilderTests int firstSeedValue = result.ProgramScheduleAnchors.Head().EnumeratorState.Seed; + result.Anchor.NextStartOffset.Should().Be(HoursAfterMidnight(6)); + DateTimeOffset start2 = HoursAfterMidnight(0); DateTimeOffset finish2 = start2 + TimeSpan.FromHours(6); @@ -460,6 +478,8 @@ public class PlayoutBuilderTests int secondSeedValue = result2.ProgramScheduleAnchors.Head().EnumeratorState.Seed; firstSeedValue.Should().Be(secondSeedValue); + + result2.Anchor.NextStartOffset.Should().Be(HoursAfterMidnight(6)); } [Test] @@ -548,6 +568,8 @@ public class PlayoutBuilderTests result.Items[3].MediaItemId.Should().Be(3); result.Items[4].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(5)); result.Items[4].MediaItemId.Should().Be(2); + + result.Anchor.NextStartOffset.Should().Be(HoursAfterMidnight(6)); } [Test] @@ -682,6 +704,8 @@ public class PlayoutBuilderTests result.Items[26].MediaItemId.Should().Be(3); result.Items[27].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(5)); result.Items[27].MediaItemId.Should().Be(2); + + result.Anchor.NextStartOffset.Should().Be(HoursAfterMidnight(30)); } [Test] @@ -777,6 +801,8 @@ public class PlayoutBuilderTests result.Items[5].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(6)); result.Items[5].MediaItemId.Should().Be(2); + + result.Anchor.NextStartOffset.Should().Be(HoursAfterMidnight(7)); } [Test] @@ -870,6 +896,8 @@ public class PlayoutBuilderTests result.Items[5].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(12)); result.Items[5].MediaItemId.Should().Be(3); + + result.Anchor.NextStartOffset.Should().Be(HoursAfterMidnight(31)); } [Test] @@ -974,6 +1002,8 @@ public class PlayoutBuilderTests result.Items[4].MediaItemId.Should().Be(2); result.Anchor.InFlood.Should().BeTrue(); + + result.Anchor.NextStartOffset.Should().Be(HoursAfterMidnight(32)); } [Test] @@ -1073,6 +1103,8 @@ public class PlayoutBuilderTests result.Items[5].MediaItemId.Should().Be(1); result.Items[6].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(5.75)); result.Items[6].MediaItemId.Should().Be(2); + + result.Anchor.NextStartOffset.Should().Be(DateTime.Today.AddHours(6.75)); } [Test] @@ -1171,6 +1203,8 @@ public class PlayoutBuilderTests result.Items[5].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(4.75)); result.Items[5].MediaItemId.Should().Be(4); + + result.Anchor.NextStartOffset.Should().Be(DateTime.Today.AddHours(6.25)); } [Test] @@ -1275,6 +1309,7 @@ public class PlayoutBuilderTests result.Anchor.ScheduleItemsEnumeratorState.Index.Should().Be(1); result.Anchor.MultipleRemaining.Should().Be(1); + result.Anchor.NextStartOffset.Should().Be(HoursAfterMidnight(5)); } [Test] @@ -1374,6 +1409,7 @@ public class PlayoutBuilderTests result.Anchor.ScheduleItemsEnumeratorState.Index.Should().Be(0); result.Anchor.MultipleRemaining.Should().BeNull(); + result.Anchor.NextStartOffset.Should().Be(HoursAfterMidnight(5)); } [Test] @@ -1480,6 +1516,7 @@ public class PlayoutBuilderTests result.Anchor.ScheduleItemsEnumeratorState.Index.Should().Be(1); result.Anchor.DurationFinish.Should().Be(HoursAfterMidnight(6).UtcDateTime); + result.Anchor.NextStartOffset.Should().Be(HoursAfterMidnight(5)); } [Test] @@ -1617,6 +1654,96 @@ public class PlayoutBuilderTests result.Anchor.ScheduleItemsEnumeratorState.Index.Should().Be(0); result.Anchor.DurationFinish.Should().BeNull(); + + result.Anchor.NextStartOffset.Should().Be(HoursAfterMidnight(6)); + } + + [Test] + public async Task Multiple_With_Filler_Should_Keep_Filler_After_End_Of_Playout() + { + var collectionOne = new Collection + { + Id = 1, + Name = "Duration Items 1", + MediaItems = new List + { + TestMovie(1, TimeSpan.FromMinutes(61), new DateTime(2020, 1, 1)) + } + }; + + var collectionTwo = new Collection + { + Id = 2, + Name = "Filler Items", + MediaItems = new List + { + TestMovie(2, TimeSpan.FromMinutes(4), new DateTime(2020, 1, 1)) + } + }; + + var fakeRepository = new FakeMediaCollectionRepository( + Map( + (collectionOne.Id, collectionOne.MediaItems.ToList()), + (collectionTwo.Id, collectionTwo.MediaItems.ToList()))); + + var items = new List + { + new ProgramScheduleItemMultiple + { + Id = 1, + Index = 1, + Collection = collectionOne, + CollectionId = collectionOne.Id, + StartTime = null, + Count = 1, + PlaybackOrder = PlaybackOrder.Chronological, + PostRollFiller = new FillerPreset + { + FillerKind = FillerKind.PostRoll, + Collection = collectionTwo, + CollectionId = collectionTwo.Id, + FillerMode = FillerMode.Count, + Count = 1 + } + } + }; + + var playout = new Playout + { + ProgramSchedule = new ProgramSchedule + { + Items = items + }, + Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" }, + ProgramScheduleAnchors = new List(), + Items = new List() + }; + + var configRepo = new Mock(); + var televisionRepo = new FakeTelevisionRepository(); + var artistRepo = new Mock(); + var builder = new PlayoutBuilder( + configRepo.Object, + fakeRepository, + televisionRepo, + artistRepo.Object, + _logger); + + DateTimeOffset start = HoursAfterMidnight(0); + DateTimeOffset finish = start + TimeSpan.FromHours(1); + + Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish); + + result.Items.Count.Should().Be(2); + + result.Items[0].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromMinutes(0)); + result.Items[0].MediaItemId.Should().Be(1); + result.Items[1].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromMinutes(61)); + result.Items[1].MediaItemId.Should().Be(2); + + result.Anchor.ScheduleItemsEnumeratorState.Index.Should().Be(0); + result.Anchor.DurationFinish.Should().BeNull(); + result.Anchor.NextStartOffset.Should().Be(DateTime.Today.AddMinutes(65)); } [Test] @@ -1696,6 +1823,7 @@ public class PlayoutBuilderTests result.Anchor.ScheduleItemsEnumeratorState.Index.Should().Be(0); result.Anchor.DurationFinish.Should().BeNull(); + result.Anchor.NextStartOffset.Should().Be(HoursAfterMidnight(6)); } [Test] @@ -1746,6 +1874,8 @@ public class PlayoutBuilderTests int seed = result.ProgramScheduleAnchors[0].EnumeratorState.Seed; result.ProgramScheduleAnchors.All(a => a.EnumeratorState.Seed == seed).Should().BeTrue(); + + result.Anchor.NextStartOffset.Should().Be(DateTime.Today.AddDays(2)); } } @@ -1898,6 +2028,8 @@ public class PlayoutBuilderTests result.Items[3].MediaItemId.Should().Be(2); result.Items[3].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(18)); result.Items[3].FinishOffset.TimeOfDay.Should().Be(TimeSpan.Zero); + + result.Anchor.NextStartOffset.Should().Be(HoursAfterMidnight(48)); } } @@ -2214,6 +2346,7 @@ public class PlayoutBuilderTests result.Items[4].MediaItemId.Should().Be(2); result.Anchor.InFlood.Should().BeTrue(); + result.Anchor.NextStartOffset.Should().Be(HoursAfterMidnight(32)); } [Test] @@ -2318,6 +2451,7 @@ public class PlayoutBuilderTests result.Anchor.ScheduleItemsEnumeratorState.Index.Should().Be(1); result.Anchor.MultipleRemaining.Should().Be(1); + result.Anchor.NextStartOffset.Should().Be(HoursAfterMidnight(5)); } [Test] @@ -2424,6 +2558,7 @@ public class PlayoutBuilderTests result.Anchor.ScheduleItemsEnumeratorState.Index.Should().Be(1); result.Anchor.DurationFinish.Should().Be(HoursAfterMidnight(6).UtcDateTime); + result.Anchor.NextStartOffset.Should().Be(HoursAfterMidnight(5)); } } diff --git a/ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerDurationTests.cs b/ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerDurationTests.cs index cbc9e37d..eb25b482 100644 --- a/ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerDurationTests.cs +++ b/ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerDurationTests.cs @@ -695,4 +695,60 @@ public class PlayoutModeSchedulerDurationTests : SchedulerTestBase playoutItems[6].FillerKind.Should().Be(FillerKind.Fallback); playoutItems[6].GuideFinish.HasValue.Should().BeFalse(); } + + [Test] + public void Should_Not_Schedule_At_HardStop() + { + Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(55)); + + var scheduleItem = new ProgramScheduleItemDuration + { + Id = 1, + Index = 1, + Collection = collectionOne, + CollectionId = collectionOne.Id, + StartTime = TimeSpan.FromHours(6), + PlaybackOrder = PlaybackOrder.Chronological, + TailFiller = null, + FallbackFiller = null, + PlayoutDuration = TimeSpan.FromHours(1) + }; + + var enumerator = new ChronologicalMediaCollectionEnumerator( + collectionOne.MediaItems, + new CollectionEnumeratorState()); + + var sortedScheduleItems = new List + { + scheduleItem, + NextScheduleItem + }; + + var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator( + sortedScheduleItems, + new CollectionEnumeratorState()); + + PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); + + var scheduler = new PlayoutModeSchedulerDuration(new Mock().Object); + (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( + startState, + CollectionEnumerators(scheduleItem, enumerator), + scheduleItem, + NextScheduleItem, + HardStop(scheduleItemsEnumerator)); + + playoutItems.Should().BeEmpty(); + + playoutBuilderState.CurrentTime.Should().Be(HardStop(scheduleItemsEnumerator)); + + playoutBuilderState.NextGuideGroup.Should().Be(1); + playoutBuilderState.DurationFinish.IsNone.Should().BeTrue(); + playoutBuilderState.InFlood.Should().BeFalse(); + playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); + playoutBuilderState.InDurationFiller.Should().BeFalse(); + playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0); + + enumerator.State.Index.Should().Be(0); + } } diff --git a/ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerFloodTests.cs b/ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerFloodTests.cs index 3cc36baf..2b24a1ae 100644 --- a/ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerFloodTests.cs +++ b/ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerFloodTests.cs @@ -831,6 +831,61 @@ public class PlayoutModeSchedulerFloodTests : SchedulerTestBase playoutItems[2].FillerKind.Should().Be(FillerKind.None); } + [Test] + public void Should_Not_Schedule_At_HardStop() + { + Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(55)); + + var scheduleItem = new ProgramScheduleItemFlood + { + Id = 1, + Index = 1, + Collection = collectionOne, + CollectionId = collectionOne.Id, + StartTime = TimeSpan.FromHours(6), + PlaybackOrder = PlaybackOrder.Chronological, + TailFiller = null, + FallbackFiller = null + }; + + var enumerator = new ChronologicalMediaCollectionEnumerator( + collectionOne.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); + (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( + startState, + CollectionEnumerators(scheduleItem, enumerator), + scheduleItem, + NextScheduleItem, + HardStop(scheduleItemsEnumerator)); + + playoutItems.Should().BeEmpty(); + + playoutBuilderState.CurrentTime.Should().Be(HardStop(scheduleItemsEnumerator)); + + playoutBuilderState.NextGuideGroup.Should().Be(1); + playoutBuilderState.DurationFinish.IsNone.Should().BeTrue(); + playoutBuilderState.InFlood.Should().BeFalse(); + playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); + playoutBuilderState.InDurationFiller.Should().BeFalse(); + playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0); + + enumerator.State.Index.Should().Be(0); + } + protected override ProgramScheduleItem NextScheduleItem => new ProgramScheduleItemOne { StartTime = TimeSpan.FromHours(3) diff --git a/ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerMultipleTests.cs b/ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerMultipleTests.cs index 6f936fa5..0eafb95d 100644 --- a/ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerMultipleTests.cs +++ b/ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerMultipleTests.cs @@ -664,6 +664,67 @@ public class PlayoutModeSchedulerMultipleTests : SchedulerTestBase playoutItems[2].FillerKind.Should().Be(FillerKind.None); } + [Test] + public void Should_Not_Schedule_At_HardStop() + { + Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(55)); + + var scheduleItem = new ProgramScheduleItemMultiple + { + Id = 1, + Index = 1, + Collection = collectionOne, + CollectionId = collectionOne.Id, + StartTime = TimeSpan.FromHours(6), + PlaybackOrder = PlaybackOrder.Chronological, + TailFiller = null, + FallbackFiller = null, + Count = 2 + }; + + var enumerator = new ChronologicalMediaCollectionEnumerator( + collectionOne.MediaItems, + new CollectionEnumeratorState()); + + var sortedScheduleItems = new List + { + scheduleItem, + NextScheduleItem + }; + + var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator( + sortedScheduleItems, + new CollectionEnumeratorState()); + + var collectionMediaItems = new Dictionary> + { + { CollectionKey.ForScheduleItem(scheduleItem), collectionOne.MediaItems } + }.ToMap(); + + PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); + + var scheduler = new PlayoutModeSchedulerMultiple(collectionMediaItems, new Mock().Object); + (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( + startState, + CollectionEnumerators(scheduleItem, enumerator), + scheduleItem, + NextScheduleItem, + HardStop(scheduleItemsEnumerator)); + + playoutItems.Should().BeEmpty(); + + playoutBuilderState.CurrentTime.Should().Be(HardStop(scheduleItemsEnumerator)); + + playoutBuilderState.NextGuideGroup.Should().Be(1); + playoutBuilderState.DurationFinish.IsNone.Should().BeTrue(); + playoutBuilderState.InFlood.Should().BeFalse(); + playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); + playoutBuilderState.InDurationFiller.Should().BeFalse(); + playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0); + + enumerator.State.Index.Should().Be(0); + } + protected override ProgramScheduleItem NextScheduleItem => new ProgramScheduleItemOne { StartTime = TimeSpan.FromHours(3) diff --git a/ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerOneTests.cs b/ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerOneTests.cs index bd168a31..ea8f628b 100644 --- a/ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerOneTests.cs +++ b/ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerOneTests.cs @@ -769,6 +769,61 @@ public class PlayoutModeSchedulerOneTests : SchedulerTestBase playoutItems[3].FillerKind.Should().Be(FillerKind.PostRoll); } + [Test] + public void Should_Not_Schedule_At_HardStop() + { + Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(55)); + + var scheduleItem = new ProgramScheduleItemOne + { + Id = 1, + Index = 1, + Collection = collectionOne, + CollectionId = collectionOne.Id, + StartTime = TimeSpan.FromHours(6), + PlaybackOrder = PlaybackOrder.Chronological, + TailFiller = null, + FallbackFiller = null + }; + + var enumerator = new ChronologicalMediaCollectionEnumerator( + collectionOne.MediaItems, + new CollectionEnumeratorState()); + + var sortedScheduleItems = new List + { + scheduleItem, + NextScheduleItem + }; + + var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator( + sortedScheduleItems, + new CollectionEnumeratorState()); + + PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); + + var scheduler = new PlayoutModeSchedulerOne(new Mock().Object); + (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( + startState, + CollectionEnumerators(scheduleItem, enumerator), + scheduleItem, + NextScheduleItem, + HardStop(scheduleItemsEnumerator)); + + playoutItems.Should().BeEmpty(); + + playoutBuilderState.CurrentTime.Should().Be(HardStop(scheduleItemsEnumerator)); + + playoutBuilderState.NextGuideGroup.Should().Be(1); + playoutBuilderState.DurationFinish.IsNone.Should().BeTrue(); + playoutBuilderState.InFlood.Should().BeFalse(); + playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); + playoutBuilderState.InDurationFiller.Should().BeFalse(); + playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0); + + enumerator.State.Index.Should().Be(0); + } + protected override ProgramScheduleItem NextScheduleItem => new ProgramScheduleItemOne { StartTime = TimeSpan.FromHours(3) diff --git a/ErsatzTV.Core/Scheduling/PlayoutBuilder.cs b/ErsatzTV.Core/Scheduling/PlayoutBuilder.cs index a02036bd..5446cb94 100644 --- a/ErsatzTV.Core/Scheduling/PlayoutBuilder.cs +++ b/ErsatzTV.Core/Scheduling/PlayoutBuilder.cs @@ -327,8 +327,25 @@ public class PlayoutBuilder : IPlayoutBuilder randomStartPoint); } - // remove any items outside the desired range - playout.Items.RemoveAll(old => old.FinishOffset < trimBefore || old.StartOffset > trimAfter); + // remove old items + playout.Items.RemoveAll(old => old.FinishOffset < trimBefore); + + // check for future items that aren't grouped inside range + var futureItems = playout.Items.Filter(i => i.StartOffset > trimAfter).ToList(); + foreach (PlayoutItem futureItem in futureItems) + { + if (playout.Items.All(i => i == futureItem || i.GuideGroup != futureItem.GuideGroup)) + { + _logger.LogError( + "Playout item scheduled for {Time} after hard stop of {HardStop}", + futureItem.StartOffset, + trimAfter); + + // it feels hacky to have to clean up a playlist like this, + // so only log the error, and leave the bad data to fail tests + // playout.Items.Remove(futureItem); + } + } return playout; } @@ -409,6 +426,8 @@ public class PlayoutBuilder : IPlayoutBuilder // loop until we're done filling the desired amount of time while (playoutBuilderState.CurrentTime < playoutFinish) { + _logger.LogDebug("Playout time is {CurrentTime}", playoutBuilderState.CurrentTime); + // get the schedule item out of the sorted list ProgramScheduleItem scheduleItem = playoutBuilderState.ScheduleItemsEnumerator.Current; diff --git a/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerDuration.cs b/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerDuration.cs index 5eafbb45..5c489255 100644 --- a/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerDuration.cs +++ b/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerDuration.cs @@ -35,6 +35,12 @@ public class PlayoutModeSchedulerDuration : PlayoutModeSchedulerBase= hardStop) + { + nextState = nextState with { CurrentTime = hardStop }; + break; + } + // remember when we need to finish this duration item if (nextState.DurationFinish.IsNone) { diff --git a/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerFlood.cs b/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerFlood.cs index 85ab481d..7906622b 100644 --- a/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerFlood.cs +++ b/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerFlood.cs @@ -30,12 +30,21 @@ public class PlayoutModeSchedulerFlood : PlayoutModeSchedulerBase= hardStop) + { + scheduledNone = playoutItems.Count == 0; + nextState = nextState with { CurrentTime = hardStop }; + break; + } + TimeSpan itemDuration = DurationForMediaItem(mediaItem); List itemChapters = ChaptersForMediaItem(mediaItem); @@ -112,7 +121,7 @@ public class PlayoutModeSchedulerFlood : PlayoutModeSchedulerBase= hardStop, + InFlood = playoutItems.Any() && nextState.CurrentTime >= hardStop, // only decrement guide group if it was bumped NextGuideGroup = playoutItems.Select(pi => pi.GuideGroup).Distinct().Count() != 1 @@ -121,7 +130,7 @@ public class PlayoutModeSchedulerFlood : PlayoutModeSchedulerBase(); + DateTimeOffset firstStart = GetStartTimeAfter(playoutBuilderState, scheduleItem); + if (firstStart >= hardStop) + { + playoutBuilderState = playoutBuilderState with { CurrentTime = hardStop }; + return Tuple(playoutBuilderState, playoutItems); + } + PlayoutBuilderState nextState = playoutBuilderState with { MultipleRemaining = playoutBuilderState.MultipleRemaining.IfNone(scheduleItem.Count) diff --git a/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerOne.cs b/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerOne.cs index 19dfe5d8..711d0e07 100644 --- a/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerOne.cs +++ b/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerOne.cs @@ -27,6 +27,12 @@ public class PlayoutModeSchedulerOne : PlayoutModeSchedulerBase= hardStop) + { + playoutBuilderState = playoutBuilderState with { CurrentTime = hardStop }; + break; + } + TimeSpan itemDuration = DurationForMediaItem(mediaItem); List itemChapters = ChaptersForMediaItem(mediaItem);