Browse Source

add playout template duration scheduler (#1811)

* fix loop with missing content

* implement template duration scheduler
pull/1812/head
Jason Dove 1 year ago committed by GitHub
parent
commit
f3e5a4e7d8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 1
      ErsatzTV.Core/ErsatzTV.Core.csproj
  2. 15
      ErsatzTV.Core/Scheduling/TemplateScheduling/PlayoutTemplateDurationItem.cs
  3. 119
      ErsatzTV.Core/Scheduling/TemplateScheduling/PlayoutTemplateSchedulerDuration.cs
  4. 86
      ErsatzTV.Core/Scheduling/TemplateScheduling/PlayoutTemplateSchedulerPadToNext.cs
  5. 64
      ErsatzTV.Core/Scheduling/TemplateScheduling/TemplatePlayoutBuilder.cs

1
ErsatzTV.Core/ErsatzTV.Core.csproj

@ -27,6 +27,7 @@
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Serilog" Version="4.0.0" /> <PackageReference Include="Serilog" Version="4.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" /> <PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="TimeSpanParserUtil" Version="1.2.0" />
<PackageReference Include="YamlDotNet" Version="16.0.0" /> <PackageReference Include="YamlDotNet" Version="16.0.0" />
</ItemGroup> </ItemGroup>

15
ErsatzTV.Core/Scheduling/TemplateScheduling/PlayoutTemplateDurationItem.cs

@ -0,0 +1,15 @@
using YamlDotNet.Serialization;
namespace ErsatzTV.Core.Scheduling.TemplateScheduling;
public class PlayoutTemplateDurationItem : PlayoutTemplateItem
{
public string Duration { get; set; }
public bool Trim { get; set; }
public string Fallback { get; set; }
[YamlMember(Alias = "discard_attempts", ApplyNamingConventions = false)]
public int DiscardAttempts { get; set; }
}

119
ErsatzTV.Core/Scheduling/TemplateScheduling/PlayoutTemplateSchedulerDuration.cs

@ -0,0 +1,119 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Filler;
using ErsatzTV.Core.Interfaces.Scheduling;
using TimeSpanParserUtil;
namespace ErsatzTV.Core.Scheduling.TemplateScheduling;
public class PlayoutTemplateSchedulerDuration : PlayoutTemplateScheduler
{
public static DateTimeOffset Schedule(
Playout playout,
DateTimeOffset currentTime,
PlayoutTemplateDurationItem duration,
IMediaCollectionEnumerator enumerator,
Option<IMediaCollectionEnumerator> fallbackEnumerator)
{
// TODO: move to up-front validation somewhere
if (!TimeSpanParser.TryParse(duration.Duration, out TimeSpan timeSpan))
{
return currentTime;
}
DateTimeOffset targetTime = currentTime.Add(timeSpan);
return Schedule(
playout,
currentTime,
targetTime,
duration.DiscardAttempts,
duration.Trim,
enumerator,
fallbackEnumerator);
}
protected static DateTimeOffset Schedule(
Playout playout,
DateTimeOffset currentTime,
DateTimeOffset targetTime,
int discardAttempts,
bool trim,
IMediaCollectionEnumerator enumerator,
Option<IMediaCollectionEnumerator> fallbackEnumerator)
{
bool done = false;
TimeSpan remainingToFill = targetTime - currentTime;
while (!done && enumerator.Current.IsSome && remainingToFill > TimeSpan.Zero)
{
foreach (MediaItem mediaItem in enumerator.Current)
{
TimeSpan itemDuration = DurationForMediaItem(mediaItem);
var playoutItem = new PlayoutItem
{
MediaItemId = mediaItem.Id,
Start = currentTime.UtcDateTime,
Finish = currentTime.UtcDateTime + itemDuration,
InPoint = TimeSpan.Zero,
OutPoint = itemDuration,
//GuideGroup = playoutBuilderState.NextGuideGroup,
//FillerKind = fillerKind,
//DisableWatermarks = !allowWatermarks
};
if (remainingToFill - itemDuration >= TimeSpan.Zero)
{
remainingToFill -= itemDuration;
currentTime += itemDuration;
playout.Items.Add(playoutItem);
enumerator.MoveNext();
}
else if (discardAttempts > 0)
{
// item won't fit; try the next one
discardAttempts--;
enumerator.MoveNext();
}
else if (trim)
{
// trim item to exactly fit
remainingToFill = TimeSpan.Zero;
currentTime = targetTime;
playoutItem.Finish = targetTime.UtcDateTime;
playoutItem.OutPoint = playoutItem.Finish - playoutItem.Start;
playout.Items.Add(playoutItem);
enumerator.MoveNext();
}
else if (fallbackEnumerator.IsSome)
{
foreach (IMediaCollectionEnumerator fallback in fallbackEnumerator)
{
remainingToFill = TimeSpan.Zero;
done = true;
// replace with fallback content
foreach (MediaItem fallbackItem in fallback.Current)
{
playoutItem.MediaItemId = fallbackItem.Id;
playoutItem.Finish = targetTime.UtcDateTime;
playoutItem.FillerKind = FillerKind.Fallback;
playout.Items.Add(playoutItem);
fallback.MoveNext();
}
}
}
else
{
// item won't fit; we're done
done = true;
}
}
}
return targetTime;
}
}

86
ErsatzTV.Core/Scheduling/TemplateScheduling/PlayoutTemplateSchedulerPadToNext.cs

@ -1,10 +1,9 @@
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Filler;
using ErsatzTV.Core.Interfaces.Scheduling; using ErsatzTV.Core.Interfaces.Scheduling;
namespace ErsatzTV.Core.Scheduling.TemplateScheduling; namespace ErsatzTV.Core.Scheduling.TemplateScheduling;
public class PlayoutTemplateSchedulerPadToNext : PlayoutTemplateScheduler public class PlayoutTemplateSchedulerPadToNext : PlayoutTemplateSchedulerDuration
{ {
public static DateTimeOffset Schedule( public static DateTimeOffset Schedule(
Playout playout, Playout playout,
@ -33,80 +32,13 @@ public class PlayoutTemplateSchedulerPadToNext : PlayoutTemplateScheduler
if (targetTime <= currentTime) if (targetTime <= currentTime)
targetTime = targetTime.AddMinutes(padToNext.PadToNext); targetTime = targetTime.AddMinutes(padToNext.PadToNext);
int discardAttempts = padToNext.DiscardAttempts; return Schedule(
bool done = false; playout,
TimeSpan remainingToFill = targetTime - currentTime; currentTime,
while (!done && enumerator.Current.IsSome && remainingToFill > TimeSpan.Zero) targetTime,
{ padToNext.DiscardAttempts,
foreach (MediaItem mediaItem in enumerator.Current) padToNext.Trim,
{ enumerator,
TimeSpan itemDuration = DurationForMediaItem(mediaItem); fallbackEnumerator);
var playoutItem = new PlayoutItem
{
MediaItemId = mediaItem.Id,
Start = currentTime.UtcDateTime,
Finish = currentTime.UtcDateTime + itemDuration,
InPoint = TimeSpan.Zero,
OutPoint = itemDuration,
//GuideGroup = playoutBuilderState.NextGuideGroup,
//FillerKind = fillerKind,
//DisableWatermarks = !allowWatermarks
};
if (remainingToFill - itemDuration >= TimeSpan.Zero)
{
remainingToFill -= itemDuration;
currentTime += itemDuration;
playout.Items.Add(playoutItem);
enumerator.MoveNext();
}
else if (discardAttempts > 0)
{
// item won't fit; try the next one
discardAttempts--;
enumerator.MoveNext();
}
else if (padToNext.Trim)
{
// trim item to exactly fit
remainingToFill = TimeSpan.Zero;
currentTime = targetTime;
playoutItem.Finish = targetTime.UtcDateTime;
playoutItem.OutPoint = playoutItem.Finish - playoutItem.Start;
playout.Items.Add(playoutItem);
enumerator.MoveNext();
}
else if (fallbackEnumerator.IsSome)
{
foreach (IMediaCollectionEnumerator fallback in fallbackEnumerator)
{
remainingToFill = TimeSpan.Zero;
done = true;
// replace with fallback content
foreach (MediaItem fallbackItem in fallback.Current)
{
playoutItem.MediaItemId = fallbackItem.Id;
playoutItem.Finish = targetTime.UtcDateTime;
playoutItem.FillerKind = FillerKind.Fallback;
playout.Items.Add(playoutItem);
fallback.MoveNext();
}
}
}
else
{
// item won't fit; we're done
done = true;
}
}
}
return targetTime;
} }
} }

64
ErsatzTV.Core/Scheduling/TemplateScheduling/TemplatePlayoutBuilder.cs

@ -44,6 +44,7 @@ public class TemplatePlayoutBuilder(
// load content and content enumerators on demand // load content and content enumerators on demand
Dictionary<string, IMediaCollectionEnumerator> enumerators = new(); Dictionary<string, IMediaCollectionEnumerator> enumerators = new();
System.Collections.Generic.HashSet<string> missingContentKeys = [];
int itemsAfterRepeat = playout.Items.Count; int itemsAfterRepeat = playout.Items.Count;
var index = 0; var index = 0;
@ -80,31 +81,49 @@ public class TemplatePlayoutBuilder(
if (maybeEnumerator.IsNone) if (maybeEnumerator.IsNone)
{ {
logger.LogWarning("Unable to locate content with key {Key}", playoutItem.Content); if (!missingContentKeys.Contains(playoutItem.Content))
continue; {
logger.LogWarning("Unable to locate content with key {Key}", playoutItem.Content);
missingContentKeys.Add(playoutItem.Content);
}
} }
IMediaCollectionEnumerator enumerator = maybeEnumerator.ValueUnsafe(); foreach (IMediaCollectionEnumerator enumerator in maybeEnumerator)
switch (playoutItem)
{ {
case PlayoutTemplateCountItem count: switch (playoutItem)
currentTime = PlayoutTemplateSchedulerCount.Schedule(playout, currentTime, count, enumerator); {
break; case PlayoutTemplateCountItem count:
case PlayoutTemplatePadToNextItem padToNext: currentTime = PlayoutTemplateSchedulerCount.Schedule(playout, currentTime, count, enumerator);
Option<IMediaCollectionEnumerator> fallbackEnumerator = await GetCachedEnumeratorForContent( break;
playout, case PlayoutTemplateDurationItem duration:
playoutTemplate, Option<IMediaCollectionEnumerator> durationFallbackEnumerator = await GetCachedEnumeratorForContent(
enumerators, playout,
padToNext.Fallback, playoutTemplate,
cancellationToken); enumerators,
currentTime = PlayoutTemplateSchedulerPadToNext.Schedule( duration.Fallback,
playout, cancellationToken);
currentTime, currentTime = PlayoutTemplateSchedulerDuration.Schedule(
padToNext, playout,
enumerator, currentTime,
fallbackEnumerator); duration,
break; enumerator,
durationFallbackEnumerator);
break;
case PlayoutTemplatePadToNextItem padToNext:
Option<IMediaCollectionEnumerator> fallbackEnumerator = await GetCachedEnumeratorForContent(
playout,
playoutTemplate,
enumerators,
padToNext.Fallback,
cancellationToken);
currentTime = PlayoutTemplateSchedulerPadToNext.Schedule(
playout,
currentTime,
padToNext,
enumerator,
fallbackEnumerator);
break;
}
} }
index++; index++;
@ -190,6 +209,7 @@ public class TemplatePlayoutBuilder(
var keyMappings = new Dictionary<string, Type> var keyMappings = new Dictionary<string, Type>
{ {
{ "count", typeof(PlayoutTemplateCountItem) }, { "count", typeof(PlayoutTemplateCountItem) },
{ "duration", typeof(PlayoutTemplateDurationItem) },
{ "pad_to_next", typeof(PlayoutTemplatePadToNextItem) }, { "pad_to_next", typeof(PlayoutTemplatePadToNextItem) },
{ "repeat", typeof(PlayoutTemplateRepeatItem) } { "repeat", typeof(PlayoutTemplateRepeatItem) }
}; };

Loading…
Cancel
Save