From 6c06fbe62136f30cc6f273c08b0dff3c42a4751a Mon Sep 17 00:00:00 2001 From: Jason Dove Date: Fri, 4 Mar 2022 21:40:08 -0600 Subject: [PATCH] fix mid-roll filler scheduling bug (#675) --- CHANGELOG.md | 2 + .../PlayoutModeSchedulerBaseTests.cs | 419 +++++++++++------- .../Scheduling/PlayoutModeSchedulerBase.cs | 10 +- 3 files changed, 265 insertions(+), 166 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d972bc310..6b14b6339 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Fix playback of interlaced mpeg2video content with NVIDIA acceleration - Fix playback of all interlaced content with QSV acceleration - Fix adding songs to collections from search results page +- Fix bug scheduling mid-roll filler with content that contains one chapter + - No mid-roll filler will be inserted for content with zero or one chapters ### Added - Add automated error reporting via Bugsnag diff --git a/ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerBaseTests.cs b/ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerBaseTests.cs index 443cdb1ae..992215d9e 100644 --- a/ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerBaseTests.cs +++ b/ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerBaseTests.cs @@ -3,190 +3,287 @@ 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; namespace ErsatzTV.Core.Tests.Scheduling; [TestFixture] -public class PlayoutModeSchedulerBaseTests +public class PlayoutModeSchedulerBaseTests : SchedulerTestBase { - [Test] - public void CalculateEndTimeWithFiller_Should_Not_Touch_Enumerator() + [TestFixture] + public class CalculateEndTimeWithFiller { - var collection = new Collection + [Test] + public void Should_Not_Touch_Enumerator() { - Id = 1, - Name = "Filler Items", - MediaItems = new List() - }; + var collection = new Collection + { + Id = 1, + Name = "Filler Items", + MediaItems = new List() + }; - for (var i = 0; i < 5; i++) - { - collection.MediaItems.Add(TestMovie(i + 1, TimeSpan.FromHours(i + 1), new DateTime(2020, 2, i + 1))); + for (var i = 0; i < 5; i++) + { + collection.MediaItems.Add(TestMovie(i + 1, TimeSpan.FromHours(i + 1), new DateTime(2020, 2, i + 1))); + } + + var fillerPreset = new FillerPreset + { + FillerKind = FillerKind.PreRoll, + FillerMode = FillerMode.Count, + Count = 3, + Collection = collection, + CollectionId = collection.Id + }; + + var enumerator = new ChronologicalMediaCollectionEnumerator( + collection.MediaItems, + new CollectionEnumeratorState { Index = 0, Seed = 1 }); + + DateTimeOffset result = PlayoutModeSchedulerBase + .CalculateEndTimeWithFiller( + new Dictionary + { + { CollectionKey.ForFillerPreset(fillerPreset), enumerator } + }, + new ProgramScheduleItemOne + { + PreRollFiller = fillerPreset + }, + new DateTimeOffset(2020, 2, 1, 12, 0, 0, TimeSpan.FromHours(-5)), + new TimeSpan(0, 12, 30), + new List()); + + result.Should().Be(new DateTimeOffset(2020, 2, 1, 18, 12, 30, TimeSpan.FromHours(-5))); + enumerator.State.Index.Should().Be(0); + enumerator.State.Seed.Should().Be(1); } - var fillerPreset = new FillerPreset + [Test] + public void Should_Pad_To_15_Minutes_15() { - FillerKind = FillerKind.PreRoll, - FillerMode = FillerMode.Count, - Count = 3, - Collection = collection, - CollectionId = collection.Id - }; + DateTimeOffset result = PlayoutModeSchedulerBase + .CalculateEndTimeWithFiller( + new Dictionary(), + new ProgramScheduleItemOne + { + MidRollFiller = new FillerPreset + { + FillerKind = FillerKind.MidRoll, + FillerMode = FillerMode.Pad, + PadToNearestMinute = 15 + } + }, + new DateTimeOffset(2020, 2, 1, 12, 0, 0, TimeSpan.FromHours(-5)), + new TimeSpan(0, 12, 30), + new List()); - var enumerator = new ChronologicalMediaCollectionEnumerator( - collection.MediaItems, - new CollectionEnumeratorState { Index = 0, Seed = 1 }); + result.Should().Be(new DateTimeOffset(2020, 2, 1, 12, 15, 0, TimeSpan.FromHours(-5))); + } - DateTimeOffset result = PlayoutModeSchedulerBase - .CalculateEndTimeWithFiller( - new Dictionary - { - { CollectionKey.ForFillerPreset(fillerPreset), enumerator } - }, - new ProgramScheduleItemOne - { - PreRollFiller = fillerPreset - }, - new DateTimeOffset(2020, 2, 1, 12, 0, 0, TimeSpan.FromHours(-5)), - new TimeSpan(0, 12, 30), - new List()); - - result.Should().Be(new DateTimeOffset(2020, 2, 1, 18, 12, 30, TimeSpan.FromHours(-5))); - enumerator.State.Index.Should().Be(0); - enumerator.State.Seed.Should().Be(1); - } + [Test] + public void Should_Pad_To_15_Minutes_30() + { + DateTimeOffset result = PlayoutModeSchedulerBase + .CalculateEndTimeWithFiller( + new Dictionary(), + new ProgramScheduleItemOne + { + MidRollFiller = new FillerPreset + { + FillerKind = FillerKind.MidRoll, + FillerMode = FillerMode.Pad, + PadToNearestMinute = 15 + } + }, + new DateTimeOffset(2020, 2, 1, 12, 16, 0, TimeSpan.FromHours(-5)), + new TimeSpan(0, 12, 30), + new List()); - [Test] - public void CalculateEndTimeWithFiller_Should_Pad_To_15_Minutes_15() - { - DateTimeOffset result = PlayoutModeSchedulerBase - .CalculateEndTimeWithFiller( - new Dictionary(), - new ProgramScheduleItemOne - { - MidRollFiller = new FillerPreset + result.Should().Be(new DateTimeOffset(2020, 2, 1, 12, 30, 0, TimeSpan.FromHours(-5))); + } + + [Test] + public void Should_Pad_To_15_Minutes_45() + { + DateTimeOffset result = PlayoutModeSchedulerBase + .CalculateEndTimeWithFiller( + new Dictionary(), + new ProgramScheduleItemOne { - FillerKind = FillerKind.MidRoll, - FillerMode = FillerMode.Pad, - PadToNearestMinute = 15 - } - }, - new DateTimeOffset(2020, 2, 1, 12, 0, 0, TimeSpan.FromHours(-5)), - new TimeSpan(0, 12, 30), - new List()); - - result.Should().Be(new DateTimeOffset(2020, 2, 1, 12, 15, 0, TimeSpan.FromHours(-5))); - } - - [Test] - public void CalculateEndTimeWithFiller_Should_Pad_To_15_Minutes_30() - { - DateTimeOffset result = PlayoutModeSchedulerBase - .CalculateEndTimeWithFiller( - new Dictionary(), - new ProgramScheduleItemOne - { - MidRollFiller = new FillerPreset + MidRollFiller = new FillerPreset + { + FillerKind = FillerKind.MidRoll, + FillerMode = FillerMode.Pad, + PadToNearestMinute = 15 + } + }, + new DateTimeOffset(2020, 2, 1, 12, 30, 0, TimeSpan.FromHours(-5)), + new TimeSpan(0, 12, 30), + new List()); + + result.Should().Be(new DateTimeOffset(2020, 2, 1, 12, 45, 0, TimeSpan.FromHours(-5))); + } + + [Test] + public void Should_Pad_To_15_Minutes_00() + { + DateTimeOffset result = PlayoutModeSchedulerBase + .CalculateEndTimeWithFiller( + new Dictionary(), + new ProgramScheduleItemOne { - FillerKind = FillerKind.MidRoll, - FillerMode = FillerMode.Pad, - PadToNearestMinute = 15 - } - }, - new DateTimeOffset(2020, 2, 1, 12, 16, 0, TimeSpan.FromHours(-5)), - new TimeSpan(0, 12, 30), - new List()); - - result.Should().Be(new DateTimeOffset(2020, 2, 1, 12, 30, 0, TimeSpan.FromHours(-5))); - } - - [Test] - public void CalculateEndTimeWithFiller_Should_Pad_To_15_Minutes_45() - { - DateTimeOffset result = PlayoutModeSchedulerBase - .CalculateEndTimeWithFiller( - new Dictionary(), - new ProgramScheduleItemOne - { - MidRollFiller = new FillerPreset + MidRollFiller = new FillerPreset + { + FillerKind = FillerKind.MidRoll, + FillerMode = FillerMode.Pad, + PadToNearestMinute = 15 + } + }, + new DateTimeOffset(2020, 2, 1, 12, 46, 0, TimeSpan.FromHours(-5)), + new TimeSpan(0, 12, 30), + new List()); + + result.Should().Be(new DateTimeOffset(2020, 2, 1, 13, 0, 0, TimeSpan.FromHours(-5))); + } + + [Test] + public void Should_Pad_To_30_Minutes_30() + { + DateTimeOffset result = PlayoutModeSchedulerBase + .CalculateEndTimeWithFiller( + new Dictionary(), + new ProgramScheduleItemOne { - FillerKind = FillerKind.MidRoll, - FillerMode = FillerMode.Pad, - PadToNearestMinute = 15 - } - }, - new DateTimeOffset(2020, 2, 1, 12, 30, 0, TimeSpan.FromHours(-5)), - new TimeSpan(0, 12, 30), - new List()); - - result.Should().Be(new DateTimeOffset(2020, 2, 1, 12, 45, 0, TimeSpan.FromHours(-5))); - } - - [Test] - public void CalculateEndTimeWithFiller_Should_Pad_To_15_Minutes_00() - { - DateTimeOffset result = PlayoutModeSchedulerBase - .CalculateEndTimeWithFiller( - new Dictionary(), - new ProgramScheduleItemOne - { - MidRollFiller = new FillerPreset + MidRollFiller = new FillerPreset + { + FillerKind = FillerKind.MidRoll, + FillerMode = FillerMode.Pad, + PadToNearestMinute = 30 + } + }, + new DateTimeOffset(2020, 2, 1, 12, 0, 0, TimeSpan.FromHours(-5)), + new TimeSpan(0, 12, 30), + new List()); + + result.Should().Be(new DateTimeOffset(2020, 2, 1, 12, 30, 0, TimeSpan.FromHours(-5))); + } + + [Test] + public void Should_Pad_To_30_Minutes_00() + { + DateTimeOffset result = PlayoutModeSchedulerBase + .CalculateEndTimeWithFiller( + new Dictionary(), + new ProgramScheduleItemOne { - FillerKind = FillerKind.MidRoll, - FillerMode = FillerMode.Pad, - PadToNearestMinute = 15 - } - }, - new DateTimeOffset(2020, 2, 1, 12, 46, 0, TimeSpan.FromHours(-5)), - new TimeSpan(0, 12, 30), - new List()); - - result.Should().Be(new DateTimeOffset(2020, 2, 1, 13, 0, 0, TimeSpan.FromHours(-5))); + MidRollFiller = new FillerPreset + { + FillerKind = FillerKind.MidRoll, + FillerMode = FillerMode.Pad, + PadToNearestMinute = 30 + } + }, + new DateTimeOffset(2020, 2, 1, 12, 20, 0, TimeSpan.FromHours(-5)), + new TimeSpan(0, 12, 30), + new List()); + + result.Should().Be(new DateTimeOffset(2020, 2, 1, 13, 0, 0, TimeSpan.FromHours(-5))); + } } - - [Test] - public void CalculateEndTimeWithFiller_Should_Pad_To_30_Minutes_30() + + [TestFixture] + public class AddFiller { - DateTimeOffset result = PlayoutModeSchedulerBase - .CalculateEndTimeWithFiller( - new Dictionary(), - new ProgramScheduleItemOne + [Test] + public void Should_Not_Crash_Mid_Roll_Zero_Chapters() + { + Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromHours(1)); + + var scheduleItem = new ProgramScheduleItemOne + { + MidRollFiller = new FillerPreset { - MidRollFiller = new FillerPreset - { - FillerKind = FillerKind.MidRoll, - FillerMode = FillerMode.Pad, - PadToNearestMinute = 30 - } - }, - new DateTimeOffset(2020, 2, 1, 12, 0, 0, TimeSpan.FromHours(-5)), - new TimeSpan(0, 12, 30), - new List()); - - result.Should().Be(new DateTimeOffset(2020, 2, 1, 12, 30, 0, TimeSpan.FromHours(-5))); - } - - [Test] - public void CalculateEndTimeWithFiller_Should_Pad_To_30_Minutes_00() - { - DateTimeOffset result = PlayoutModeSchedulerBase - .CalculateEndTimeWithFiller( - new Dictionary(), - new ProgramScheduleItemOne + FillerKind = FillerKind.MidRoll, + FillerMode = FillerMode.Pad, + PadToNearestMinute = 15 + } + }; + + var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator( + new List { scheduleItem }, + new CollectionEnumeratorState()); + + var enumerator = new ChronologicalMediaCollectionEnumerator( + collectionOne.MediaItems, + new CollectionEnumeratorState()); + + PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); + + List playoutItems = PlayoutModeSchedulerBase + .AddFiller( + startState, + CollectionEnumerators(scheduleItem, enumerator), + scheduleItem, + new PlayoutItem(), + new List()); + + playoutItems.Count.Should().Be(1); + } + + [Test] + public void Should_Not_Crash_Mid_Roll_One_Chapter() + { + Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromHours(1)); + + 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 { - MidRollFiller = new FillerPreset - { - FillerKind = FillerKind.MidRoll, - FillerMode = FillerMode.Pad, - PadToNearestMinute = 30 - } - }, - new DateTimeOffset(2020, 2, 1, 12, 20, 0, TimeSpan.FromHours(-5)), - new TimeSpan(0, 12, 30), - new List()); - - result.Should().Be(new DateTimeOffset(2020, 2, 1, 13, 0, 0, TimeSpan.FromHours(-5))); + FillerKind = FillerKind.MidRoll, + FillerMode = FillerMode.Pad, + PadToNearestMinute = 15 + } + }; + + var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator( + new List { scheduleItem }, + new CollectionEnumeratorState()); + + var enumerator = new ChronologicalMediaCollectionEnumerator( + collectionOne.MediaItems, + new CollectionEnumeratorState()); + + PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); + + Dictionary enumerators = CollectionEnumerators( + scheduleItem, + enumerator); + + // too lazy to make another enumerator for the filler that we don't want + enumerators.Add(CollectionKey.ForFillerPreset(scheduleItem.MidRollFiller), enumerator); + + List playoutItems = PlayoutModeSchedulerBase + .AddFiller( + startState, + enumerators, + scheduleItem, + new PlayoutItem(), + new List { new() }); + + playoutItems.Count.Should().Be(1); + } } private static Movie TestMovie(int id, TimeSpan duration, DateTime aired) => diff --git a/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerBase.cs b/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerBase.cs index ca91c10d4..3001910bf 100644 --- a/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerBase.cs +++ b/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerBase.cs @@ -310,7 +310,7 @@ public abstract class PlayoutModeSchedulerBase : IPlayoutModeScheduler whe return itemStartTime + totalDuration; } - protected List AddFiller( + internal static List AddFiller( PlayoutBuilderState playoutBuilderState, Dictionary enumerators, ProgramScheduleItem scheduleItem, @@ -333,7 +333,7 @@ public abstract class PlayoutModeSchedulerBase : IPlayoutModeScheduler whe } List effectiveChapters = chapters; - if (allFiller.All(fp => fp.FillerKind != FillerKind.MidRoll)) + if (allFiller.All(fp => fp.FillerKind != FillerKind.MidRoll) || effectiveChapters.Count <= 1) { effectiveChapters = new List(); } @@ -356,7 +356,7 @@ public abstract class PlayoutModeSchedulerBase : IPlayoutModeScheduler whe } } - if (!effectiveChapters.Any()) + if (effectiveChapters.Count <= 1) { result.Add(playoutItem); } @@ -493,7 +493,7 @@ public abstract class PlayoutModeSchedulerBase : IPlayoutModeScheduler whe ? remainingToFill : remainingToFill / (effectiveChapters.Count - 1); TimeSpan filled = TimeSpan.Zero; - for (var i = 0; i < chapters.Count; i++) + for (var i = 0; i < effectiveChapters.Count; i++) { result.Add(playoutItem.ForChapter(effectiveChapters[i])); if (i < effectiveChapters.Count - 1) @@ -650,7 +650,7 @@ public abstract class PlayoutModeSchedulerBase : IPlayoutModeScheduler whe return result; } - private Option FallbackFillerForPad( + private static Option FallbackFillerForPad( PlayoutBuilderState playoutBuilderState, Dictionary enumerators, ProgramScheduleItem scheduleItem,