Browse Source

add yaml mid roll instruction (#2232)

* refactor filler expression

* add yaml mid roll instruction

* schedule midroll for yaml count and all instructions

* update changelog
pull/2233/head
Jason Dove 5 months ago committed by GitHub
parent
commit
06d9e59a7a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 8
      CHANGELOG.md
  2. 4
      ErsatzTV.Core.Tests/Scheduling/FillerExpressionTests.cs
  3. 12
      ErsatzTV.Core/Domain/PlayoutItem.cs
  4. 7
      ErsatzTV.Core/Scheduling/FillerExpression.cs
  5. 4
      ErsatzTV.Core/Scheduling/PlayoutModeSchedulerBase.cs
  6. 2
      ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutAllHandler.cs
  7. 52
      ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutContentHandler.cs
  8. 2
      ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutCountHandler.cs
  9. 34
      ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutMidRollHandler.cs
  10. 13
      ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlMidRollInstruction.cs
  11. 2
      ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutBuilder.cs
  12. 15
      ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutContext.cs
  13. 12
      ErsatzTV/Resources/yaml-playout.schema.json

8
CHANGELOG.md

@ -18,6 +18,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -18,6 +18,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- YAML playout: add `post_roll` instruction to enable and disable a post-roll sequence
- With value of `true` and `sequence` property, will enable automatic post-roll for all content in the playout to the sequence with the provided key
- With value of `false`, will disable automatic post-roll in the playout
- YAML playout: add `mid_roll` instruction to enable and disable a mid-roll sequence
- With value of `true` and `sequence` property, will enable automatic mid-roll for (`count` and `all`) content in the playout to the sequence with the provided key
- With value of `false`, will disable automatic post-roll in the playout
- `expression` can be used to influence which chapters are selected for mid roll (same as in filler preset)
- Add YAML playout validation (using JSON Schema)
- Invalid YAML playout definitions will fail to build and will log validation failures as warnings
- `content` is fully validated
@ -27,8 +31,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -27,8 +31,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Add `Playlist` collection type to filler presets
- This will force filler mode `Count`
- Whenever the filler is used, it will schedule `Count` times full time through the playlist
- If the playlist has 3 items and none configured to play all, it will schedule 3 items when `Count = 1`
- If the playlist has 3 items and none configured to play all, it will schedule 6 items when `Count = 2`
- If the playlist has 3 items and none set to play all, it will schedule 3 items when `Count = 1`
- If the playlist has 3 items and none set to play all, it will schedule 6 items when `Count = 2`
- Using the same playlist in the same schedule for anything other than filler may cause undesired behavior
### Fixed

4
ErsatzTV.Core.Tests/Scheduling/FillerExpressionTests.cs

@ -34,7 +34,7 @@ public class FillerExpressionTests @@ -34,7 +34,7 @@ public class FillerExpressionTests
Expression = "(point > 5 * 60) and (last_mid_filler > 5 * 60) and (matched_points < 2)"
};
List<MediaChapter> result = FillerExpression.FilterChapters(fillerPreset, chapters, playoutItem);
List<MediaChapter> result = FillerExpression.FilterChapters(fillerPreset.Expression, chapters, playoutItem);
result.Count.ShouldBe(3);
result[0].EndTime.ShouldBe(TimeSpan.FromMinutes(10));
@ -67,7 +67,7 @@ public class FillerExpressionTests @@ -67,7 +67,7 @@ public class FillerExpressionTests
Expression = "(total_progress >= 0.2 and matched_points = 0) or (total_progress >= 0.6 and matched_points = 1)"
};
List<MediaChapter> result = FillerExpression.FilterChapters(fillerPreset, chapters, playoutItem);
List<MediaChapter> result = FillerExpression.FilterChapters(fillerPreset.Expression, chapters, playoutItem);
result.Count.ShouldBe(3);
result[0].EndTime.ShouldBe(TimeSpan.FromMinutes(10));

12
ErsatzTV.Core/Domain/PlayoutItem.cs

@ -54,7 +54,17 @@ public class PlayoutItem @@ -54,7 +54,17 @@ public class PlayoutItem
Playout = Playout,
InPoint = chapter.StartTime,
OutPoint = chapter.EndTime,
ChapterTitle = chapter.Title
ChapterTitle = chapter.Title,
Watermark = Watermark,
WatermarkId = WatermarkId,
DisableWatermarks = DisableWatermarks,
PreferredAudioLanguageCode = PreferredAudioLanguageCode,
PreferredAudioTitle = PreferredAudioTitle,
PreferredSubtitleLanguageCode = PreferredSubtitleLanguageCode,
SubtitleMode = SubtitleMode,
BlockKey = BlockKey,
CollectionKey = CollectionKey,
CollectionEtag = CollectionEtag
};
public string GetDisplayDuration()

7
ErsatzTV.Core/Scheduling/FillerExpression.cs

@ -1,14 +1,13 @@ @@ -1,14 +1,13 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Filler;
using NCalc;
namespace ErsatzTV.Core.Scheduling;
public static class FillerExpression
{
public static List<MediaChapter> FilterChapters(FillerPreset fillerPreset, List<MediaChapter> effectiveChapters, PlayoutItem playoutItem)
public static List<MediaChapter> FilterChapters(string fillerExpression, List<MediaChapter> effectiveChapters, PlayoutItem playoutItem)
{
if (effectiveChapters.Count == 0 || fillerPreset is null || string.IsNullOrWhiteSpace(fillerPreset.Expression))
if (effectiveChapters.Count == 0 || string.IsNullOrWhiteSpace(fillerExpression))
{
return effectiveChapters;
}
@ -27,7 +26,7 @@ public static class FillerExpression @@ -27,7 +26,7 @@ public static class FillerExpression
for (var index = 0; index < chapterPoints.Count; index++)
{
TimeSpan chapterPoint = chapterPoints[index];
var expression = new Expression(fillerPreset.Expression);
var expression = new Expression(fillerExpression);
int chapterNum = index + 1;
double sinceLastFiller = chapterPoint.TotalSeconds - lastFiller;
int matchedPoints = matches;

4
ErsatzTV.Core/Scheduling/PlayoutModeSchedulerBase.cs

@ -403,7 +403,7 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe @@ -403,7 +403,7 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe
foreach (FillerPreset filler in allFiller.Filter(f =>
f.FillerKind == FillerKind.MidRoll && f.FillerMode != FillerMode.Pad))
{
List<MediaChapter> filteredChapters = FillerExpression.FilterChapters(filler, effectiveChapters, playoutItem);
List<MediaChapter> filteredChapters = FillerExpression.FilterChapters(filler.Expression, effectiveChapters, playoutItem);
if (filteredChapters.Count <= 1)
{
result.Add(playoutItem);
@ -529,7 +529,7 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe @@ -529,7 +529,7 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe
var totalDuration = TimeSpan.FromTicks(result.Sum(pi => (pi.Finish - pi.Start).Ticks));
List<MediaChapter> filteredChapters =
FillerExpression.FilterChapters(padFiller, effectiveChapters, playoutItem);
FillerExpression.FilterChapters(padFiller.Expression, effectiveChapters, playoutItem);
FillerKind fillerKind = padFiller.FillerKind;
if (filteredChapters.Count <= 1 && effectiveChapters.Count > 1)

2
ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutAllHandler.cs

@ -71,7 +71,7 @@ public class YamlPlayoutAllHandler(EnumeratorCache enumeratorCache) : YamlPlayou @@ -71,7 +71,7 @@ public class YamlPlayoutAllHandler(EnumeratorCache enumeratorCache) : YamlPlayou
playoutItem.WatermarkId = watermarkId;
}
context.Playout.Items.Add(playoutItem);
await AddItemAndMidRoll(context, playoutItem, executeSequence);
context.AdvanceGuideGroup();
// create history record

52
ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutContentHandler.cs

@ -163,4 +163,56 @@ public abstract class YamlPlayoutContentHandler(EnumeratorCache enumeratorCache) @@ -163,4 +163,56 @@ public abstract class YamlPlayoutContentHandler(EnumeratorCache enumeratorCache)
return FillerKind.None;
}
protected static List<MediaChapter> ChaptersForMediaItem(MediaItem mediaItem)
{
MediaVersion version = mediaItem.GetHeadVersion();
return Optional(version.Chapters).Flatten().OrderBy(c => c.StartTime).ToList();
}
protected static async Task AddItemAndMidRoll(YamlPlayoutContext context, PlayoutItem playoutItem,
Func<string, Task> executeSequence)
{
List<MediaChapter> itemChapters = ChaptersForMediaItem(playoutItem.MediaItem);
Option<YamlPlayoutContext.MidRollSequence> maybeMidRollSequence = context.GetMidRollSequence();
if (itemChapters.Count < 2 || maybeMidRollSequence.IsNone)
{
context.Playout.Items.Add(playoutItem);
}
else
{
foreach (var midRollSequence in maybeMidRollSequence)
{
var filteredChapters = FillerExpression.FilterChapters(
midRollSequence.Expression,
itemChapters,
playoutItem);
if (filteredChapters.Count < 2)
{
context.Playout.Items.Add(playoutItem);
}
else
{
for (var j = 0; j < filteredChapters.Count; j++)
{
var nextItem = playoutItem.ForChapter(filteredChapters[j]);
nextItem.Start = context.CurrentTime.UtcDateTime;
nextItem.Finish = context.CurrentTime.UtcDateTime + (nextItem.OutPoint - nextItem.InPoint);
context.Playout.Items.Add(nextItem);
context.CurrentTime += nextItem.OutPoint - nextItem.InPoint;
if (j < filteredChapters.Count - 1)
{
context.PushFillerKind(FillerKind.MidRoll);
await executeSequence(midRollSequence.Sequence);
context.PopFillerKind();
}
}
}
}
}
}
}

2
ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutCountHandler.cs

@ -96,7 +96,7 @@ public class YamlPlayoutCountHandler(EnumeratorCache enumeratorCache) : YamlPlay @@ -96,7 +96,7 @@ public class YamlPlayoutCountHandler(EnumeratorCache enumeratorCache) : YamlPlay
playoutItem.WatermarkId = watermarkId;
}
context.Playout.Items.Add(playoutItem);
await AddItemAndMidRoll(context, playoutItem, executeSequence);
context.AdvanceGuideGroup();
// create history record

34
ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutMidRollHandler.cs

@ -0,0 +1,34 @@ @@ -0,0 +1,34 @@
using ErsatzTV.Core.Scheduling.YamlScheduling.Models;
using Microsoft.Extensions.Logging;
namespace ErsatzTV.Core.Scheduling.YamlScheduling.Handlers;
public class YamlPlayoutMidRollHandler : IYamlPlayoutHandler
{
public bool Reset => false;
public Task<bool> Handle(
YamlPlayoutContext context,
YamlPlayoutInstruction instruction,
PlayoutBuildMode mode,
Func<string, Task> executeSequence,
ILogger<YamlPlayoutBuilder> logger,
CancellationToken cancellationToken)
{
if (instruction is not YamlMidRollInstruction midRoll)
{
return Task.FromResult(false);
}
if (midRoll.MidRoll && !string.IsNullOrWhiteSpace(midRoll.Sequence) && !string.IsNullOrWhiteSpace(midRoll.Expression))
{
context.SetMidRollSequence(new YamlPlayoutContext.MidRollSequence(midRoll.Sequence, midRoll.Expression));
}
else
{
context.ClearMidRollSequence();
}
return Task.FromResult(true);
}
}

13
ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlMidRollInstruction.cs

@ -0,0 +1,13 @@ @@ -0,0 +1,13 @@
using YamlDotNet.Serialization;
namespace ErsatzTV.Core.Scheduling.YamlScheduling.Models;
public class YamlMidRollInstruction : YamlPlayoutInstruction
{
[YamlMember(Alias = "mid_roll", ApplyNamingConventions = false)]
public bool MidRoll { get; set; }
public string Sequence { get; set; }
public string Expression { get; set; } = "true";
}

2
ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutBuilder.cs

@ -345,6 +345,7 @@ public class YamlPlayoutBuilder( @@ -345,6 +345,7 @@ public class YamlPlayoutBuilder(
YamlPlayoutShuffleSequenceInstruction => new YamlPlayoutShuffleSequenceHandler(),
YamlPreRollInstruction => new YamlPlayoutPreRollHandler(),
YamlPostRollInstruction => new YamlPlayoutPostRollHandler(),
YamlMidRollInstruction => new YamlPlayoutMidRollHandler(),
YamlPlayoutSkipItemsInstruction => new YamlPlayoutSkipItemsHandler(enumeratorCache),
YamlPlayoutSkipToItemInstruction => new YamlPlayoutSkipToItemHandler(enumeratorCache),
@ -405,6 +406,7 @@ public class YamlPlayoutBuilder( @@ -405,6 +406,7 @@ public class YamlPlayoutBuilder(
{ "pad_until", typeof(YamlPlayoutPadUntilInstruction) },
{ "pre_roll", typeof(YamlPreRollInstruction) },
{ "post_roll", typeof(YamlPostRollInstruction) },
{ "mid_roll", typeof(YamlMidRollInstruction) },
{ "repeat", typeof(YamlPlayoutRepeatInstruction) },
{ "sequence", typeof(YamlPlayoutSequenceInstruction) },
{ "shuffle_sequence", typeof(YamlPlayoutShuffleSequenceInstruction) },

15
ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutContext.cs

@ -20,6 +20,7 @@ public class YamlPlayoutContext(Playout playout, YamlPlayoutDefinition definitio @@ -20,6 +20,7 @@ public class YamlPlayoutContext(Playout playout, YamlPlayoutDefinition definitio
private Option<int> _channelWatermarkId;
private Option<string> _preRollSequence;
private Option<string> _postRollSequence;
private Option<MidRollSequence> _midRollSequence;
public Playout Playout { get; } = playout;
@ -117,6 +118,18 @@ public class YamlPlayoutContext(Playout playout, YamlPlayoutDefinition definitio @@ -117,6 +118,18 @@ public class YamlPlayoutContext(Playout playout, YamlPlayoutDefinition definitio
public Option<string> GetPostRollSequence() => _postRollSequence;
public void SetMidRollSequence(MidRollSequence sequence)
{
_midRollSequence = sequence;
}
public void ClearMidRollSequence()
{
_midRollSequence = Option<MidRollSequence>.None;
}
public Option<MidRollSequence> GetMidRollSequence() => _midRollSequence;
public void PushFillerKind(FillerKind fillerKind)
{
_fillerKind.Push(fillerKind);
@ -188,4 +201,6 @@ public class YamlPlayoutContext(Playout playout, YamlPlayoutDefinition definitio @@ -188,4 +201,6 @@ public class YamlPlayoutContext(Playout playout, YamlPlayoutDefinition definitio
bool? GuideGroupLocked,
int? ChannelWatermarkId,
string PreRollSequence);
public record MidRollSequence(string Sequence, string Expression);
}

12
ErsatzTV/Resources/yaml-playout.schema.json

@ -41,6 +41,7 @@ @@ -41,6 +41,7 @@
{ "$ref": "#/$defs/control/epgGroupInstruction" },
{ "$ref": "#/$defs/control/preRollInstruction"},
{ "$ref": "#/$defs/control/postRollInstruction"},
{ "$ref": "#/$defs/control/midRollInstruction"},
{ "$ref": "#/$defs/control/repeatInstruction" },
{ "$ref": "#/$defs/control/shuffleSequenceInstruction" },
{ "$ref": "#/$defs/control/skipItemsInstruction" },
@ -81,6 +82,7 @@ @@ -81,6 +82,7 @@
{ "$ref": "#/$defs/control/epgGroupInstruction" },
{ "$ref": "#/$defs/control/preRollInstruction"},
{ "$ref": "#/$defs/control/postRollInstruction"},
{ "$ref": "#/$defs/control/midRollInstruction"},
{ "$ref": "#/$defs/control/repeatInstruction" },
{ "$ref": "#/$defs/control/shuffleSequenceInstruction" },
{ "$ref": "#/$defs/control/skipItemsInstruction" },
@ -309,6 +311,16 @@ @@ -309,6 +311,16 @@
"required": [ "post_roll" ],
"additionalProperties": false
},
"midRollInstruction": {
"type": "object",
"properties": {
"mid_roll": { "type": "boolean" },
"sequence": { "type": "string" },
"expression": { "type": "string" }
},
"required": [ "mid_roll" ],
"additionalProperties": false
},
"repeatInstruction": {
"type": "object",
"properties": {

Loading…
Cancel
Save