From e8cbcc935fb34fc36670e0f869a4cf60028650b3 Mon Sep 17 00:00:00 2001 From: Jason Dove <1695733+jasongdove@users.noreply.github.com> Date: Wed, 14 Jun 2023 15:54:41 -0500 Subject: [PATCH] rework pad and duration filler (#1304) --- CHANGELOG.md | 5 + .../PlayoutModeSchedulerBaseTests.cs | 188 +----------------- .../ShuffledMediaCollectionEnumeratorTests.cs | 92 --------- .../Domain/CollectionEnumeratorState.cs | 1 + .../Scheduling/IMediaCollectionEnumerator.cs | 3 +- .../ChronologicalMediaCollectionEnumerator.cs | 10 +- .../CustomOrderCollectionEnumerator.cs | 16 +- .../Scheduling/PlayoutModeSchedulerBase.cs | 175 +++------------- .../PlayoutModeSchedulerDuration.cs | 42 ++-- .../Scheduling/PlayoutModeSchedulerFlood.cs | 44 ++-- .../PlayoutModeSchedulerMultiple.cs | 9 +- .../Scheduling/PlayoutModeSchedulerOne.cs | 10 +- .../RandomizedMediaCollectionEnumerator.cs | 10 +- .../SeasonEpisodeMediaCollectionEnumerator.cs | 10 +- .../ShuffleInOrderCollectionEnumerator.cs | 9 +- .../ShuffledMediaCollectionEnumerator.cs | 35 +--- ...MultiEpisodeShuffleCollectionEnumerator.cs | 45 ++--- 17 files changed, 156 insertions(+), 548 deletions(-) delete mode 100644 ErsatzTV.Core.Tests/Scheduling/ShuffledMediaCollectionEnumeratorTests.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 3099789f..9642e616 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Fixed - Skip checking for subtitles to extract when subtitles are not enabled on a channel/schedule item +### Changed +- For `Pad` and `Duration` filler - prioritize filling the configured pad/duration + - This will skip filler that is too long in an attempt to avoid unscheduled time + - You may see the same filler more often, which means you may want to add more filler to your library so ETV has more options + ## [0.7.9-beta] - 2023-06-10 ### Added - Synchronize actor metadata from Jellyfin and Emby television libraries diff --git a/ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerBaseTests.cs b/ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerBaseTests.cs index 5a7eee80..2a4c742e 100644 --- a/ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerBaseTests.cs +++ b/ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerBaseTests.cs @@ -23,189 +23,6 @@ public class PlayoutModeSchedulerBaseTests : SchedulerTestBase private CancellationToken _cancellationToken; private PlayoutModeSchedulerBase _scheduler; - [TestFixture] - public class CalculateEndTimeWithFiller : PlayoutModeSchedulerBaseTests - { - [Test] - public void Should_Not_Touch_Enumerator() - { - 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))); - } - - 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); - } - - [Test] - public void Should_Pad_To_15_Minutes_15() - { - 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()); - - result.Should().Be(new DateTimeOffset(2020, 2, 1, 12, 15, 0, TimeSpan.FromHours(-5))); - } - - [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()); - - 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 - { - 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 - { - 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 - { - 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 - { - 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))); - } - } - [TestFixture] public class AddFiller : PlayoutModeSchedulerBaseTests { @@ -241,6 +58,7 @@ public class PlayoutModeSchedulerBaseTests : SchedulerTestBase scheduleItem, new PlayoutItem(), new List(), + log: true, _cancellationToken); playoutItems.Count.Should().Be(1); @@ -293,6 +111,7 @@ public class PlayoutModeSchedulerBaseTests : SchedulerTestBase scheduleItem, new PlayoutItem(), new List { new() }, + log: true, _cancellationToken); playoutItems.Count.Should().Be(1); @@ -359,6 +178,7 @@ public class PlayoutModeSchedulerBaseTests : SchedulerTestBase new() { StartTime = TimeSpan.Zero, EndTime = TimeSpan.FromMinutes(6) }, new() { StartTime = TimeSpan.FromMinutes(6), EndTime = TimeSpan.FromMinutes(60) } }, + log: true, _cancellationToken); playoutItems.Count.Should().Be(3); @@ -450,6 +270,7 @@ public class PlayoutModeSchedulerBaseTests : SchedulerTestBase new() { StartTime = TimeSpan.Zero, EndTime = TimeSpan.FromMinutes(6) }, new() { StartTime = TimeSpan.FromMinutes(6), EndTime = TimeSpan.FromMinutes(45) } }, + log: true, _cancellationToken); playoutItems.Count.Should().Be(5); @@ -555,6 +376,7 @@ public class PlayoutModeSchedulerBaseTests : SchedulerTestBase new() { StartTime = TimeSpan.Zero, EndTime = TimeSpan.FromMinutes(6) }, new() { StartTime = TimeSpan.FromMinutes(6), EndTime = TimeSpan.FromMinutes(45) } }, + log: true, _cancellationToken); playoutItems.Count.Should().Be(5); diff --git a/ErsatzTV.Core.Tests/Scheduling/ShuffledMediaCollectionEnumeratorTests.cs b/ErsatzTV.Core.Tests/Scheduling/ShuffledMediaCollectionEnumeratorTests.cs deleted file mode 100644 index 30fa546b..00000000 --- a/ErsatzTV.Core.Tests/Scheduling/ShuffledMediaCollectionEnumeratorTests.cs +++ /dev/null @@ -1,92 +0,0 @@ -using ErsatzTV.Core.Domain; -using ErsatzTV.Core.Scheduling; -using FluentAssertions; -using LanguageExt.UnsafeValueAccess; -using NUnit.Framework; - -namespace ErsatzTV.Core.Tests.Scheduling; - -[TestFixture] -public class ShuffledMediaCollectionEnumeratorTests -{ - [SetUp] - public void SetUp() => _cancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(10)).Token; - - private readonly List _mediaItems = new() - { - new GroupedMediaItem(new MediaItem { Id = 1 }, new List()), - new GroupedMediaItem(new MediaItem { Id = 2 }, new List()), - new GroupedMediaItem(new MediaItem { Id = 3 }, new List()) - }; - - private CancellationToken _cancellationToken; - - [Test] - public void Peek_Zero_Should_Match_Current() - { - var state = new CollectionEnumeratorState { Index = 0, Seed = 0 }; - var enumerator = new ShuffledMediaCollectionEnumerator(_mediaItems, state, _cancellationToken); - - Option peek = enumerator.Peek(0); - Option current = enumerator.Current; - - peek.IsSome.Should().BeTrue(); - current.IsSome.Should().BeTrue(); - peek.ValueUnsafe().Id.Should().Be(1); - current.ValueUnsafe().Id.Should().Be(1); - } - - [Test] - public void Peek_One_Should_Match_Next() - { - var state = new CollectionEnumeratorState { Index = 0, Seed = 0 }; - var enumerator = new ShuffledMediaCollectionEnumerator(_mediaItems, state, _cancellationToken); - - Option peek = enumerator.Peek(1); - - enumerator.MoveNext(); - Option next = enumerator.Current; - - peek.IsSome.Should().BeTrue(); - next.IsSome.Should().BeTrue(); - peek.ValueUnsafe().Id.Should().Be(2); - next.ValueUnsafe().Id.Should().Be(2); - } - - [Test] - public void Peek_Two_Should_Match_NextNext() - { - var state = new CollectionEnumeratorState { Index = 0, Seed = 0 }; - var enumerator = new ShuffledMediaCollectionEnumerator(_mediaItems, state, _cancellationToken); - - Option peek = enumerator.Peek(2); - - enumerator.MoveNext(); - enumerator.MoveNext(); - Option next = enumerator.Current; - - peek.IsSome.Should().BeTrue(); - next.IsSome.Should().BeTrue(); - peek.ValueUnsafe().Id.Should().Be(3); - next.ValueUnsafe().Id.Should().Be(3); - } - - [Test] - public void Peek_Three_Should_Match_NextNextNext() - { - var state = new CollectionEnumeratorState { Index = 0, Seed = 0 }; - var enumerator = new ShuffledMediaCollectionEnumerator(_mediaItems, state, _cancellationToken); - - Option peek = enumerator.Peek(3); - - enumerator.MoveNext(); - enumerator.MoveNext(); - enumerator.MoveNext(); - Option next = enumerator.Current; - - peek.IsSome.Should().BeTrue(); - next.IsSome.Should().BeTrue(); - peek.ValueUnsafe().Id.Should().Be(2); - next.ValueUnsafe().Id.Should().Be(2); - } -} diff --git a/ErsatzTV.Core/Domain/CollectionEnumeratorState.cs b/ErsatzTV.Core/Domain/CollectionEnumeratorState.cs index cf1c082a..167632d1 100644 --- a/ErsatzTV.Core/Domain/CollectionEnumeratorState.cs +++ b/ErsatzTV.Core/Domain/CollectionEnumeratorState.cs @@ -4,4 +4,5 @@ public class CollectionEnumeratorState { public int Seed { get; set; } public int Index { get; set; } + public CollectionEnumeratorState Clone() => new CollectionEnumeratorState { Seed = Seed, Index = Index }; } diff --git a/ErsatzTV.Core/Interfaces/Scheduling/IMediaCollectionEnumerator.cs b/ErsatzTV.Core/Interfaces/Scheduling/IMediaCollectionEnumerator.cs index ab9e53c0..bfa5c909 100644 --- a/ErsatzTV.Core/Interfaces/Scheduling/IMediaCollectionEnumerator.cs +++ b/ErsatzTV.Core/Interfaces/Scheduling/IMediaCollectionEnumerator.cs @@ -4,9 +4,10 @@ namespace ErsatzTV.Core.Interfaces.Scheduling; public interface IMediaCollectionEnumerator { + IMediaCollectionEnumerator Clone(CollectionEnumeratorState state, CancellationToken cancellationToken); CollectionEnumeratorState State { get; } Option Current { get; } void MoveNext(); - Option Peek(int offset); + int Count { get; } Option MinimumDuration { get; } } diff --git a/ErsatzTV.Core/Scheduling/ChronologicalMediaCollectionEnumerator.cs b/ErsatzTV.Core/Scheduling/ChronologicalMediaCollectionEnumerator.cs index 4a6fb58e..be6ac93d 100644 --- a/ErsatzTV.Core/Scheduling/ChronologicalMediaCollectionEnumerator.cs +++ b/ErsatzTV.Core/Scheduling/ChronologicalMediaCollectionEnumerator.cs @@ -31,14 +31,18 @@ public sealed class ChronologicalMediaCollectionEnumerator : IMediaCollectionEnu } } + public IMediaCollectionEnumerator Clone(CollectionEnumeratorState state, CancellationToken cancellationToken) + { + return new ChronologicalMediaCollectionEnumerator(_sortedMediaItems, state); + } + public CollectionEnumeratorState State { get; } public Option Current => _sortedMediaItems.Any() ? _sortedMediaItems[State.Index] : None; public void MoveNext() => State.Index = (State.Index + 1) % _sortedMediaItems.Count; - public Option Peek(int offset) => - _sortedMediaItems.Any() ? _sortedMediaItems[(State.Index + offset) % _sortedMediaItems.Count] : None; - public Option MinimumDuration => _lazyMinimumDuration.Value; + + public int Count => _sortedMediaItems.Count; } diff --git a/ErsatzTV.Core/Scheduling/CustomOrderCollectionEnumerator.cs b/ErsatzTV.Core/Scheduling/CustomOrderCollectionEnumerator.cs index 9f3e7dd2..d9d6fb3a 100644 --- a/ErsatzTV.Core/Scheduling/CustomOrderCollectionEnumerator.cs +++ b/ErsatzTV.Core/Scheduling/CustomOrderCollectionEnumerator.cs @@ -6,6 +6,9 @@ namespace ErsatzTV.Core.Scheduling; public class CustomOrderCollectionEnumerator : IMediaCollectionEnumerator { + private readonly Collection _collection; + private readonly IList _mediaItems; + private readonly IList _sortedMediaItems; private readonly Lazy> _lazyMinimumDuration; @@ -14,6 +17,9 @@ public class CustomOrderCollectionEnumerator : IMediaCollectionEnumerator IList mediaItems, CollectionEnumeratorState state) { + _collection = collection; + _mediaItems = mediaItems; + // TODO: this will break if we allow shows and seasons _sortedMediaItems = collection.CollectionItems .OrderBy(ci => ci.CustomIndex) @@ -29,14 +35,18 @@ public class CustomOrderCollectionEnumerator : IMediaCollectionEnumerator } } + public IMediaCollectionEnumerator Clone(CollectionEnumeratorState state, CancellationToken cancellationToken) + { + return new CustomOrderCollectionEnumerator(_collection, _mediaItems, state); + } + public CollectionEnumeratorState State { get; } public Option Current => _sortedMediaItems.Any() ? _sortedMediaItems[State.Index] : None; public void MoveNext() => State.Index = (State.Index + 1) % _sortedMediaItems.Count; - public Option Peek(int offset) => - throw new NotSupportedException(); - public Option MinimumDuration => _lazyMinimumDuration.Value; + + public int Count => _sortedMediaItems.Count; } diff --git a/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerBase.cs b/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerBase.cs index ed0ff8ac..2590e86f 100644 --- a/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerBase.cs +++ b/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerBase.cs @@ -4,6 +4,8 @@ using ErsatzTV.Core.Extensions; using ErsatzTV.Core.Interfaces.Scheduling; using LanguageExt.UnsafeValueAccess; using Microsoft.Extensions.Logging; +using Serilog; +using ILogger = Microsoft.Extensions.Logging.ILogger; namespace ErsatzTV.Core.Scheduling; @@ -195,142 +197,13 @@ public abstract class PlayoutModeSchedulerBase : IPlayoutModeScheduler whe PlayoutBuilder.DisplayTitle(mediaItem), startTime); - internal static DateTimeOffset CalculateEndTimeWithFiller( - Dictionary enumerators, - ProgramScheduleItem scheduleItem, - DateTimeOffset itemStartTime, - TimeSpan itemDuration, - List chapters) - { - var allFiller = Optional(scheduleItem.PreRollFiller) - .Append(Optional(scheduleItem.MidRollFiller)) - .Append(Optional(scheduleItem.PostRollFiller)) - .ToList(); - - // multiple pad-to-nearest-minute values are invalid; use no filler - if (allFiller.Count(f => f.FillerMode == FillerMode.Pad && f.PadToNearestMinute.HasValue) > 1) - { - return itemStartTime + itemDuration; - } - - TimeSpan totalDuration = itemDuration; - foreach (FillerPreset filler in allFiller) - { - switch (filler.FillerKind, filler.FillerMode) - { - case (FillerKind.MidRoll, FillerMode.Duration) when filler.Duration.HasValue: - IMediaCollectionEnumerator mrde = enumerators[CollectionKey.ForFillerPreset(filler)]; - var mrdePeekOffset = 0; - for (var i = 0; i < chapters.Count - 1; i++) - { - TimeSpan midRollDuration = filler.Duration.Value; - while (mrde.Peek(mrdePeekOffset)) - { - foreach (MediaItem mediaItem in mrde.Peek(mrdePeekOffset)) - { - TimeSpan currentDuration = DurationForMediaItem(mediaItem); - midRollDuration -= currentDuration; - if (midRollDuration >= TimeSpan.Zero) - { - totalDuration += currentDuration; - mrdePeekOffset++; - } - } - - if (midRollDuration < TimeSpan.Zero) - { - break; - } - } - } - - break; - case (FillerKind.MidRoll, FillerMode.Count) when filler.Count.HasValue: - IMediaCollectionEnumerator mrce = enumerators[CollectionKey.ForFillerPreset(filler)]; - var mrcePeekOffset = 0; - for (var i = 0; i < chapters.Count - 1; i++) - { - for (var j = 0; j < filler.Count.Value; j++) - { - foreach (MediaItem mediaItem in mrce.Peek(mrcePeekOffset)) - { - totalDuration += DurationForMediaItem(mediaItem); - mrcePeekOffset++; - } - } - } - - break; - case (_, FillerMode.Duration) when filler.Duration.HasValue: - IMediaCollectionEnumerator e1 = enumerators[CollectionKey.ForFillerPreset(filler)]; - var peekOffset1 = 0; - TimeSpan duration = filler.Duration.Value; - while (e1.Peek(peekOffset1).IsSome) - { - foreach (MediaItem mediaItem in e1.Peek(peekOffset1)) - { - TimeSpan currentDuration = DurationForMediaItem(mediaItem); - duration -= currentDuration; - if (duration >= TimeSpan.Zero) - { - totalDuration += currentDuration; - peekOffset1++; - } - } - - if (duration < TimeSpan.Zero) - { - break; - } - } - - break; - case (_, FillerMode.Count) when filler.Count.HasValue: - IMediaCollectionEnumerator e2 = enumerators[CollectionKey.ForFillerPreset(filler)]; - var peekOffset2 = 0; - for (var i = 0; i < filler.Count.Value; i++) - { - foreach (MediaItem mediaItem in e2.Peek(peekOffset2)) - { - totalDuration += DurationForMediaItem(mediaItem); - peekOffset2++; - } - } - - break; - } - } - - 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, - targetTime.Day, - targetTime.Hour, - targetTime.Minute, - 0, - targetTime.Offset); - } - - return itemStartTime + totalDuration; - } - internal List AddFiller( PlayoutBuilderState playoutBuilderState, Dictionary enumerators, ProgramScheduleItem scheduleItem, PlayoutItem playoutItem, List chapters, + bool log, CancellationToken cancellationToken) { var result = new List(); @@ -367,6 +240,7 @@ public abstract class PlayoutModeSchedulerBase : IPlayoutModeScheduler whe filler.Duration.Value, FillerKind.PreRoll, filler.AllowWatermarks, + log, cancellationToken)); break; case FillerMode.Count when filler.Count.HasValue: @@ -408,6 +282,7 @@ public abstract class PlayoutModeSchedulerBase : IPlayoutModeScheduler whe filler.Duration.Value, FillerKind.MidRoll, filler.AllowWatermarks, + log, cancellationToken)); } } @@ -450,6 +325,7 @@ public abstract class PlayoutModeSchedulerBase : IPlayoutModeScheduler whe filler.Duration.Value, FillerKind.PostRoll, filler.AllowWatermarks, + log, cancellationToken)); break; case FillerMode.Count when filler.Count.HasValue: @@ -518,6 +394,7 @@ public abstract class PlayoutModeSchedulerBase : IPlayoutModeScheduler whe remainingToFill, FillerKind.PreRoll, padFiller.AllowWatermarks, + log, cancellationToken)); totalDuration = TimeSpan.FromMilliseconds(result.Sum(pi => (pi.Finish - pi.Start).TotalMilliseconds)); @@ -544,6 +421,7 @@ public abstract class PlayoutModeSchedulerBase : IPlayoutModeScheduler whe remainingToFill, FillerKind.MidRoll, padFiller.AllowWatermarks, + log, cancellationToken)); TimeSpan average = effectiveChapters.Count <= 1 ? remainingToFill @@ -607,6 +485,7 @@ public abstract class PlayoutModeSchedulerBase : IPlayoutModeScheduler whe remainingToFill, FillerKind.PostRoll, padFiller.AllowWatermarks, + log, cancellationToken)); totalDuration = TimeSpan.FromMilliseconds(result.Sum(pi => (pi.Finish - pi.Start).TotalMilliseconds)); @@ -682,13 +561,15 @@ public abstract class PlayoutModeSchedulerBase : IPlayoutModeScheduler whe TimeSpan duration, FillerKind fillerKind, bool allowWatermarks, + bool log, CancellationToken cancellationToken) { var result = new List(); TimeSpan remainingToFill = duration; - var skipped = false; - while (enumerator.Current.IsSome && remainingToFill > TimeSpan.Zero) + var discardToFillAttempts = 0; + while (enumerator.Current.IsSome && remainingToFill > TimeSpan.Zero && + remainingToFill >= enumerator.MinimumDuration) { foreach (MediaItem mediaItem in enumerator.Current) { @@ -712,40 +593,30 @@ public abstract class PlayoutModeSchedulerBase : IPlayoutModeScheduler whe result.Add(playoutItem); enumerator.MoveNext(); } - else if (skipped) - { - // set to zero so it breaks out of the while loop - remainingToFill = TimeSpan.Zero; - } else { - if (itemDuration >= duration * 1.5) + discardToFillAttempts++; + if (discardToFillAttempts >= enumerator.Count) { - _logger.LogWarning( - "Filler item is too long {FillerDuration} to fill {GapDuration}; skipping to next filler item", - itemDuration, - duration); - - skipped = true; - enumerator.MoveNext(); + // set to zero so it breaks out of the while loop + remainingToFill = TimeSpan.Zero; } else { - if (itemDuration > duration) + if (log) { - _logger.LogWarning( - "Filler item is too long {FillerDuration} to fill {GapDuration}; aborting filler block", + _logger.LogDebug( + "Filler item is too long {FillerDuration:g} to fill {GapDuration:g}; skipping to next filler item", itemDuration, - duration); + remainingToFill); } - // set to zero so it breaks out of the while loop - remainingToFill = TimeSpan.Zero; + enumerator.MoveNext(); } } } } - + return result; } diff --git a/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerDuration.cs b/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerDuration.cs index c8416790..df64d6cb 100644 --- a/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerDuration.cs +++ b/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerDuration.cs @@ -121,25 +121,41 @@ public class PlayoutModeSchedulerDuration : PlayoutModeSchedulerBase playoutItem.GuideFinish = du.UtcDateTime); DateTimeOffset durationFinish = nextState.DurationFinish.IfNone(SystemTime.MaxValueUtc); - DateTimeOffset itemEndTimeWithFiller = CalculateEndTimeWithFiller( - collectionEnumerators, + + var enumeratorClones = new Dictionary(); + foreach ((CollectionKey key, IMediaCollectionEnumerator enumerator) in collectionEnumerators) + { + IMediaCollectionEnumerator clone = enumerator.Clone(enumerator.State.Clone(), cancellationToken); + enumeratorClones.Add(key, clone); + } + + List maybePlayoutItems = AddFiller( + nextState, + enumeratorClones, scheduleItem, - itemStartTime, - itemDuration, - itemChapters); + playoutItem, + itemChapters, + log: false, + cancellationToken); + + DateTimeOffset itemEndTimeWithFiller = maybePlayoutItems.Max(pi => pi.FinishOffset); + willFinishInTime = itemStartTime > durationFinish || itemEndTimeWithFiller <= durationFinish; if (willFinishInTime) { // LogScheduledItem(scheduleItem, mediaItem, itemStartTime); - playoutItems.AddRange( - AddFiller( - nextState, - collectionEnumerators, - scheduleItem, - playoutItem, - itemChapters, - cancellationToken)); + playoutItems.AddRange(maybePlayoutItems); + + // update original enumerators + foreach ((CollectionKey key, IMediaCollectionEnumerator enumerator) in collectionEnumerators) + { + IMediaCollectionEnumerator clone = enumeratorClones[key]; + while (enumerator.State.Seed != clone.State.Seed || enumerator.State.Index != clone.State.Index) + { + enumerator.MoveNext(); + } + } nextState = nextState with { diff --git a/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerFlood.cs b/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerFlood.cs index c7a7072a..4f17997c 100644 --- a/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerFlood.cs +++ b/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerFlood.cs @@ -74,12 +74,23 @@ public class PlayoutModeSchedulerFlood : PlayoutModeSchedulerBase(); + foreach ((CollectionKey key, IMediaCollectionEnumerator enumerator) in collectionEnumerators) + { + IMediaCollectionEnumerator clone = enumerator.Clone(enumerator.State.Clone(), cancellationToken); + enumeratorClones.Add(key, clone); + } + + List maybePlayoutItems = AddFiller( + nextState, + enumeratorClones, scheduleItem, - itemStartTime, - itemDuration, - itemChapters); + playoutItem, + itemChapters, + log: false, + cancellationToken); + + DateTimeOffset itemEndTimeWithFiller = maybePlayoutItems.Max(pi => pi.FinishOffset); // if the next schedule item is supposed to start during this item, // don't schedule this item and just move on @@ -88,27 +99,16 @@ public class PlayoutModeSchedulerFlood : PlayoutModeSchedulerBase 0) + // update original enumerators + foreach ((CollectionKey key, IMediaCollectionEnumerator enumerator) in collectionEnumerators) { - DateTimeOffset actualEndTime = playoutItems.Max(p => p.FinishOffset); - if (Math.Abs((itemEndTimeWithFiller - actualEndTime).TotalSeconds) > 1) + IMediaCollectionEnumerator clone = enumeratorClones[key]; + while (enumerator.State.Seed != clone.State.Seed || enumerator.State.Index != clone.State.Index) { - _logger.LogWarning( - "Filler prediction failure: predicted {PredictedDuration} doesn't match actual {ActualDuration}", - itemEndTimeWithFiller, - actualEndTime); - - // _logger.LogWarning("Playout items: {@PlayoutItems}", playoutItems); + enumerator.MoveNext(); } } diff --git a/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerMultiple.cs b/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerMultiple.cs index 4c6ff623..7cc950cd 100644 --- a/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerMultiple.cs +++ b/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerMultiple.cs @@ -77,12 +77,6 @@ public class PlayoutModeSchedulerMultiple : PlayoutModeSchedulerBase pi.FinishOffset), MultipleRemaining = nextState.MultipleRemaining.Map(i => i - 1), // only bump guide group if we don't have a custom title diff --git a/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerOne.cs b/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerOne.cs index c882c335..452aa932 100644 --- a/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerOne.cs +++ b/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerOne.cs @@ -53,24 +53,18 @@ public class PlayoutModeSchedulerOne : PlayoutModeSchedulerBase playoutItems = AddFiller( playoutBuilderState, collectionEnumerators, scheduleItem, playoutItem, itemChapters, + log: true, cancellationToken); PlayoutBuilderState nextState = playoutBuilderState with { - CurrentTime = itemEndTimeWithFiller + CurrentTime = playoutItems.Max(pi => pi.FinishOffset) }; nextState.ScheduleItemsEnumerator.MoveNext(); diff --git a/ErsatzTV.Core/Scheduling/RandomizedMediaCollectionEnumerator.cs b/ErsatzTV.Core/Scheduling/RandomizedMediaCollectionEnumerator.cs index ab60fdcb..88b63ec4 100644 --- a/ErsatzTV.Core/Scheduling/RandomizedMediaCollectionEnumerator.cs +++ b/ErsatzTV.Core/Scheduling/RandomizedMediaCollectionEnumerator.cs @@ -26,6 +26,11 @@ public class RandomizedMediaCollectionEnumerator : IMediaCollectionEnumerator } } + public IMediaCollectionEnumerator Clone(CollectionEnumeratorState state, CancellationToken cancellationToken) + { + return new RandomizedMediaCollectionEnumerator(_mediaItems, state); + } + public CollectionEnumeratorState State { get; } public Option Current => _mediaItems.Any() ? _mediaItems[_index] : None; @@ -36,8 +41,7 @@ public class RandomizedMediaCollectionEnumerator : IMediaCollectionEnumerator State.Index++; } - public Option Peek(int offset) => - throw new NotSupportedException(); - public Option MinimumDuration => _lazyMinimumDuration.Value; + + public int Count => _mediaItems.Count; } diff --git a/ErsatzTV.Core/Scheduling/SeasonEpisodeMediaCollectionEnumerator.cs b/ErsatzTV.Core/Scheduling/SeasonEpisodeMediaCollectionEnumerator.cs index c9d5fa74..471d4b71 100644 --- a/ErsatzTV.Core/Scheduling/SeasonEpisodeMediaCollectionEnumerator.cs +++ b/ErsatzTV.Core/Scheduling/SeasonEpisodeMediaCollectionEnumerator.cs @@ -31,14 +31,18 @@ public sealed class SeasonEpisodeMediaCollectionEnumerator : IMediaCollectionEnu } } + public IMediaCollectionEnumerator Clone(CollectionEnumeratorState state, CancellationToken cancellationToken) + { + return new SeasonEpisodeMediaCollectionEnumerator(_sortedMediaItems, state); + } + public CollectionEnumeratorState State { get; } public Option Current => _sortedMediaItems.Any() ? _sortedMediaItems[State.Index] : None; public void MoveNext() => State.Index = (State.Index + 1) % _sortedMediaItems.Count; - public Option Peek(int offset) => - _sortedMediaItems.Any() ? _sortedMediaItems[(State.Index + offset) % _sortedMediaItems.Count] : None; - public Option MinimumDuration => _lazyMinimumDuration.Value; + + public int Count => _sortedMediaItems.Count; } diff --git a/ErsatzTV.Core/Scheduling/ShuffleInOrderCollectionEnumerator.cs b/ErsatzTV.Core/Scheduling/ShuffleInOrderCollectionEnumerator.cs index 4685c629..f0573d56 100644 --- a/ErsatzTV.Core/Scheduling/ShuffleInOrderCollectionEnumerator.cs +++ b/ErsatzTV.Core/Scheduling/ShuffleInOrderCollectionEnumerator.cs @@ -42,6 +42,11 @@ public class ShuffleInOrderCollectionEnumerator : IMediaCollectionEnumerator } } + public IMediaCollectionEnumerator Clone(CollectionEnumeratorState state, CancellationToken cancellationToken) + { + return new ShuffleInOrderCollectionEnumerator(_collections, state, _randomStartPoint, cancellationToken); + } + public CollectionEnumeratorState State { get; } public Option Current => _shuffled.Any() ? _shuffled[State.Index % _mediaItemCount] : None; @@ -69,8 +74,6 @@ public class ShuffleInOrderCollectionEnumerator : IMediaCollectionEnumerator State.Index %= _shuffled.Count; } - public Option Peek(int offset) => throw new NotSupportedException(); - private IList Shuffle(IList collections, Random random) { // based on https://keyj.emphy.de/balanced-shuffle/ @@ -208,4 +211,6 @@ public class ShuffleInOrderCollectionEnumerator : IMediaCollectionEnumerator } public Option MinimumDuration => _lazyMinimumDuration.Value; + + public int Count => _shuffled.Count; } diff --git a/ErsatzTV.Core/Scheduling/ShuffledMediaCollectionEnumerator.cs b/ErsatzTV.Core/Scheduling/ShuffledMediaCollectionEnumerator.cs index 106b20c7..ffa5f571 100644 --- a/ErsatzTV.Core/Scheduling/ShuffledMediaCollectionEnumerator.cs +++ b/ErsatzTV.Core/Scheduling/ShuffledMediaCollectionEnumerator.cs @@ -39,6 +39,11 @@ public class ShuffledMediaCollectionEnumerator : IMediaCollectionEnumerator } } + public IMediaCollectionEnumerator Clone(CollectionEnumeratorState state, CancellationToken cancellationToken) + { + return new ShuffledMediaCollectionEnumerator(_mediaItems, state, cancellationToken); + } + public CollectionEnumeratorState State { get; } public Option Current => _shuffled.Any() ? _shuffled[State.Index % _mediaItemCount] : None; @@ -66,34 +71,6 @@ public class ShuffledMediaCollectionEnumerator : IMediaCollectionEnumerator State.Index %= _mediaItemCount; } - public Option Peek(int offset) - { - if (offset == 0) - { - return Current; - } - - if ((State.Index + offset) % _mediaItemCount == 0) - { - IList shuffled; - Option tail = Current; - - // clone the random - CloneableRandom randomCopy = _random.Clone(); - - do - { - int newSeed = randomCopy.Next(); - randomCopy = new CloneableRandom(newSeed); - shuffled = Shuffle(_mediaItems, randomCopy); - } while (_mediaItems.Count > 1 && shuffled[0] == tail); - - return shuffled.Any() ? shuffled[0] : None; - } - - return _shuffled.Any() ? _shuffled[(State.Index + offset) % _mediaItemCount] : None; - } - private IList Shuffle(IEnumerable list, CloneableRandom random) { GroupedMediaItem[] copy = list.ToArray(); @@ -110,4 +87,6 @@ public class ShuffledMediaCollectionEnumerator : IMediaCollectionEnumerator } public Option MinimumDuration => _lazyMinimumDuration.Value; + + public int Count => _shuffled.Count; } diff --git a/ErsatzTV.Infrastructure/Scheduling/MultiEpisodeShuffleCollectionEnumerator.cs b/ErsatzTV.Infrastructure/Scheduling/MultiEpisodeShuffleCollectionEnumerator.cs index 56937a42..84ea0996 100644 --- a/ErsatzTV.Infrastructure/Scheduling/MultiEpisodeShuffleCollectionEnumerator.cs +++ b/ErsatzTV.Infrastructure/Scheduling/MultiEpisodeShuffleCollectionEnumerator.cs @@ -10,6 +10,8 @@ namespace ErsatzTV.Infrastructure.Scheduling; public class MultiEpisodeShuffleCollectionEnumerator : IMediaCollectionEnumerator { private readonly CancellationToken _cancellationToken; + private readonly IScriptEngine _scriptEngine; + private readonly string _scriptFile; private readonly ILogger _logger; private readonly int _mediaItemCount; private readonly Dictionary> _mediaItemGroups; @@ -26,6 +28,8 @@ public class MultiEpisodeShuffleCollectionEnumerator : IMediaCollectionEnumerato ILogger logger, CancellationToken cancellationToken) { + _scriptEngine = scriptEngine; + _scriptFile = scriptFile; _logger = logger; _cancellationToken = cancellationToken; @@ -83,6 +87,17 @@ public class MultiEpisodeShuffleCollectionEnumerator : IMediaCollectionEnumerato } } + public IMediaCollectionEnumerator Clone(CollectionEnumeratorState state, CancellationToken cancellationToken) + { + return new MultiEpisodeShuffleCollectionEnumerator( + _ungrouped, + state, + _scriptEngine, + _scriptFile, + _logger, + cancellationToken); + } + public CollectionEnumeratorState State { get; } public Option Current => _shuffled.Any() ? _shuffled[State.Index % _mediaItemCount] : None; @@ -110,34 +125,6 @@ public class MultiEpisodeShuffleCollectionEnumerator : IMediaCollectionEnumerato State.Index %= _mediaItemCount; } - public Option Peek(int offset) - { - if (offset == 0) - { - return Current; - } - - if ((State.Index + offset) % _mediaItemCount == 0) - { - IList shuffled; - Option tail = Current; - - // clone the random - CloneableRandom randomCopy = _random.Clone(); - - do - { - int newSeed = randomCopy.Next(); - randomCopy = new CloneableRandom(newSeed); - shuffled = Shuffle(randomCopy); - } while (_mediaItemCount > 1 && shuffled[0] == tail); - - return shuffled.Any() ? shuffled[0] : None; - } - - return _shuffled.Any() ? _shuffled[(State.Index + offset) % _mediaItemCount] : None; - } - private IList Shuffle(CloneableRandom random) { int maxGroupNumber = _mediaItemGroups.Max(a => a.Key); @@ -202,4 +189,6 @@ public class MultiEpisodeShuffleCollectionEnumerator : IMediaCollectionEnumerato } public Option MinimumDuration => _lazyMinimumDuration.Value; + + public int Count => _shuffled.Count; }