Browse Source

fix multiple filler scheduling bugs (#738)

pull/739/head
Jason Dove 4 years ago committed by GitHub
parent
commit
741b00fd52
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      CHANGELOG.md
  2. 107
      ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerBaseTests.cs
  3. 48
      ErsatzTV.Core/Scheduling/PlayoutModeSchedulerBase.cs
  4. 7
      ErsatzTV/ViewModels/FillerPresetEditViewModel.cs

2
CHANGELOG.md

@ -11,6 +11,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Fix subtitles using software encoders, videotoolbox, VAAPI - Fix subtitles using software encoders, videotoolbox, VAAPI
- Fix setting VAAPI driver name - Fix setting VAAPI driver name
- Fix ffmpeg troubleshooting reports - Fix ffmpeg troubleshooting reports
- Fix bug where filler would behave as if it were configured to pad even though a different mode was selected
- Fix bug where mid-roll count filler would skip scheduling the final chapter in an episode
## [0.5.0-beta] - 2022-04-13 ## [0.5.0-beta] - 2022-04-13
### Fixed ### Fixed

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

@ -3,8 +3,10 @@ using ErsatzTV.Core.Domain.Filler;
using ErsatzTV.Core.Interfaces.Scheduling; using ErsatzTV.Core.Interfaces.Scheduling;
using ErsatzTV.Core.Scheduling; using ErsatzTV.Core.Scheduling;
using FluentAssertions; using FluentAssertions;
using Microsoft.Extensions.Logging;
using Moq; using Moq;
using NUnit.Framework; using NUnit.Framework;
using Serilog;
namespace ErsatzTV.Core.Tests.Scheduling; namespace ErsatzTV.Core.Tests.Scheduling;
@ -222,7 +224,7 @@ public class PlayoutModeSchedulerBaseTests : SchedulerTestBase
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
List<PlayoutItem> playoutItems = PlayoutModeSchedulerBase<ProgramScheduleItem> List<PlayoutItem> playoutItems = Scheduler()
.AddFiller( .AddFiller(
startState, startState,
CollectionEnumerators(scheduleItem, enumerator), CollectionEnumerators(scheduleItem, enumerator),
@ -273,7 +275,7 @@ public class PlayoutModeSchedulerBaseTests : SchedulerTestBase
// too lazy to make another enumerator for the filler that we don't want // too lazy to make another enumerator for the filler that we don't want
enumerators.Add(CollectionKey.ForFillerPreset(scheduleItem.MidRollFiller), enumerator); enumerators.Add(CollectionKey.ForFillerPreset(scheduleItem.MidRollFiller), enumerator);
List<PlayoutItem> playoutItems = PlayoutModeSchedulerBase<ProgramScheduleItem> List<PlayoutItem> playoutItems = Scheduler()
.AddFiller( .AddFiller(
startState, startState,
enumerators, enumerators,
@ -283,6 +285,77 @@ public class PlayoutModeSchedulerBaseTests : SchedulerTestBase
playoutItems.Count.Should().Be(1); playoutItems.Count.Should().Be(1);
} }
[Test]
public void Should_Schedule_Mid_Roll_Count_Filler_Correctly()
{
Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromHours(1));
Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(5));
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
{
FillerKind = FillerKind.MidRoll,
FillerMode = FillerMode.Count,
PadToNearestMinute = 60, // this should be ignored
Count = 1
}
};
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
new List<ProgramScheduleItem> { scheduleItem },
new CollectionEnumeratorState());
var enumerator = new ChronologicalMediaCollectionEnumerator(
collectionOne.MediaItems,
new CollectionEnumeratorState());
var fillerEnumerator = new ChronologicalMediaCollectionEnumerator(
collectionTwo.MediaItems,
new CollectionEnumeratorState());
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
Dictionary<CollectionKey, IMediaCollectionEnumerator> enumerators = CollectionEnumerators(
scheduleItem,
enumerator);
enumerators.Add(CollectionKey.ForFillerPreset(scheduleItem.MidRollFiller), fillerEnumerator);
List<PlayoutItem> playoutItems = Scheduler()
.AddFiller(
startState,
enumerators,
scheduleItem,
new PlayoutItem
{
MediaItemId = 1,
Start = startState.CurrentTime.UtcDateTime,
Finish = startState.CurrentTime.AddHours(1).UtcDateTime
},
new List<MediaChapter>
{
new() { StartTime = TimeSpan.Zero, EndTime = TimeSpan.FromMinutes(6) },
new() { StartTime = TimeSpan.FromMinutes(6), EndTime = TimeSpan.FromMinutes(60) }
});
playoutItems.Count.Should().Be(3);
playoutItems[0].MediaItemId.Should().Be(1);
playoutItems[0].StartOffset.Should().Be(startState.CurrentTime);
playoutItems[1].MediaItemId.Should().Be(3);
playoutItems[1].StartOffset.Should().Be(startState.CurrentTime + TimeSpan.FromMinutes(6));
playoutItems[2].MediaItemId.Should().Be(1);
playoutItems[2].StartOffset.Should().Be(startState.CurrentTime + TimeSpan.FromMinutes(11));
}
} }
[TestFixture] [TestFixture]
@ -324,4 +397,34 @@ public class PlayoutModeSchedulerBaseTests : SchedulerTestBase
new() { Duration = duration } new() { Duration = duration }
} }
}; };
private static PlayoutModeSchedulerBase<ProgramScheduleItem> Scheduler() =>
new TestScheduler();
private class TestScheduler : PlayoutModeSchedulerBase<ProgramScheduleItem>
{
private static readonly ILoggerFactory LoggerFactory;
static TestScheduler()
{
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.Console()
.CreateLogger();
LoggerFactory = new LoggerFactory().AddSerilog(Log.Logger);
}
public TestScheduler() : base(LoggerFactory.CreateLogger<TestScheduler>())
{
}
public override Tuple<PlayoutBuilderState, List<PlayoutItem>> Schedule(
PlayoutBuilderState playoutBuilderState,
Dictionary<CollectionKey, IMediaCollectionEnumerator> collectionEnumerators,
ProgramScheduleItem scheduleItem,
ProgramScheduleItem nextScheduleItem,
DateTimeOffset hardStop) =>
throw new NotSupportedException();
}
} }

48
ErsatzTV.Core/Scheduling/PlayoutModeSchedulerBase.cs

@ -191,11 +191,9 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe
.Append(Optional(scheduleItem.PostRollFiller)) .Append(Optional(scheduleItem.PostRollFiller))
.ToList(); .ToList();
if (allFiller.Count(f => f.PadToNearestMinute.HasValue) > 1) // multiple pad-to-nearest-minute values are invalid; use no filler
// if (allFiller.Map(f => Optional(f.PadToNearestMinute)).Sequence().Flatten().Distinct().Count() > 1) if (allFiller.Count(f => f.FillerMode == FillerMode.Pad && f.PadToNearestMinute.HasValue) > 1)
{ {
// multiple pad-to-nearest-minute values are invalid; use no filler
// TODO: log error?
return itemStartTime + itemDuration; return itemStartTime + itemDuration;
} }
@ -287,16 +285,17 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe
} }
} }
foreach (FillerPreset padFiller in Optional(allFiller.FirstOrDefault(f => f.PadToNearestMinute.HasValue))) foreach (FillerPreset padFiller in Optional(
allFiller.FirstOrDefault(f => f.FillerMode == FillerMode.Pad && f.PadToNearestMinute.HasValue)))
{ {
int currentMinute = (itemStartTime + totalDuration).Minute; int currentMinute = (itemStartTime + totalDuration).Minute;
// ReSharper disable once PossibleInvalidOperationException // ReSharper disable once PossibleInvalidOperationException
int targetMinute = (currentMinute + padFiller.PadToNearestMinute.Value - 1) / int targetMinute = (currentMinute + padFiller.PadToNearestMinute.Value - 1) /
padFiller.PadToNearestMinute.Value * padFiller.PadToNearestMinute.Value; padFiller.PadToNearestMinute.Value * padFiller.PadToNearestMinute.Value;
DateTimeOffset targetTime = itemStartTime + totalDuration - TimeSpan.FromMinutes(currentMinute) + DateTimeOffset targetTime = itemStartTime + totalDuration - TimeSpan.FromMinutes(currentMinute) +
TimeSpan.FromMinutes(targetMinute); TimeSpan.FromMinutes(targetMinute);
return new DateTimeOffset( return new DateTimeOffset(
targetTime.Year, targetTime.Year,
targetTime.Month, targetTime.Month,
@ -310,7 +309,7 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe
return itemStartTime + totalDuration; return itemStartTime + totalDuration;
} }
internal static List<PlayoutItem> AddFiller( internal List<PlayoutItem> AddFiller(
PlayoutBuilderState playoutBuilderState, PlayoutBuilderState playoutBuilderState,
Dictionary<CollectionKey, IMediaCollectionEnumerator> enumerators, Dictionary<CollectionKey, IMediaCollectionEnumerator> enumerators,
ProgramScheduleItem scheduleItem, ProgramScheduleItem scheduleItem,
@ -324,11 +323,10 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe
.Append(Optional(scheduleItem.PostRollFiller)) .Append(Optional(scheduleItem.PostRollFiller))
.ToList(); .ToList();
if (allFiller.Count(f => f.PadToNearestMinute.HasValue) > 1) // multiple pad-to-nearest-minute values are invalid; use no filler
// if (allFiller.Map(f => Optional(f.PadToNearestMinute)).Sequence().Flatten().Distinct().Count() > 1) if (allFiller.Count(f => f.FillerMode == FillerMode.Pad && f.PadToNearestMinute.HasValue) > 1)
{ {
// multiple pad-to-nearest-minute values are invalid; use no filler _logger.LogError("Multiple pad-to-nearest-minute values are invalid; no filler will be used");
// TODO: log error?
return new List<PlayoutItem> { playoutItem }; return new List<PlayoutItem> { playoutItem };
} }
@ -386,15 +384,18 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe
break; break;
case FillerMode.Count when filler.Count.HasValue: case FillerMode.Count when filler.Count.HasValue:
IMediaCollectionEnumerator e2 = enumerators[CollectionKey.ForFillerPreset(filler)]; IMediaCollectionEnumerator e2 = enumerators[CollectionKey.ForFillerPreset(filler)];
for (var i = 0; i < effectiveChapters.Count - 1; i++) for (var i = 0; i < effectiveChapters.Count; i++)
{ {
result.Add(playoutItem.ForChapter(effectiveChapters[i])); result.Add(playoutItem.ForChapter(effectiveChapters[i]));
result.AddRange( if (i < effectiveChapters.Count - 1)
AddCountFiller( {
playoutBuilderState, result.AddRange(
e2, AddCountFiller(
filler.Count.Value, playoutBuilderState,
FillerKind.MidRoll)); e2,
filler.Count.Value,
FillerKind.MidRoll));
}
} }
break; break;
@ -421,13 +422,14 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe
} }
// after all non-padded filler has been added, figure out padding // after all non-padded filler has been added, figure out padding
foreach (FillerPreset padFiller in Optional(allFiller.FirstOrDefault(f => f.PadToNearestMinute.HasValue))) foreach (FillerPreset padFiller in Optional(
allFiller.FirstOrDefault(f => f.FillerMode == FillerMode.Pad && f.PadToNearestMinute.HasValue)))
{ {
var totalDuration = var totalDuration =
TimeSpan.FromMilliseconds( TimeSpan.FromMilliseconds(
result.Sum(pi => (pi.Finish - pi.Start).TotalMilliseconds) + result.Sum(pi => (pi.Finish - pi.Start).TotalMilliseconds) +
effectiveChapters.Sum(c => (c.EndTime - c.StartTime).TotalMilliseconds)); effectiveChapters.Sum(c => (c.EndTime - c.StartTime).TotalMilliseconds));
int currentMinute = (playoutItem.StartOffset + totalDuration).Minute; int currentMinute = (playoutItem.StartOffset + totalDuration).Minute;
// ReSharper disable once PossibleInvalidOperationException // ReSharper disable once PossibleInvalidOperationException
int targetMinute = (currentMinute + padFiller.PadToNearestMinute.Value - 1) / int targetMinute = (currentMinute + padFiller.PadToNearestMinute.Value - 1) /
@ -437,7 +439,7 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe
TimeSpan.FromMinutes(currentMinute) + TimeSpan.FromMinutes(currentMinute) +
TimeSpan.FromMinutes(targetMinute); TimeSpan.FromMinutes(targetMinute);
var targetTime = new DateTimeOffset( var targetTime = new DateTimeOffset(
almostTargetTime.Year, almostTargetTime.Year,
almostTargetTime.Month, almostTargetTime.Month,
@ -532,7 +534,7 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe
} }
} }
} }
break; break;
case FillerKind.PostRoll: case FillerKind.PostRoll:
IMediaCollectionEnumerator post1 = enumerators[CollectionKey.ForFillerPreset(padFiller)]; IMediaCollectionEnumerator post1 = enumerators[CollectionKey.ForFillerPreset(padFiller)];

7
ErsatzTV/ViewModels/FillerPresetEditViewModel.cs

@ -15,6 +15,7 @@ public class FillerPresetEditViewModel
private FillerMode _fillerMode; private FillerMode _fillerMode;
private int? _count; private int? _count;
private ProgramScheduleItemCollectionType _collectionType; private ProgramScheduleItemCollectionType _collectionType;
private int? _padToNearestMinute;
public int Id { get; set; } public int Id { get; set; }
public string Name { get; set; } public string Name { get; set; }
@ -50,7 +51,11 @@ public class FillerPresetEditViewModel
set => _count = value; set => _count = value;
} }
public int? PadToNearestMinute { get; set; } public int? PadToNearestMinute
{
get => FillerMode == FillerMode.Pad ? _padToNearestMinute : null;
set => _padToNearestMinute = value;
}
public ProgramScheduleItemCollectionType CollectionType public ProgramScheduleItemCollectionType CollectionType
{ {

Loading…
Cancel
Save