Browse Source

rework pad and duration filler (#1304)

pull/1305/head
Jason Dove 2 years ago committed by GitHub
parent
commit
e8cbcc935f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      CHANGELOG.md
  2. 188
      ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerBaseTests.cs
  3. 92
      ErsatzTV.Core.Tests/Scheduling/ShuffledMediaCollectionEnumeratorTests.cs
  4. 1
      ErsatzTV.Core/Domain/CollectionEnumeratorState.cs
  5. 3
      ErsatzTV.Core/Interfaces/Scheduling/IMediaCollectionEnumerator.cs
  6. 10
      ErsatzTV.Core/Scheduling/ChronologicalMediaCollectionEnumerator.cs
  7. 16
      ErsatzTV.Core/Scheduling/CustomOrderCollectionEnumerator.cs
  8. 175
      ErsatzTV.Core/Scheduling/PlayoutModeSchedulerBase.cs
  9. 42
      ErsatzTV.Core/Scheduling/PlayoutModeSchedulerDuration.cs
  10. 44
      ErsatzTV.Core/Scheduling/PlayoutModeSchedulerFlood.cs
  11. 9
      ErsatzTV.Core/Scheduling/PlayoutModeSchedulerMultiple.cs
  12. 10
      ErsatzTV.Core/Scheduling/PlayoutModeSchedulerOne.cs
  13. 10
      ErsatzTV.Core/Scheduling/RandomizedMediaCollectionEnumerator.cs
  14. 10
      ErsatzTV.Core/Scheduling/SeasonEpisodeMediaCollectionEnumerator.cs
  15. 9
      ErsatzTV.Core/Scheduling/ShuffleInOrderCollectionEnumerator.cs
  16. 35
      ErsatzTV.Core/Scheduling/ShuffledMediaCollectionEnumerator.cs
  17. 45
      ErsatzTV.Infrastructure/Scheduling/MultiEpisodeShuffleCollectionEnumerator.cs

5
CHANGELOG.md

@ -16,6 +16,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -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

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

@ -23,189 +23,6 @@ public class PlayoutModeSchedulerBaseTests : SchedulerTestBase @@ -23,189 +23,6 @@ public class PlayoutModeSchedulerBaseTests : SchedulerTestBase
private CancellationToken _cancellationToken;
private PlayoutModeSchedulerBase<ProgramScheduleItem> _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<MediaItem>()
};
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<ProgramScheduleItem>
.CalculateEndTimeWithFiller(
new Dictionary<CollectionKey, IMediaCollectionEnumerator>
{
{ 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<MediaChapter>());
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<ProgramScheduleItem>
.CalculateEndTimeWithFiller(
new Dictionary<CollectionKey, IMediaCollectionEnumerator>(),
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<MediaChapter>());
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<ProgramScheduleItem>
.CalculateEndTimeWithFiller(
new Dictionary<CollectionKey, IMediaCollectionEnumerator>(),
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<MediaChapter>());
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<ProgramScheduleItem>
.CalculateEndTimeWithFiller(
new Dictionary<CollectionKey, IMediaCollectionEnumerator>(),
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<MediaChapter>());
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<ProgramScheduleItem>
.CalculateEndTimeWithFiller(
new Dictionary<CollectionKey, IMediaCollectionEnumerator>(),
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<MediaChapter>());
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<ProgramScheduleItem>
.CalculateEndTimeWithFiller(
new Dictionary<CollectionKey, IMediaCollectionEnumerator>(),
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<MediaChapter>());
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<ProgramScheduleItem>
.CalculateEndTimeWithFiller(
new Dictionary<CollectionKey, IMediaCollectionEnumerator>(),
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<MediaChapter>());
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 @@ -241,6 +58,7 @@ public class PlayoutModeSchedulerBaseTests : SchedulerTestBase
scheduleItem,
new PlayoutItem(),
new List<MediaChapter>(),
log: true,
_cancellationToken);
playoutItems.Count.Should().Be(1);
@ -293,6 +111,7 @@ public class PlayoutModeSchedulerBaseTests : SchedulerTestBase @@ -293,6 +111,7 @@ public class PlayoutModeSchedulerBaseTests : SchedulerTestBase
scheduleItem,
new PlayoutItem(),
new List<MediaChapter> { new() },
log: true,
_cancellationToken);
playoutItems.Count.Should().Be(1);
@ -359,6 +178,7 @@ public class PlayoutModeSchedulerBaseTests : SchedulerTestBase @@ -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 @@ -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 @@ -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);

92
ErsatzTV.Core.Tests/Scheduling/ShuffledMediaCollectionEnumeratorTests.cs

@ -1,92 +0,0 @@ @@ -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<GroupedMediaItem> _mediaItems = new()
{
new GroupedMediaItem(new MediaItem { Id = 1 }, new List<MediaItem>()),
new GroupedMediaItem(new MediaItem { Id = 2 }, new List<MediaItem>()),
new GroupedMediaItem(new MediaItem { Id = 3 }, new List<MediaItem>())
};
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<MediaItem> peek = enumerator.Peek(0);
Option<MediaItem> 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<MediaItem> peek = enumerator.Peek(1);
enumerator.MoveNext();
Option<MediaItem> 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<MediaItem> peek = enumerator.Peek(2);
enumerator.MoveNext();
enumerator.MoveNext();
Option<MediaItem> 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<MediaItem> peek = enumerator.Peek(3);
enumerator.MoveNext();
enumerator.MoveNext();
enumerator.MoveNext();
Option<MediaItem> next = enumerator.Current;
peek.IsSome.Should().BeTrue();
next.IsSome.Should().BeTrue();
peek.ValueUnsafe().Id.Should().Be(2);
next.ValueUnsafe().Id.Should().Be(2);
}
}

1
ErsatzTV.Core/Domain/CollectionEnumeratorState.cs

@ -4,4 +4,5 @@ public class CollectionEnumeratorState @@ -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 };
}

3
ErsatzTV.Core/Interfaces/Scheduling/IMediaCollectionEnumerator.cs

@ -4,9 +4,10 @@ namespace ErsatzTV.Core.Interfaces.Scheduling; @@ -4,9 +4,10 @@ namespace ErsatzTV.Core.Interfaces.Scheduling;
public interface IMediaCollectionEnumerator
{
IMediaCollectionEnumerator Clone(CollectionEnumeratorState state, CancellationToken cancellationToken);
CollectionEnumeratorState State { get; }
Option<MediaItem> Current { get; }
void MoveNext();
Option<MediaItem> Peek(int offset);
int Count { get; }
Option<TimeSpan> MinimumDuration { get; }
}

10
ErsatzTV.Core/Scheduling/ChronologicalMediaCollectionEnumerator.cs

@ -31,14 +31,18 @@ public sealed class ChronologicalMediaCollectionEnumerator : IMediaCollectionEnu @@ -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<MediaItem> Current => _sortedMediaItems.Any() ? _sortedMediaItems[State.Index] : None;
public void MoveNext() => State.Index = (State.Index + 1) % _sortedMediaItems.Count;
public Option<MediaItem> Peek(int offset) =>
_sortedMediaItems.Any() ? _sortedMediaItems[(State.Index + offset) % _sortedMediaItems.Count] : None;
public Option<TimeSpan> MinimumDuration => _lazyMinimumDuration.Value;
public int Count => _sortedMediaItems.Count;
}

16
ErsatzTV.Core/Scheduling/CustomOrderCollectionEnumerator.cs

@ -6,6 +6,9 @@ namespace ErsatzTV.Core.Scheduling; @@ -6,6 +6,9 @@ namespace ErsatzTV.Core.Scheduling;
public class CustomOrderCollectionEnumerator : IMediaCollectionEnumerator
{
private readonly Collection _collection;
private readonly IList<MediaItem> _mediaItems;
private readonly IList<MediaItem> _sortedMediaItems;
private readonly Lazy<Option<TimeSpan>> _lazyMinimumDuration;
@ -14,6 +17,9 @@ public class CustomOrderCollectionEnumerator : IMediaCollectionEnumerator @@ -14,6 +17,9 @@ public class CustomOrderCollectionEnumerator : IMediaCollectionEnumerator
IList<MediaItem> 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 @@ -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<MediaItem> Current => _sortedMediaItems.Any() ? _sortedMediaItems[State.Index] : None;
public void MoveNext() => State.Index = (State.Index + 1) % _sortedMediaItems.Count;
public Option<MediaItem> Peek(int offset) =>
throw new NotSupportedException();
public Option<TimeSpan> MinimumDuration => _lazyMinimumDuration.Value;
public int Count => _sortedMediaItems.Count;
}

175
ErsatzTV.Core/Scheduling/PlayoutModeSchedulerBase.cs

@ -4,6 +4,8 @@ using ErsatzTV.Core.Extensions; @@ -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<T> : IPlayoutModeScheduler<T> whe @@ -195,142 +197,13 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe
PlayoutBuilder.DisplayTitle(mediaItem),
startTime);
internal static DateTimeOffset CalculateEndTimeWithFiller(
Dictionary<CollectionKey, IMediaCollectionEnumerator> enumerators,
ProgramScheduleItem scheduleItem,
DateTimeOffset itemStartTime,
TimeSpan itemDuration,
List<MediaChapter> 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<PlayoutItem> AddFiller(
PlayoutBuilderState playoutBuilderState,
Dictionary<CollectionKey, IMediaCollectionEnumerator> enumerators,
ProgramScheduleItem scheduleItem,
PlayoutItem playoutItem,
List<MediaChapter> chapters,
bool log,
CancellationToken cancellationToken)
{
var result = new List<PlayoutItem>();
@ -367,6 +240,7 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe @@ -367,6 +240,7 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> 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<T> : IPlayoutModeScheduler<T> whe @@ -408,6 +282,7 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe
filler.Duration.Value,
FillerKind.MidRoll,
filler.AllowWatermarks,
log,
cancellationToken));
}
}
@ -450,6 +325,7 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe @@ -450,6 +325,7 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> 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<T> : IPlayoutModeScheduler<T> whe @@ -518,6 +394,7 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> 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<T> : IPlayoutModeScheduler<T> whe @@ -544,6 +421,7 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe
remainingToFill,
FillerKind.MidRoll,
padFiller.AllowWatermarks,
log,
cancellationToken));
TimeSpan average = effectiveChapters.Count <= 1
? remainingToFill
@ -607,6 +485,7 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe @@ -607,6 +485,7 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> 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<T> : IPlayoutModeScheduler<T> whe @@ -682,13 +561,15 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe
TimeSpan duration,
FillerKind fillerKind,
bool allowWatermarks,
bool log,
CancellationToken cancellationToken)
{
var result = new List<PlayoutItem>();
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<T> : IPlayoutModeScheduler<T> whe @@ -712,40 +593,30 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> 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;
}

42
ErsatzTV.Core/Scheduling/PlayoutModeSchedulerDuration.cs

@ -121,25 +121,41 @@ public class PlayoutModeSchedulerDuration : PlayoutModeSchedulerBase<ProgramSche @@ -121,25 +121,41 @@ public class PlayoutModeSchedulerDuration : PlayoutModeSchedulerBase<ProgramSche
durationUntil.Do(du => playoutItem.GuideFinish = du.UtcDateTime);
DateTimeOffset durationFinish = nextState.DurationFinish.IfNone(SystemTime.MaxValueUtc);
DateTimeOffset itemEndTimeWithFiller = CalculateEndTimeWithFiller(
collectionEnumerators,
var enumeratorClones = new Dictionary<CollectionKey, IMediaCollectionEnumerator>();
foreach ((CollectionKey key, IMediaCollectionEnumerator enumerator) in collectionEnumerators)
{
IMediaCollectionEnumerator clone = enumerator.Clone(enumerator.State.Clone(), cancellationToken);
enumeratorClones.Add(key, clone);
}
List<PlayoutItem> 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
{

44
ErsatzTV.Core/Scheduling/PlayoutModeSchedulerFlood.cs

@ -74,12 +74,23 @@ public class PlayoutModeSchedulerFlood : PlayoutModeSchedulerBase<ProgramSchedul @@ -74,12 +74,23 @@ public class PlayoutModeSchedulerFlood : PlayoutModeSchedulerBase<ProgramSchedul
? GetStartTimeAfter(nextState with { InFlood = false }, peekScheduleItem)
: DateTimeOffset.MaxValue;
DateTimeOffset itemEndTimeWithFiller = CalculateEndTimeWithFiller(
collectionEnumerators,
var enumeratorClones = new Dictionary<CollectionKey, IMediaCollectionEnumerator>();
foreach ((CollectionKey key, IMediaCollectionEnumerator enumerator) in collectionEnumerators)
{
IMediaCollectionEnumerator clone = enumerator.Clone(enumerator.State.Clone(), cancellationToken);
enumeratorClones.Add(key, clone);
}
List<PlayoutItem> 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<ProgramSchedul @@ -88,27 +99,16 @@ public class PlayoutModeSchedulerFlood : PlayoutModeSchedulerBase<ProgramSchedul
if (willFinishInTime)
{
playoutItems.AddRange(
AddFiller(
nextState,
collectionEnumerators,
scheduleItem,
playoutItem,
itemChapters,
cancellationToken));
playoutItems.AddRange(maybePlayoutItems);
// LogScheduledItem(scheduleItem, mediaItem, itemStartTime);
if (playoutItems.Count > 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();
}
}

9
ErsatzTV.Core/Scheduling/PlayoutModeSchedulerMultiple.cs

@ -77,12 +77,6 @@ public class PlayoutModeSchedulerMultiple : PlayoutModeSchedulerBase<ProgramSche @@ -77,12 +77,6 @@ public class PlayoutModeSchedulerMultiple : PlayoutModeSchedulerBase<ProgramSche
};
// LogScheduledItem(scheduleItem, mediaItem, itemStartTime);
DateTimeOffset itemEndTimeWithFiller = CalculateEndTimeWithFiller(
collectionEnumerators,
scheduleItem,
itemStartTime,
itemDuration,
itemChapters);
playoutItems.AddRange(
AddFiller(
@ -91,11 +85,12 @@ public class PlayoutModeSchedulerMultiple : PlayoutModeSchedulerBase<ProgramSche @@ -91,11 +85,12 @@ public class PlayoutModeSchedulerMultiple : PlayoutModeSchedulerBase<ProgramSche
scheduleItem,
playoutItem,
itemChapters,
log: true,
cancellationToken));
nextState = nextState with
{
CurrentTime = itemEndTimeWithFiller,
CurrentTime = playoutItems.Max(pi => pi.FinishOffset),
MultipleRemaining = nextState.MultipleRemaining.Map(i => i - 1),
// only bump guide group if we don't have a custom title

10
ErsatzTV.Core/Scheduling/PlayoutModeSchedulerOne.cs

@ -53,24 +53,18 @@ public class PlayoutModeSchedulerOne : PlayoutModeSchedulerBase<ProgramScheduleI @@ -53,24 +53,18 @@ public class PlayoutModeSchedulerOne : PlayoutModeSchedulerBase<ProgramScheduleI
SubtitleMode = scheduleItem.SubtitleMode
};
DateTimeOffset itemEndTimeWithFiller = CalculateEndTimeWithFiller(
collectionEnumerators,
scheduleItem,
itemStartTime,
itemDuration,
itemChapters);
List<PlayoutItem> 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();

10
ErsatzTV.Core/Scheduling/RandomizedMediaCollectionEnumerator.cs

@ -26,6 +26,11 @@ public class RandomizedMediaCollectionEnumerator : IMediaCollectionEnumerator @@ -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<MediaItem> Current => _mediaItems.Any() ? _mediaItems[_index] : None;
@ -36,8 +41,7 @@ public class RandomizedMediaCollectionEnumerator : IMediaCollectionEnumerator @@ -36,8 +41,7 @@ public class RandomizedMediaCollectionEnumerator : IMediaCollectionEnumerator
State.Index++;
}
public Option<MediaItem> Peek(int offset) =>
throw new NotSupportedException();
public Option<TimeSpan> MinimumDuration => _lazyMinimumDuration.Value;
public int Count => _mediaItems.Count;
}

10
ErsatzTV.Core/Scheduling/SeasonEpisodeMediaCollectionEnumerator.cs

@ -31,14 +31,18 @@ public sealed class SeasonEpisodeMediaCollectionEnumerator : IMediaCollectionEnu @@ -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<MediaItem> Current => _sortedMediaItems.Any() ? _sortedMediaItems[State.Index] : None;
public void MoveNext() => State.Index = (State.Index + 1) % _sortedMediaItems.Count;
public Option<MediaItem> Peek(int offset) =>
_sortedMediaItems.Any() ? _sortedMediaItems[(State.Index + offset) % _sortedMediaItems.Count] : None;
public Option<TimeSpan> MinimumDuration => _lazyMinimumDuration.Value;
public int Count => _sortedMediaItems.Count;
}

9
ErsatzTV.Core/Scheduling/ShuffleInOrderCollectionEnumerator.cs

@ -42,6 +42,11 @@ public class ShuffleInOrderCollectionEnumerator : IMediaCollectionEnumerator @@ -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<MediaItem> Current => _shuffled.Any() ? _shuffled[State.Index % _mediaItemCount] : None;
@ -69,8 +74,6 @@ public class ShuffleInOrderCollectionEnumerator : IMediaCollectionEnumerator @@ -69,8 +74,6 @@ public class ShuffleInOrderCollectionEnumerator : IMediaCollectionEnumerator
State.Index %= _shuffled.Count;
}
public Option<MediaItem> Peek(int offset) => throw new NotSupportedException();
private IList<MediaItem> Shuffle(IList<CollectionWithItems> collections, Random random)
{
// based on https://keyj.emphy.de/balanced-shuffle/
@ -208,4 +211,6 @@ public class ShuffleInOrderCollectionEnumerator : IMediaCollectionEnumerator @@ -208,4 +211,6 @@ public class ShuffleInOrderCollectionEnumerator : IMediaCollectionEnumerator
}
public Option<TimeSpan> MinimumDuration => _lazyMinimumDuration.Value;
public int Count => _shuffled.Count;
}

35
ErsatzTV.Core/Scheduling/ShuffledMediaCollectionEnumerator.cs

@ -39,6 +39,11 @@ public class ShuffledMediaCollectionEnumerator : IMediaCollectionEnumerator @@ -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<MediaItem> Current => _shuffled.Any() ? _shuffled[State.Index % _mediaItemCount] : None;
@ -66,34 +71,6 @@ public class ShuffledMediaCollectionEnumerator : IMediaCollectionEnumerator @@ -66,34 +71,6 @@ public class ShuffledMediaCollectionEnumerator : IMediaCollectionEnumerator
State.Index %= _mediaItemCount;
}
public Option<MediaItem> Peek(int offset)
{
if (offset == 0)
{
return Current;
}
if ((State.Index + offset) % _mediaItemCount == 0)
{
IList<MediaItem> shuffled;
Option<MediaItem> 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<MediaItem> Shuffle(IEnumerable<GroupedMediaItem> list, CloneableRandom random)
{
GroupedMediaItem[] copy = list.ToArray();
@ -110,4 +87,6 @@ public class ShuffledMediaCollectionEnumerator : IMediaCollectionEnumerator @@ -110,4 +87,6 @@ public class ShuffledMediaCollectionEnumerator : IMediaCollectionEnumerator
}
public Option<TimeSpan> MinimumDuration => _lazyMinimumDuration.Value;
public int Count => _shuffled.Count;
}

45
ErsatzTV.Infrastructure/Scheduling/MultiEpisodeShuffleCollectionEnumerator.cs

@ -10,6 +10,8 @@ namespace ErsatzTV.Infrastructure.Scheduling; @@ -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<int, List<MediaItem>> _mediaItemGroups;
@ -26,6 +28,8 @@ public class MultiEpisodeShuffleCollectionEnumerator : IMediaCollectionEnumerato @@ -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 @@ -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<MediaItem> Current => _shuffled.Any() ? _shuffled[State.Index % _mediaItemCount] : None;
@ -110,34 +125,6 @@ public class MultiEpisodeShuffleCollectionEnumerator : IMediaCollectionEnumerato @@ -110,34 +125,6 @@ public class MultiEpisodeShuffleCollectionEnumerator : IMediaCollectionEnumerato
State.Index %= _mediaItemCount;
}
public Option<MediaItem> Peek(int offset)
{
if (offset == 0)
{
return Current;
}
if ((State.Index + offset) % _mediaItemCount == 0)
{
IList<MediaItem> shuffled;
Option<MediaItem> 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<MediaItem> Shuffle(CloneableRandom random)
{
int maxGroupNumber = _mediaItemGroups.Max(a => a.Key);
@ -202,4 +189,6 @@ public class MultiEpisodeShuffleCollectionEnumerator : IMediaCollectionEnumerato @@ -202,4 +189,6 @@ public class MultiEpisodeShuffleCollectionEnumerator : IMediaCollectionEnumerato
}
public Option<TimeSpan> MinimumDuration => _lazyMinimumDuration.Value;
public int Count => _shuffled.Count;
}

Loading…
Cancel
Save