Browse Source

fix bugs with playout mode multiple (#2160)

pull/2161/head
Jason Dove 1 month ago committed by GitHub
parent
commit
464c1e2ea8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 4
      CHANGELOG.md
  2. 107
      ErsatzTV.Core.Tests/Scheduling/PlayoutBuilderTests.cs
  3. 71
      ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerMultipleTests.cs
  4. 2
      ErsatzTV.Core/Scheduling/PlaylistEnumerator.cs
  5. 19
      ErsatzTV.Core/Scheduling/PlayoutModeSchedulerMultiple.cs

4
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 - Split main `Settings` page into multiple pages
- Update UI layout on all pages to be less cramped and to work better on mobile - 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 - 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 ### Fixed
- Fix QSV acceleration in docker with older Intel devices - 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 some NVIDIA edge cases when media servers don't provide video bit depth information
- Fix VAAPI tonemap failure - Fix VAAPI tonemap failure
- Fix green bars after VAAPI tonemap - Fix green bars after VAAPI tonemap
- Fix bug where playout mode `Multiple` would ignore fixed start time
## [25.2.0] - 2025-06-24 ## [25.2.0] - 2025-06-24
### Added ### Added

107
ErsatzTV.Core.Tests/Scheduling/PlayoutBuilderTests.cs

@ -840,6 +840,113 @@ public class PlayoutBuilderTests
result.Anchor.NextStartOffset.ShouldBe(HoursAfterMidnight(7)); 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<MediaItem>
{
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<MediaItem>
{
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<ProgramScheduleItem>
{
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<PlayoutProgramScheduleAnchor>(),
Items = new List<PlayoutItem>(),
ProgramScheduleAlternates = new List<ProgramScheduleAlternate>(),
FillGroupIndices = []
};
IConfigElementRepository configRepo = Substitute.For<IConfigElementRepository>();
var televisionRepo = new FakeTelevisionRepository();
IArtistRepository artistRepo = Substitute.For<IArtistRepository>();
IMultiEpisodeShuffleCollectionEnumeratorFactory factory =
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
var builder = new PlayoutBuilder(
Substitute.For<IPlayoutTimeShifter>(),
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] [Test]
public async Task FloodContent_Should_FloodWithFixedStartTime() public async Task FloodContent_Should_FloodWithFixedStartTime()
{ {

71
ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerMultipleTests.cs

@ -16,6 +16,77 @@ public class PlayoutModeSchedulerMultipleTests : SchedulerTestBase
private CancellationToken _cancellationToken; 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<ProgramScheduleItem> { scheduleItem },
new CollectionEnumeratorState());
var enumerator = new ChronologicalMediaCollectionEnumerator(
collectionOne.MediaItems,
new CollectionEnumeratorState());
var collectionItemCount = new Dictionary<CollectionKey, int>
{
{ CollectionKey.ForScheduleItem(scheduleItem), collectionOne.MediaItems.Count }
}.ToMap();
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
var scheduler = new PlayoutModeSchedulerMultiple(collectionItemCount, Substitute.For<ILogger>());
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> 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] [Test]
public void Should_Fill_Exactly_To_Next_Schedule_Item() public void Should_Fill_Exactly_To_Next_Schedule_Item()
{ {

2
ErsatzTV.Core/Scheduling/PlaylistEnumerator.cs

@ -24,6 +24,8 @@ public class PlaylistEnumerator : IMediaCollectionEnumerator
public ImmutableList<PlaylistEnumeratorCollectionKey> ChildEnumerators { get; private set; } public ImmutableList<PlaylistEnumeratorCollectionKey> ChildEnumerators { get; private set; }
public bool CurrentEnumeratorPlayAll => _playAll[EnumeratorIndex];
public int EnumeratorIndex { get; private set; } public int EnumeratorIndex { get; private set; }
public void ResetState(CollectionEnumeratorState state) => public void ResetState(CollectionEnumeratorState state) =>

19
ErsatzTV.Core/Scheduling/PlayoutModeSchedulerMultiple.cs

@ -33,19 +33,34 @@ public class PlayoutModeSchedulerMultiple : PlayoutModeSchedulerBase<ProgramSche
PlayoutBuilderState nextState = playoutBuilderState with PlayoutBuilderState nextState = playoutBuilderState with
{ {
CurrentTime = firstStart,
MultipleRemaining = playoutBuilderState.MultipleRemaining.IfNone(scheduleItem.Count) MultipleRemaining = playoutBuilderState.MultipleRemaining.IfNone(scheduleItem.Count)
}; };
IMediaCollectionEnumerator contentEnumerator =
collectionEnumerators[CollectionKey.ForScheduleItem(scheduleItem)];
if (nextState.MultipleRemaining == 0) if (nextState.MultipleRemaining == 0)
{
// playlist count of zero means play all media items in the current playlist item
if (contentEnumerator is PlaylistEnumerator { CurrentEnumeratorPlayAll: true } playlistEnumerator)
{
nextState = nextState with
{
MultipleRemaining = playlistEnumerator
.ChildEnumerators[playlistEnumerator.EnumeratorIndex]
.Enumerator.Count
};
}
else
{ {
nextState = nextState with nextState = nextState with
{ {
MultipleRemaining = _collectionItemCount[CollectionKey.ForScheduleItem(scheduleItem)] MultipleRemaining = _collectionItemCount[CollectionKey.ForScheduleItem(scheduleItem)]
}; };
} }
}
IMediaCollectionEnumerator contentEnumerator =
collectionEnumerators[CollectionKey.ForScheduleItem(scheduleItem)];
while (contentEnumerator.Current.IsSome && nextState.MultipleRemaining > 0 && while (contentEnumerator.Current.IsSome && nextState.MultipleRemaining > 0 &&
nextState.CurrentTime < hardStop) nextState.CurrentTime < hardStop)
{ {

Loading…
Cancel
Save