diff --git a/ErsatzTV.Core/ErsatzTV.Core.csproj b/ErsatzTV.Core/ErsatzTV.Core.csproj index 1df6568f..2d24122d 100644 --- a/ErsatzTV.Core/ErsatzTV.Core.csproj +++ b/ErsatzTV.Core/ErsatzTV.Core.csproj @@ -27,6 +27,7 @@ + diff --git a/ErsatzTV.Core/Scheduling/TemplateScheduling/PlayoutTemplateDurationItem.cs b/ErsatzTV.Core/Scheduling/TemplateScheduling/PlayoutTemplateDurationItem.cs new file mode 100644 index 00000000..ca728247 --- /dev/null +++ b/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; } +} diff --git a/ErsatzTV.Core/Scheduling/TemplateScheduling/PlayoutTemplateSchedulerDuration.cs b/ErsatzTV.Core/Scheduling/TemplateScheduling/PlayoutTemplateSchedulerDuration.cs new file mode 100644 index 00000000..eced7c73 --- /dev/null +++ b/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 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 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; + } +} diff --git a/ErsatzTV.Core/Scheduling/TemplateScheduling/PlayoutTemplateSchedulerPadToNext.cs b/ErsatzTV.Core/Scheduling/TemplateScheduling/PlayoutTemplateSchedulerPadToNext.cs index 7aef99d1..a8f16225 100644 --- a/ErsatzTV.Core/Scheduling/TemplateScheduling/PlayoutTemplateSchedulerPadToNext.cs +++ b/ErsatzTV.Core/Scheduling/TemplateScheduling/PlayoutTemplateSchedulerPadToNext.cs @@ -1,10 +1,9 @@ using ErsatzTV.Core.Domain; -using ErsatzTV.Core.Domain.Filler; using ErsatzTV.Core.Interfaces.Scheduling; namespace ErsatzTV.Core.Scheduling.TemplateScheduling; -public class PlayoutTemplateSchedulerPadToNext : PlayoutTemplateScheduler +public class PlayoutTemplateSchedulerPadToNext : PlayoutTemplateSchedulerDuration { public static DateTimeOffset Schedule( Playout playout, @@ -33,80 +32,13 @@ public class PlayoutTemplateSchedulerPadToNext : PlayoutTemplateScheduler if (targetTime <= currentTime) targetTime = targetTime.AddMinutes(padToNext.PadToNext); - int discardAttempts = padToNext.DiscardAttempts; - 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 (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; + return Schedule( + playout, + currentTime, + targetTime, + padToNext.DiscardAttempts, + padToNext.Trim, + enumerator, + fallbackEnumerator); } } diff --git a/ErsatzTV.Core/Scheduling/TemplateScheduling/TemplatePlayoutBuilder.cs b/ErsatzTV.Core/Scheduling/TemplateScheduling/TemplatePlayoutBuilder.cs index 2b5d51a1..a0fb69fe 100644 --- a/ErsatzTV.Core/Scheduling/TemplateScheduling/TemplatePlayoutBuilder.cs +++ b/ErsatzTV.Core/Scheduling/TemplateScheduling/TemplatePlayoutBuilder.cs @@ -44,6 +44,7 @@ public class TemplatePlayoutBuilder( // load content and content enumerators on demand Dictionary enumerators = new(); + System.Collections.Generic.HashSet missingContentKeys = []; int itemsAfterRepeat = playout.Items.Count; var index = 0; @@ -80,31 +81,49 @@ public class TemplatePlayoutBuilder( if (maybeEnumerator.IsNone) { - logger.LogWarning("Unable to locate content with key {Key}", playoutItem.Content); - continue; + if (!missingContentKeys.Contains(playoutItem.Content)) + { + logger.LogWarning("Unable to locate content with key {Key}", playoutItem.Content); + missingContentKeys.Add(playoutItem.Content); + } } - IMediaCollectionEnumerator enumerator = maybeEnumerator.ValueUnsafe(); - - switch (playoutItem) + foreach (IMediaCollectionEnumerator enumerator in maybeEnumerator) { - case PlayoutTemplateCountItem count: - currentTime = PlayoutTemplateSchedulerCount.Schedule(playout, currentTime, count, enumerator); - break; - case PlayoutTemplatePadToNextItem padToNext: - Option fallbackEnumerator = await GetCachedEnumeratorForContent( - playout, - playoutTemplate, - enumerators, - padToNext.Fallback, - cancellationToken); - currentTime = PlayoutTemplateSchedulerPadToNext.Schedule( - playout, - currentTime, - padToNext, - enumerator, - fallbackEnumerator); - break; + switch (playoutItem) + { + case PlayoutTemplateCountItem count: + currentTime = PlayoutTemplateSchedulerCount.Schedule(playout, currentTime, count, enumerator); + break; + case PlayoutTemplateDurationItem duration: + Option durationFallbackEnumerator = await GetCachedEnumeratorForContent( + playout, + playoutTemplate, + enumerators, + duration.Fallback, + cancellationToken); + currentTime = PlayoutTemplateSchedulerDuration.Schedule( + playout, + currentTime, + duration, + enumerator, + durationFallbackEnumerator); + break; + case PlayoutTemplatePadToNextItem padToNext: + Option fallbackEnumerator = await GetCachedEnumeratorForContent( + playout, + playoutTemplate, + enumerators, + padToNext.Fallback, + cancellationToken); + currentTime = PlayoutTemplateSchedulerPadToNext.Schedule( + playout, + currentTime, + padToNext, + enumerator, + fallbackEnumerator); + break; + } } index++; @@ -190,6 +209,7 @@ public class TemplatePlayoutBuilder( var keyMappings = new Dictionary { { "count", typeof(PlayoutTemplateCountItem) }, + { "duration", typeof(PlayoutTemplateDurationItem) }, { "pad_to_next", typeof(PlayoutTemplatePadToNextItem) }, { "repeat", typeof(PlayoutTemplateRepeatItem) } };