From 464c1e2ea8dfd3ff4c7cce12a423c932e01d5dc2 Mon Sep 17 00:00:00 2001 From: Jason Dove <1695733+jasongdove@users.noreply.github.com> Date: Fri, 18 Jul 2025 01:53:19 +0000 Subject: [PATCH] fix bugs with playout mode multiple (#2160) --- CHANGELOG.md | 4 + .../Scheduling/PlayoutBuilderTests.cs | 107 ++++++++++++++++++ .../PlayoutModeSchedulerMultipleTests.cs | 71 ++++++++++++ .../Scheduling/PlaylistEnumerator.cs | 2 + .../PlayoutModeSchedulerMultiple.cs | 25 +++- .../ErsatzTV.Infrastructure.csproj | 102 ++++++++--------- ErsatzTV.Scanner/ErsatzTV.Scanner.csproj | 88 +++++++------- 7 files changed, 299 insertions(+), 100 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c3fdc1ca..eb90c2a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -106,6 +106,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Split main `Settings` page into multiple pages - Update UI layout on all pages to be less cramped and to work better on mobile - Add CPU and Video Controller info to `Troubleshooting` > `General` output +- Expand special zero-count case for `Multiple` playout mode with playlists + - This configuration will automatically maintain the multiple count so that it is equal to the number of items in each playlist item + - This configuration should be used if you want to play every media item in a playlist item exactly once before advancing ### Fixed - Fix QSV acceleration in docker with older Intel devices @@ -127,6 +130,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Fix some NVIDIA edge cases when media servers don't provide video bit depth information - Fix VAAPI tonemap failure - Fix green bars after VAAPI tonemap +- Fix bug where playout mode `Multiple` would ignore fixed start time ## [25.2.0] - 2025-06-24 ### Added diff --git a/ErsatzTV.Core.Tests/Scheduling/PlayoutBuilderTests.cs b/ErsatzTV.Core.Tests/Scheduling/PlayoutBuilderTests.cs index 7de0c4d0..b361cc71 100644 --- a/ErsatzTV.Core.Tests/Scheduling/PlayoutBuilderTests.cs +++ b/ErsatzTV.Core.Tests/Scheduling/PlayoutBuilderTests.cs @@ -840,6 +840,113 @@ public class PlayoutBuilderTests result.Anchor.NextStartOffset.ShouldBe(HoursAfterMidnight(7)); } + [Test] + public async Task FloodContent_Should_FloodAroundFixedContent_Multiple_With_Gap() + { + var floodCollection = new Collection + { + Id = 1, + Name = "Flood Items", + MediaItems = new List + { + TestMovie(1, TimeSpan.FromMinutes(50), new DateTime(2020, 1, 1)), + TestMovie(2, TimeSpan.FromHours(1), new DateTime(2020, 2, 1)) + } + }; + + var fixedCollection = new Collection + { + Id = 2, + Name = "Fixed Items", + MediaItems = new List + { + TestMovie(3, TimeSpan.FromHours(2), new DateTime(2020, 1, 1)), + TestMovie(4, TimeSpan.FromHours(1), new DateTime(2020, 1, 2)) + } + }; + + var fakeRepository = new FakeMediaCollectionRepository( + Map( + (floodCollection.Id, floodCollection.MediaItems.ToList()), + (fixedCollection.Id, fixedCollection.MediaItems.ToList()))); + + var items = new List + { + new ProgramScheduleItemFlood + { + Id = 1, + Index = 1, + Collection = floodCollection, + CollectionId = floodCollection.Id, + StartTime = null, + PlaybackOrder = PlaybackOrder.Chronological + }, + new ProgramScheduleItemMultiple + { + Id = 2, + Index = 2, + Collection = fixedCollection, + CollectionId = fixedCollection.Id, + StartTime = TimeSpan.FromHours(3), + Count = 2, + PlaybackOrder = PlaybackOrder.Chronological + } + }; + + 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(), + ProgramScheduleAlternates = new List(), + FillGroupIndices = [] + }; + + IConfigElementRepository configRepo = Substitute.For(); + var televisionRepo = new FakeTelevisionRepository(); + IArtistRepository artistRepo = Substitute.For(); + IMultiEpisodeShuffleCollectionEnumeratorFactory factory = + Substitute.For(); + ILocalFileSystem localFileSystem = Substitute.For(); + var builder = new PlayoutBuilder( + Substitute.For(), + configRepo, + fakeRepository, + televisionRepo, + artistRepo, + factory, + localFileSystem, + _logger); + + DateTimeOffset start = HoursAfterMidnight(0); + DateTimeOffset finish = start + TimeSpan.FromHours(7); + + Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish, _cancellationToken); + + result.Items.Count.ShouldBe(6); + + result.Items[0].StartOffset.TimeOfDay.ShouldBe(TimeSpan.Zero); + result.Items[0].MediaItemId.ShouldBe(1); + result.Items[1].StartOffset.TimeOfDay.ShouldBe(TimeSpan.FromMinutes(50)); + result.Items[1].MediaItemId.ShouldBe(2); + result.Items[2].StartOffset.TimeOfDay.ShouldBe(TimeSpan.FromMinutes(50 + 60)); + result.Items[2].MediaItemId.ShouldBe(1); + + result.Items[3].StartOffset.TimeOfDay.ShouldBe(TimeSpan.FromHours(3)); + result.Items[3].MediaItemId.ShouldBe(3); + result.Items[4].StartOffset.TimeOfDay.ShouldBe(TimeSpan.FromHours(5)); + result.Items[4].MediaItemId.ShouldBe(4); + + result.Items[5].StartOffset.TimeOfDay.ShouldBe(TimeSpan.FromHours(6)); + result.Items[5].MediaItemId.ShouldBe(2); + + result.Anchor.NextStartOffset.ShouldBe(HoursAfterMidnight(7)); + } + [Test] public async Task FloodContent_Should_FloodWithFixedStartTime() { diff --git a/ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerMultipleTests.cs b/ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerMultipleTests.cs index b050be92..7f14d211 100644 --- a/ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerMultipleTests.cs +++ b/ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerMultipleTests.cs @@ -16,6 +16,77 @@ public class PlayoutModeSchedulerMultipleTests : SchedulerTestBase private CancellationToken _cancellationToken; + [Test] + public void Should_Respect_Fixed_Start_Time() + { + Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromHours(1)); + + var scheduleItem = new ProgramScheduleItemMultiple + { + Id = 1, + Index = 1, + CollectionType = ProgramScheduleItemCollectionType.Collection, + Collection = collectionOne, + CollectionId = collectionOne.Id, + StartTime = TimeSpan.FromHours(1), + PlaybackOrder = PlaybackOrder.Chronological, + TailFiller = null, + FallbackFiller = null, + Count = 0, + CustomTitle = "CustomTitle" + }; + + var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator( + new List { scheduleItem }, + new CollectionEnumeratorState()); + + var enumerator = new ChronologicalMediaCollectionEnumerator( + collectionOne.MediaItems, + new CollectionEnumeratorState()); + + var collectionItemCount = new Dictionary + { + { CollectionKey.ForScheduleItem(scheduleItem), collectionOne.MediaItems.Count } + }.ToMap(); + + PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); + + var scheduler = new PlayoutModeSchedulerMultiple(collectionItemCount, Substitute.For()); + (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( + startState, + CollectionEnumerators(scheduleItem, enumerator), + scheduleItem, + NextScheduleItem, + HardStop(scheduleItemsEnumerator), + _cancellationToken); + + playoutBuilderState.CurrentTime.ShouldBe(startState.CurrentTime.AddHours(3)); + playoutItems.Last().FinishOffset.ShouldBe(playoutBuilderState.CurrentTime); + + playoutBuilderState.NextGuideGroup.ShouldBe(2); // one guide group here because of custom title + playoutBuilderState.DurationFinish.IsNone.ShouldBeTrue(); + playoutBuilderState.InFlood.ShouldBeFalse(); + playoutBuilderState.MultipleRemaining.IsNone.ShouldBeTrue(); + playoutBuilderState.InDurationFiller.ShouldBeFalse(); + playoutBuilderState.ScheduleItemsEnumerator.State.Index.ShouldBe(0); + + enumerator.State.Index.ShouldBe(0); + + playoutItems.Count.ShouldBe(2); + + playoutItems[0].MediaItemId.ShouldBe(1); + playoutItems[0].StartOffset.ShouldBe(startState.CurrentTime.AddHours(1)); + playoutItems[0].GuideGroup.ShouldBe(1); + playoutItems[0].FillerKind.ShouldBe(FillerKind.None); + playoutItems[0].CustomTitle.ShouldBe("CustomTitle"); + + playoutItems[1].MediaItemId.ShouldBe(2); + playoutItems[1].StartOffset.ShouldBe(startState.CurrentTime.AddHours(2)); + playoutItems[1].GuideGroup.ShouldBe(1); + playoutItems[1].FillerKind.ShouldBe(FillerKind.None); + playoutItems[1].CustomTitle.ShouldBe("CustomTitle"); + } + [Test] public void Should_Fill_Exactly_To_Next_Schedule_Item() { diff --git a/ErsatzTV.Core/Scheduling/PlaylistEnumerator.cs b/ErsatzTV.Core/Scheduling/PlaylistEnumerator.cs index 51568293..778b4df6 100644 --- a/ErsatzTV.Core/Scheduling/PlaylistEnumerator.cs +++ b/ErsatzTV.Core/Scheduling/PlaylistEnumerator.cs @@ -24,6 +24,8 @@ public class PlaylistEnumerator : IMediaCollectionEnumerator public ImmutableList ChildEnumerators { get; private set; } + public bool CurrentEnumeratorPlayAll => _playAll[EnumeratorIndex]; + public int EnumeratorIndex { get; private set; } public void ResetState(CollectionEnumeratorState state) => diff --git a/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerMultiple.cs b/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerMultiple.cs index 9f2d0bde..4c11d533 100644 --- a/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerMultiple.cs +++ b/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerMultiple.cs @@ -33,19 +33,34 @@ public class PlayoutModeSchedulerMultiple : PlayoutModeSchedulerBase 0 && nextState.CurrentTime < hardStop) { diff --git a/ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj b/ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj index e599928f..56580bdd 100644 --- a/ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj +++ b/ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj @@ -1,52 +1,52 @@ - - - - net9.0 - true - VSTHRD200 - enable - latest-Recommended - true - - - - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - - - - - - <_Parameter1>ErsatzTV.Infrastructure.Tests - - - + + + + net9.0 + true + VSTHRD200 + enable + latest-Recommended + true + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + <_Parameter1>ErsatzTV.Infrastructure.Tests + + + \ No newline at end of file diff --git a/ErsatzTV.Scanner/ErsatzTV.Scanner.csproj b/ErsatzTV.Scanner/ErsatzTV.Scanner.csproj index 48d12ee8..18f6ae54 100644 --- a/ErsatzTV.Scanner/ErsatzTV.Scanner.csproj +++ b/ErsatzTV.Scanner/ErsatzTV.Scanner.csproj @@ -1,45 +1,45 @@ - - - - Exe - net9.0 - enable - enable - Debug;Release;Debug No Sync - AnyCPU - 729e6271-c307-43c8-8e36-1b36c39f6de2 - latest-Recommended - true - - - - - - - - - - - - - - - - - - - - - - - - - - - - - <_Parameter1>ErsatzTV.Scanner.Tests - - - + + + + Exe + net9.0 + enable + enable + Debug;Release;Debug No Sync + AnyCPU + 729e6271-c307-43c8-8e36-1b36c39f6de2 + latest-Recommended + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <_Parameter1>ErsatzTV.Scanner.Tests + + + \ No newline at end of file