Browse Source

fix a handful of scheduling edge cases (#814)

pull/815/head
Jason Dove 3 years ago committed by GitHub
parent
commit
a61c4b3472
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      CHANGELOG.md
  2. 139
      ErsatzTV.Core.Tests/Scheduling/PlayoutBuilderTests.cs
  3. 56
      ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerDurationTests.cs
  4. 55
      ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerFloodTests.cs
  5. 61
      ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerMultipleTests.cs
  6. 55
      ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerOneTests.cs
  7. 23
      ErsatzTV.Core/Scheduling/PlayoutBuilder.cs
  8. 6
      ErsatzTV.Core/Scheduling/PlayoutModeSchedulerDuration.cs
  9. 13
      ErsatzTV.Core/Scheduling/PlayoutModeSchedulerFlood.cs
  10. 7
      ErsatzTV.Core/Scheduling/PlayoutModeSchedulerMultiple.cs
  11. 6
      ErsatzTV.Core/Scheduling/PlayoutModeSchedulerOne.cs

1
CHANGELOG.md

@ -7,6 +7,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Fixed ### Fixed
- Fix error display with `HLS Segmenter` and `MPEG-TS` streaming modes - 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 - 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 ### Added
- Clean transcode cache folder on startup and after `HLS Segmenter` session terminates for any reason - Clean transcode cache folder on startup and after `HLS Segmenter` session terminates for any reason

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

@ -68,6 +68,8 @@ public class PlayoutBuilderTests
result.Items.Head().MediaItemId.Should().Be(2); result.Items.Head().MediaItemId.Should().Be(2);
result.Items.Head().StartOffset.TimeOfDay.Should().Be(TimeSpan.Zero); result.Items.Head().StartOffset.TimeOfDay.Should().Be(TimeSpan.Zero);
result.Items.Head().FinishOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(6)); result.Items.Head().FinishOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(6));
result.Anchor.NextStartOffset.Should().Be(HoursAfterMidnight(6));
} }
[Test] [Test]
@ -127,6 +129,8 @@ public class PlayoutBuilderTests
result.Items.Head().MediaItemId.Should().Be(2); result.Items.Head().MediaItemId.Should().Be(2);
result.Items.Head().StartOffset.TimeOfDay.Should().Be(TimeSpan.Zero); result.Items.Head().StartOffset.TimeOfDay.Should().Be(TimeSpan.Zero);
result.Items.Head().FinishOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(6)); result.Items.Head().FinishOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(6));
result.Anchor.NextStartOffset.Should().Be(HoursAfterMidnight(6));
} }
[Test] [Test]
@ -186,6 +190,8 @@ public class PlayoutBuilderTests
result.Items.Head().MediaItemId.Should().Be(2); result.Items.Head().MediaItemId.Should().Be(2);
result.Items.Head().StartOffset.TimeOfDay.Should().Be(TimeSpan.Zero); result.Items.Head().StartOffset.TimeOfDay.Should().Be(TimeSpan.Zero);
result.Items.Head().FinishOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(6)); result.Items.Head().FinishOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(6));
result.Anchor.NextStartOffset.Should().Be(HoursAfterMidnight(6));
} }
[Test] [Test]
@ -214,6 +220,8 @@ public class PlayoutBuilderTests
result.Items.Count.Should().Be(1); result.Items.Count.Should().Be(1);
result.Items.Head().StartOffset.TimeOfDay.Should().Be(TimeSpan.Zero); result.Items.Head().StartOffset.TimeOfDay.Should().Be(TimeSpan.Zero);
result.Items.Head().FinishOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(6)); result.Items.Head().FinishOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(6));
result.Anchor.NextStartOffset.Should().Be(HoursAfterMidnight(6));
} }
[Test] [Test]
@ -242,6 +250,8 @@ public class PlayoutBuilderTests
result.Items.Count.Should().Be(1); result.Items.Count.Should().Be(1);
result.Items.Head().StartOffset.TimeOfDay.Should().Be(TimeSpan.Zero); result.Items.Head().StartOffset.TimeOfDay.Should().Be(TimeSpan.Zero);
result.Items.Head().FinishOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(6)); result.Items.Head().FinishOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(6));
result.Anchor.NextStartOffset.Should().Be(HoursAfterMidnight(6));
} }
[Test] [Test]
@ -261,6 +271,8 @@ public class PlayoutBuilderTests
result.Items.Count.Should().Be(1); result.Items.Count.Should().Be(1);
result.Items.Head().StartOffset.TimeOfDay.Should().Be(TimeSpan.Zero); result.Items.Head().StartOffset.TimeOfDay.Should().Be(TimeSpan.Zero);
result.Items.Head().FinishOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(6)); result.Items.Head().FinishOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(6));
result.Anchor.NextStartOffset.Should().Be(HoursAfterMidnight(6));
} }
[Test] [Test]
@ -281,6 +293,8 @@ public class PlayoutBuilderTests
result.Items[0].StartOffset.TimeOfDay.Should().Be(TimeSpan.Zero); result.Items[0].StartOffset.TimeOfDay.Should().Be(TimeSpan.Zero);
result.Items[1].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(6)); result.Items[1].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(6));
result.Items[1].FinishOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(12)); result.Items[1].FinishOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(12));
result.Anchor.NextStartOffset.Should().Be(HoursAfterMidnight(12));
} }
[Test] [Test]
@ -308,6 +322,8 @@ public class PlayoutBuilderTests
result.Items[2].MediaItemId.Should().Be(1); result.Items[2].MediaItemId.Should().Be(1);
result.Items[3].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(3)); result.Items[3].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(3));
result.Items[3].MediaItemId.Should().Be(2); result.Items[3].MediaItemId.Should().Be(2);
result.Anchor.NextStartOffset.Should().Be(HoursAfterMidnight(4));
} }
[Test] [Test]
@ -329,7 +345,7 @@ public class PlayoutBuilderTests
result.Items.Count.Should().Be(1); result.Items.Count.Should().Be(1);
result.Items.Head().MediaItemId.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.Count.Should().Be(1);
result.ProgramScheduleAnchors.Head().EnumeratorState.Index.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().StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(6));
result2.Items.Last().MediaItemId.Should().Be(2); 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.Count.Should().Be(1);
result2.ProgramScheduleAnchors.Head().EnumeratorState.Index.Should().Be(0); result2.ProgramScheduleAnchors.Head().EnumeratorState.Index.Should().Be(0);
} }
@ -452,6 +468,8 @@ public class PlayoutBuilderTests
int firstSeedValue = result.ProgramScheduleAnchors.Head().EnumeratorState.Seed; int firstSeedValue = result.ProgramScheduleAnchors.Head().EnumeratorState.Seed;
result.Anchor.NextStartOffset.Should().Be(HoursAfterMidnight(6));
DateTimeOffset start2 = HoursAfterMidnight(0); DateTimeOffset start2 = HoursAfterMidnight(0);
DateTimeOffset finish2 = start2 + TimeSpan.FromHours(6); DateTimeOffset finish2 = start2 + TimeSpan.FromHours(6);
@ -460,6 +478,8 @@ public class PlayoutBuilderTests
int secondSeedValue = result2.ProgramScheduleAnchors.Head().EnumeratorState.Seed; int secondSeedValue = result2.ProgramScheduleAnchors.Head().EnumeratorState.Seed;
firstSeedValue.Should().Be(secondSeedValue); firstSeedValue.Should().Be(secondSeedValue);
result2.Anchor.NextStartOffset.Should().Be(HoursAfterMidnight(6));
} }
[Test] [Test]
@ -548,6 +568,8 @@ public class PlayoutBuilderTests
result.Items[3].MediaItemId.Should().Be(3); result.Items[3].MediaItemId.Should().Be(3);
result.Items[4].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(5)); result.Items[4].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(5));
result.Items[4].MediaItemId.Should().Be(2); result.Items[4].MediaItemId.Should().Be(2);
result.Anchor.NextStartOffset.Should().Be(HoursAfterMidnight(6));
} }
[Test] [Test]
@ -682,6 +704,8 @@ public class PlayoutBuilderTests
result.Items[26].MediaItemId.Should().Be(3); result.Items[26].MediaItemId.Should().Be(3);
result.Items[27].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(5)); result.Items[27].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(5));
result.Items[27].MediaItemId.Should().Be(2); result.Items[27].MediaItemId.Should().Be(2);
result.Anchor.NextStartOffset.Should().Be(HoursAfterMidnight(30));
} }
[Test] [Test]
@ -777,6 +801,8 @@ public class PlayoutBuilderTests
result.Items[5].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(6)); result.Items[5].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(6));
result.Items[5].MediaItemId.Should().Be(2); result.Items[5].MediaItemId.Should().Be(2);
result.Anchor.NextStartOffset.Should().Be(HoursAfterMidnight(7));
} }
[Test] [Test]
@ -870,6 +896,8 @@ public class PlayoutBuilderTests
result.Items[5].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(12)); result.Items[5].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(12));
result.Items[5].MediaItemId.Should().Be(3); result.Items[5].MediaItemId.Should().Be(3);
result.Anchor.NextStartOffset.Should().Be(HoursAfterMidnight(31));
} }
[Test] [Test]
@ -974,6 +1002,8 @@ public class PlayoutBuilderTests
result.Items[4].MediaItemId.Should().Be(2); result.Items[4].MediaItemId.Should().Be(2);
result.Anchor.InFlood.Should().BeTrue(); result.Anchor.InFlood.Should().BeTrue();
result.Anchor.NextStartOffset.Should().Be(HoursAfterMidnight(32));
} }
[Test] [Test]
@ -1073,6 +1103,8 @@ public class PlayoutBuilderTests
result.Items[5].MediaItemId.Should().Be(1); result.Items[5].MediaItemId.Should().Be(1);
result.Items[6].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(5.75)); result.Items[6].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(5.75));
result.Items[6].MediaItemId.Should().Be(2); result.Items[6].MediaItemId.Should().Be(2);
result.Anchor.NextStartOffset.Should().Be(DateTime.Today.AddHours(6.75));
} }
[Test] [Test]
@ -1171,6 +1203,8 @@ public class PlayoutBuilderTests
result.Items[5].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(4.75)); result.Items[5].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(4.75));
result.Items[5].MediaItemId.Should().Be(4); result.Items[5].MediaItemId.Should().Be(4);
result.Anchor.NextStartOffset.Should().Be(DateTime.Today.AddHours(6.25));
} }
[Test] [Test]
@ -1275,6 +1309,7 @@ public class PlayoutBuilderTests
result.Anchor.ScheduleItemsEnumeratorState.Index.Should().Be(1); result.Anchor.ScheduleItemsEnumeratorState.Index.Should().Be(1);
result.Anchor.MultipleRemaining.Should().Be(1); result.Anchor.MultipleRemaining.Should().Be(1);
result.Anchor.NextStartOffset.Should().Be(HoursAfterMidnight(5));
} }
[Test] [Test]
@ -1374,6 +1409,7 @@ public class PlayoutBuilderTests
result.Anchor.ScheduleItemsEnumeratorState.Index.Should().Be(0); result.Anchor.ScheduleItemsEnumeratorState.Index.Should().Be(0);
result.Anchor.MultipleRemaining.Should().BeNull(); result.Anchor.MultipleRemaining.Should().BeNull();
result.Anchor.NextStartOffset.Should().Be(HoursAfterMidnight(5));
} }
[Test] [Test]
@ -1480,6 +1516,7 @@ public class PlayoutBuilderTests
result.Anchor.ScheduleItemsEnumeratorState.Index.Should().Be(1); result.Anchor.ScheduleItemsEnumeratorState.Index.Should().Be(1);
result.Anchor.DurationFinish.Should().Be(HoursAfterMidnight(6).UtcDateTime); result.Anchor.DurationFinish.Should().Be(HoursAfterMidnight(6).UtcDateTime);
result.Anchor.NextStartOffset.Should().Be(HoursAfterMidnight(5));
} }
[Test] [Test]
@ -1617,6 +1654,96 @@ public class PlayoutBuilderTests
result.Anchor.ScheduleItemsEnumeratorState.Index.Should().Be(0); result.Anchor.ScheduleItemsEnumeratorState.Index.Should().Be(0);
result.Anchor.DurationFinish.Should().BeNull(); 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<MediaItem>
{
TestMovie(1, TimeSpan.FromMinutes(61), new DateTime(2020, 1, 1))
}
};
var collectionTwo = new Collection
{
Id = 2,
Name = "Filler Items",
MediaItems = new List<MediaItem>
{
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<ProgramScheduleItem>
{
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<PlayoutProgramScheduleAnchor>(),
Items = new List<PlayoutItem>()
};
var configRepo = new Mock<IConfigElementRepository>();
var televisionRepo = new FakeTelevisionRepository();
var artistRepo = new Mock<IArtistRepository>();
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] [Test]
@ -1696,6 +1823,7 @@ public class PlayoutBuilderTests
result.Anchor.ScheduleItemsEnumeratorState.Index.Should().Be(0); result.Anchor.ScheduleItemsEnumeratorState.Index.Should().Be(0);
result.Anchor.DurationFinish.Should().BeNull(); result.Anchor.DurationFinish.Should().BeNull();
result.Anchor.NextStartOffset.Should().Be(HoursAfterMidnight(6));
} }
[Test] [Test]
@ -1746,6 +1874,8 @@ public class PlayoutBuilderTests
int seed = result.ProgramScheduleAnchors[0].EnumeratorState.Seed; int seed = result.ProgramScheduleAnchors[0].EnumeratorState.Seed;
result.ProgramScheduleAnchors.All(a => a.EnumeratorState.Seed == seed).Should().BeTrue(); 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].MediaItemId.Should().Be(2);
result.Items[3].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(18)); result.Items[3].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(18));
result.Items[3].FinishOffset.TimeOfDay.Should().Be(TimeSpan.Zero); 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.Items[4].MediaItemId.Should().Be(2);
result.Anchor.InFlood.Should().BeTrue(); result.Anchor.InFlood.Should().BeTrue();
result.Anchor.NextStartOffset.Should().Be(HoursAfterMidnight(32));
} }
[Test] [Test]
@ -2318,6 +2451,7 @@ public class PlayoutBuilderTests
result.Anchor.ScheduleItemsEnumeratorState.Index.Should().Be(1); result.Anchor.ScheduleItemsEnumeratorState.Index.Should().Be(1);
result.Anchor.MultipleRemaining.Should().Be(1); result.Anchor.MultipleRemaining.Should().Be(1);
result.Anchor.NextStartOffset.Should().Be(HoursAfterMidnight(5));
} }
[Test] [Test]
@ -2424,6 +2558,7 @@ public class PlayoutBuilderTests
result.Anchor.ScheduleItemsEnumeratorState.Index.Should().Be(1); result.Anchor.ScheduleItemsEnumeratorState.Index.Should().Be(1);
result.Anchor.DurationFinish.Should().Be(HoursAfterMidnight(6).UtcDateTime); result.Anchor.DurationFinish.Should().Be(HoursAfterMidnight(6).UtcDateTime);
result.Anchor.NextStartOffset.Should().Be(HoursAfterMidnight(5));
} }
} }

56
ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerDurationTests.cs

@ -695,4 +695,60 @@ public class PlayoutModeSchedulerDurationTests : SchedulerTestBase
playoutItems[6].FillerKind.Should().Be(FillerKind.Fallback); playoutItems[6].FillerKind.Should().Be(FillerKind.Fallback);
playoutItems[6].GuideFinish.HasValue.Should().BeFalse(); 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<ProgramScheduleItem>
{
scheduleItem,
NextScheduleItem
};
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
sortedScheduleItems,
new CollectionEnumeratorState());
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
var scheduler = new PlayoutModeSchedulerDuration(new Mock<ILogger>().Object);
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> 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);
}
} }

55
ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerFloodTests.cs

@ -831,6 +831,61 @@ public class PlayoutModeSchedulerFloodTests : SchedulerTestBase
playoutItems[2].FillerKind.Should().Be(FillerKind.None); 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<ProgramScheduleItem>
{
scheduleItem,
NextScheduleItem
};
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
sortedScheduleItems,
new CollectionEnumeratorState());
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
var scheduler = new PlayoutModeSchedulerFlood(new Mock<ILogger>().Object);
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> 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 protected override ProgramScheduleItem NextScheduleItem => new ProgramScheduleItemOne
{ {
StartTime = TimeSpan.FromHours(3) StartTime = TimeSpan.FromHours(3)

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

@ -664,6 +664,67 @@ public class PlayoutModeSchedulerMultipleTests : SchedulerTestBase
playoutItems[2].FillerKind.Should().Be(FillerKind.None); 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<ProgramScheduleItem>
{
scheduleItem,
NextScheduleItem
};
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
sortedScheduleItems,
new CollectionEnumeratorState());
var collectionMediaItems = new Dictionary<CollectionKey, List<MediaItem>>
{
{ CollectionKey.ForScheduleItem(scheduleItem), collectionOne.MediaItems }
}.ToMap();
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
var scheduler = new PlayoutModeSchedulerMultiple(collectionMediaItems, new Mock<ILogger>().Object);
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> 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 protected override ProgramScheduleItem NextScheduleItem => new ProgramScheduleItemOne
{ {
StartTime = TimeSpan.FromHours(3) StartTime = TimeSpan.FromHours(3)

55
ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerOneTests.cs

@ -769,6 +769,61 @@ public class PlayoutModeSchedulerOneTests : SchedulerTestBase
playoutItems[3].FillerKind.Should().Be(FillerKind.PostRoll); 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<ProgramScheduleItem>
{
scheduleItem,
NextScheduleItem
};
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
sortedScheduleItems,
new CollectionEnumeratorState());
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
var scheduler = new PlayoutModeSchedulerOne(new Mock<ILogger>().Object);
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> 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 protected override ProgramScheduleItem NextScheduleItem => new ProgramScheduleItemOne
{ {
StartTime = TimeSpan.FromHours(3) StartTime = TimeSpan.FromHours(3)

23
ErsatzTV.Core/Scheduling/PlayoutBuilder.cs

@ -327,8 +327,25 @@ public class PlayoutBuilder : IPlayoutBuilder
randomStartPoint); randomStartPoint);
} }
// remove any items outside the desired range // remove old items
playout.Items.RemoveAll(old => old.FinishOffset < trimBefore || old.StartOffset > trimAfter); 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; return playout;
} }
@ -409,6 +426,8 @@ public class PlayoutBuilder : IPlayoutBuilder
// loop until we're done filling the desired amount of time // loop until we're done filling the desired amount of time
while (playoutBuilderState.CurrentTime < playoutFinish) while (playoutBuilderState.CurrentTime < playoutFinish)
{ {
_logger.LogDebug("Playout time is {CurrentTime}", playoutBuilderState.CurrentTime);
// get the schedule item out of the sorted list // get the schedule item out of the sorted list
ProgramScheduleItem scheduleItem = playoutBuilderState.ScheduleItemsEnumerator.Current; ProgramScheduleItem scheduleItem = playoutBuilderState.ScheduleItemsEnumerator.Current;

6
ErsatzTV.Core/Scheduling/PlayoutModeSchedulerDuration.cs

@ -35,6 +35,12 @@ public class PlayoutModeSchedulerDuration : PlayoutModeSchedulerBase<ProgramSche
// find when we should start this item, based on the current time // find when we should start this item, based on the current time
DateTimeOffset itemStartTime = GetStartTimeAfter(nextState, scheduleItem); DateTimeOffset itemStartTime = GetStartTimeAfter(nextState, scheduleItem);
if (itemStartTime >= hardStop)
{
nextState = nextState with { CurrentTime = hardStop };
break;
}
// remember when we need to finish this duration item // remember when we need to finish this duration item
if (nextState.DurationFinish.IsNone) if (nextState.DurationFinish.IsNone)
{ {

13
ErsatzTV.Core/Scheduling/PlayoutModeSchedulerFlood.cs

@ -30,12 +30,21 @@ public class PlayoutModeSchedulerFlood : PlayoutModeSchedulerBase<ProgramSchedul
ProgramScheduleItem peekScheduleItem = nextScheduleItem; ProgramScheduleItem peekScheduleItem = nextScheduleItem;
var scheduledNone = false;
while (contentEnumerator.Current.IsSome && nextState.CurrentTime < hardStop && willFinishInTime) while (contentEnumerator.Current.IsSome && nextState.CurrentTime < hardStop && willFinishInTime)
{ {
MediaItem mediaItem = contentEnumerator.Current.ValueUnsafe(); MediaItem mediaItem = contentEnumerator.Current.ValueUnsafe();
// find when we should start this item, based on the current time // find when we should start this item, based on the current time
DateTimeOffset itemStartTime = GetStartTimeAfter(nextState, scheduleItem); DateTimeOffset itemStartTime = GetStartTimeAfter(nextState, scheduleItem);
if (itemStartTime >= hardStop)
{
scheduledNone = playoutItems.Count == 0;
nextState = nextState with { CurrentTime = hardStop };
break;
}
TimeSpan itemDuration = DurationForMediaItem(mediaItem); TimeSpan itemDuration = DurationForMediaItem(mediaItem);
List<MediaChapter> itemChapters = ChaptersForMediaItem(mediaItem); List<MediaChapter> itemChapters = ChaptersForMediaItem(mediaItem);
@ -112,7 +121,7 @@ public class PlayoutModeSchedulerFlood : PlayoutModeSchedulerBase<ProgramSchedul
nextState = nextState with nextState = nextState with
{ {
InFlood = nextState.CurrentTime >= hardStop, InFlood = playoutItems.Any() && nextState.CurrentTime >= hardStop,
// only decrement guide group if it was bumped // only decrement guide group if it was bumped
NextGuideGroup = playoutItems.Select(pi => pi.GuideGroup).Distinct().Count() != 1 NextGuideGroup = playoutItems.Select(pi => pi.GuideGroup).Distinct().Count() != 1
@ -121,7 +130,7 @@ public class PlayoutModeSchedulerFlood : PlayoutModeSchedulerBase<ProgramSchedul
}; };
// only advance to the next schedule item if we aren't still in a flood // only advance to the next schedule item if we aren't still in a flood
if (!nextState.InFlood) if (!nextState.InFlood && !scheduledNone)
{ {
nextState.ScheduleItemsEnumerator.MoveNext(); nextState.ScheduleItemsEnumerator.MoveNext();
} }

7
ErsatzTV.Core/Scheduling/PlayoutModeSchedulerMultiple.cs

@ -23,6 +23,13 @@ public class PlayoutModeSchedulerMultiple : PlayoutModeSchedulerBase<ProgramSche
{ {
var playoutItems = new List<PlayoutItem>(); var playoutItems = new List<PlayoutItem>();
DateTimeOffset firstStart = GetStartTimeAfter(playoutBuilderState, scheduleItem);
if (firstStart >= hardStop)
{
playoutBuilderState = playoutBuilderState with { CurrentTime = hardStop };
return Tuple(playoutBuilderState, playoutItems);
}
PlayoutBuilderState nextState = playoutBuilderState with PlayoutBuilderState nextState = playoutBuilderState with
{ {
MultipleRemaining = playoutBuilderState.MultipleRemaining.IfNone(scheduleItem.Count) MultipleRemaining = playoutBuilderState.MultipleRemaining.IfNone(scheduleItem.Count)

6
ErsatzTV.Core/Scheduling/PlayoutModeSchedulerOne.cs

@ -27,6 +27,12 @@ public class PlayoutModeSchedulerOne : PlayoutModeSchedulerBase<ProgramScheduleI
playoutBuilderState, playoutBuilderState,
scheduleItem); scheduleItem);
if (itemStartTime >= hardStop)
{
playoutBuilderState = playoutBuilderState with { CurrentTime = hardStop };
break;
}
TimeSpan itemDuration = DurationForMediaItem(mediaItem); TimeSpan itemDuration = DurationForMediaItem(mediaItem);
List<MediaChapter> itemChapters = ChaptersForMediaItem(mediaItem); List<MediaChapter> itemChapters = ChaptersForMediaItem(mediaItem);

Loading…
Cancel
Save