Browse Source

yaml playout watermarks (#2149)

pull/2150/head
Jason Dove 1 month ago committed by GitHub
parent
commit
43e1cbd919
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 6
      CHANGELOG.md
  2. 33
      ErsatzTV.Core.Tests/Scheduling/FillerExpressionTests.cs
  3. 1
      ErsatzTV.Core/Interfaces/Repositories/IChannelRepository.cs
  4. 7
      ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutAllHandler.cs
  5. 7
      ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutCountHandler.cs
  6. 11
      ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutDurationHandler.cs
  7. 1
      ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutPadToNextHandler.cs
  8. 1
      ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutPadUntilHandler.cs
  9. 39
      ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutWatermarkHandler.cs
  10. 3
      ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutInstruction.cs
  11. 11
      ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutWatermarkInstruction.cs
  12. 5
      ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutBuilder.cs
  13. 13
      ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutContext.cs
  14. 13
      ErsatzTV.Infrastructure/Data/Repositories/ChannelRepository.cs

6
CHANGELOG.md

@ -62,6 +62,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -62,6 +62,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- `count / 2` will play half of the items in the content
- `random % 4 + 1` will play between 1 and 4 items
- `2` (similar to before this change) will play exactly two items
- YAML playout: add `disable_watermarks` property to all content instructions
- This property defaults to `false` (meaning watermarks are allowed by default)
- Setting to `true` will prevent watermarks from ever appearing over the content
- YAML playout: add `watermark` instruction
- With value of `true` and `name` property, will override the watermark in the playout to the watermark with the provided name
- With value of `false`, will restore default watermark value (channel watermark, global watermark)
- Show health check warning and error badges in nav menu
- Add `Expression` for mid-roll filler to allow custom logic for using or skipping chapter markers
- The following parameters can be used:

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

@ -41,4 +41,37 @@ public class FillerExpressionTests @@ -41,4 +41,37 @@ public class FillerExpressionTests
result[1].EndTime.ShouldBe(TimeSpan.FromMinutes(20));
result[2].EndTime.ShouldBe(TimeSpan.FromMinutes(30));
}
[Test]
public void Two_Points_In_30_Minute_Content_Another_Expression()
{
// 30 min content
var playoutItem = new PlayoutItem { Start = DateTimeOffset.Now.UtcDateTime };
playoutItem.Finish = playoutItem.Start + TimeSpan.FromMinutes(30);
// chapters every 5 min
var chapters = new List<MediaChapter>
{
new() { ChapterId = 1, StartTime = TimeSpan.Zero, EndTime = TimeSpan.FromMinutes(5) },
new() { ChapterId = 2, StartTime = TimeSpan.FromMinutes(5), EndTime = TimeSpan.FromMinutes(10) },
new() { ChapterId = 3, StartTime = TimeSpan.FromMinutes(10), EndTime = TimeSpan.FromMinutes(15) },
new() { ChapterId = 4, StartTime = TimeSpan.FromMinutes(15), EndTime = TimeSpan.FromMinutes(20) },
new() { ChapterId = 5, StartTime = TimeSpan.FromMinutes(20), EndTime = TimeSpan.FromMinutes(25) },
new() { ChapterId = 6, StartTime = TimeSpan.FromMinutes(25), EndTime = TimeSpan.FromMinutes(30) }
};
// skip first 5 min of content, wait at least 5 min between points, only match up to 2 points
var fillerPreset = new FillerPreset
{
FillerKind = FillerKind.MidRoll,
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);
result.Count.ShouldBe(3);
result[0].EndTime.ShouldBe(TimeSpan.FromMinutes(10));
result[1].EndTime.ShouldBe(TimeSpan.FromMinutes(20));
result[2].EndTime.ShouldBe(TimeSpan.FromMinutes(30));
}
}

1
ErsatzTV.Core/Interfaces/Repositories/IChannelRepository.cs

@ -7,4 +7,5 @@ public interface IChannelRepository @@ -7,4 +7,5 @@ public interface IChannelRepository
Task<Option<Channel>> GetChannel(int id);
Task<Option<Channel>> GetByNumber(string number);
Task<List<Channel>> GetAll();
Task<Option<ChannelWatermark>> GetWatermarkByName(string name);
}

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

@ -44,7 +44,7 @@ public class YamlPlayoutAllHandler(EnumeratorCache enumeratorCache) : YamlPlayou @@ -44,7 +44,7 @@ public class YamlPlayoutAllHandler(EnumeratorCache enumeratorCache) : YamlPlayou
OutPoint = itemDuration,
FillerKind = GetFillerKind(all),
CustomTitle = string.IsNullOrWhiteSpace(all.CustomTitle) ? null : all.CustomTitle,
//WatermarkId = scheduleItem.WatermarkId,
DisableWatermarks = all.DisableWatermarks,
//PreferredAudioLanguageCode = scheduleItem.PreferredAudioLanguageCode,
//PreferredAudioTitle = scheduleItem.PreferredAudioTitle,
//PreferredSubtitleLanguageCode = scheduleItem.PreferredSubtitleLanguageCode,
@ -57,6 +57,11 @@ public class YamlPlayoutAllHandler(EnumeratorCache enumeratorCache) : YamlPlayou @@ -57,6 +57,11 @@ public class YamlPlayoutAllHandler(EnumeratorCache enumeratorCache) : YamlPlayou
//CollectionEtag = collectionEtags[collectionKey]
};
foreach (int watermarkId in context.GetChannelWatermarkId())
{
playoutItem.WatermarkId = watermarkId;
}
context.Playout.Items.Add(playoutItem);
context.AdvanceGuideGroup();

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

@ -69,7 +69,7 @@ public class YamlPlayoutCountHandler(EnumeratorCache enumeratorCache) : YamlPlay @@ -69,7 +69,7 @@ public class YamlPlayoutCountHandler(EnumeratorCache enumeratorCache) : YamlPlay
OutPoint = itemDuration,
FillerKind = GetFillerKind(count),
CustomTitle = string.IsNullOrWhiteSpace(count.CustomTitle) ? null : count.CustomTitle,
//WatermarkId = scheduleItem.WatermarkId,
DisableWatermarks = count.DisableWatermarks,
//PreferredAudioLanguageCode = scheduleItem.PreferredAudioLanguageCode,
//PreferredAudioTitle = scheduleItem.PreferredAudioTitle,
//PreferredSubtitleLanguageCode = scheduleItem.PreferredSubtitleLanguageCode,
@ -82,6 +82,11 @@ public class YamlPlayoutCountHandler(EnumeratorCache enumeratorCache) : YamlPlay @@ -82,6 +82,11 @@ public class YamlPlayoutCountHandler(EnumeratorCache enumeratorCache) : YamlPlay
//CollectionEtag = collectionEtags[collectionKey]
};
foreach (int watermarkId in context.GetChannelWatermarkId())
{
playoutItem.WatermarkId = watermarkId;
}
context.Playout.Items.Add(playoutItem);
context.AdvanceGuideGroup();

11
ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutDurationHandler.cs

@ -61,6 +61,7 @@ public class YamlPlayoutDurationHandler(EnumeratorCache enumeratorCache) : YamlP @@ -61,6 +61,7 @@ public class YamlPlayoutDurationHandler(EnumeratorCache enumeratorCache) : YamlP
duration.OfflineTail,
GetFillerKind(duration),
duration.CustomTitle,
duration.DisableWatermarks,
enumerator,
fallbackEnumerator,
logger);
@ -82,6 +83,7 @@ public class YamlPlayoutDurationHandler(EnumeratorCache enumeratorCache) : YamlP @@ -82,6 +83,7 @@ public class YamlPlayoutDurationHandler(EnumeratorCache enumeratorCache) : YamlP
bool offlineTail,
FillerKind fillerKind,
string customTitle,
bool disableWatermarks,
IMediaCollectionEnumerator enumerator,
Option<IMediaCollectionEnumerator> fallbackEnumerator,
ILogger<YamlPlayoutBuilder> logger)
@ -103,10 +105,15 @@ public class YamlPlayoutDurationHandler(EnumeratorCache enumeratorCache) : YamlP @@ -103,10 +105,15 @@ public class YamlPlayoutDurationHandler(EnumeratorCache enumeratorCache) : YamlP
OutPoint = itemDuration,
GuideGroup = context.PeekNextGuideGroup(),
FillerKind = fillerKind,
CustomTitle = string.IsNullOrWhiteSpace(customTitle) ? null : customTitle
//DisableWatermarks = !allowWatermarks
CustomTitle = string.IsNullOrWhiteSpace(customTitle) ? null : customTitle,
DisableWatermarks = disableWatermarks
};
foreach (int watermarkId in context.GetChannelWatermarkId())
{
playoutItem.WatermarkId = watermarkId;
}
if (remainingToFill - itemDuration >= TimeSpan.Zero || !stopBeforeEnd)
{
context.Playout.Items.Add(playoutItem);

1
ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutPadToNextHandler.cs

@ -65,6 +65,7 @@ public class YamlPlayoutPadToNextHandler(EnumeratorCache enumeratorCache) : Yaml @@ -65,6 +65,7 @@ public class YamlPlayoutPadToNextHandler(EnumeratorCache enumeratorCache) : Yaml
true,
GetFillerKind(padToNext),
padToNext.CustomTitle,
padToNext.DisableWatermarks,
enumerator,
fallbackEnumerator,
logger);

1
ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutPadUntilHandler.cs

@ -84,6 +84,7 @@ public class YamlPlayoutPadUntilHandler(EnumeratorCache enumeratorCache) : YamlP @@ -84,6 +84,7 @@ public class YamlPlayoutPadUntilHandler(EnumeratorCache enumeratorCache) : YamlP
padUntil.OfflineTail,
GetFillerKind(padUntil),
padUntil.CustomTitle,
padUntil.DisableWatermarks,
enumerator,
fallbackEnumerator,
logger);

39
ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutWatermarkHandler.cs

@ -0,0 +1,39 @@ @@ -0,0 +1,39 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Scheduling.YamlScheduling.Models;
using Microsoft.Extensions.Logging;
namespace ErsatzTV.Core.Scheduling.YamlScheduling.Handlers;
public class YamlPlayoutWatermarkHandler(IChannelRepository channelRepository) : IYamlPlayoutHandler
{
public bool Reset => false;
public async Task<bool> Handle(
YamlPlayoutContext context,
YamlPlayoutInstruction instruction,
PlayoutBuildMode mode,
ILogger<YamlPlayoutBuilder> logger,
CancellationToken cancellationToken)
{
if (instruction is not YamlPlayoutWatermarkInstruction watermark)
{
return false;
}
if (watermark.Watermark && !string.IsNullOrWhiteSpace(watermark.Name))
{
Option<ChannelWatermark> maybeWatermark = await channelRepository.GetWatermarkByName(watermark.Name);
foreach (ChannelWatermark channelWatermark in maybeWatermark)
{
context.SetChannelWatermarkId(channelWatermark.Id);
}
}
else
{
context.ClearChannelWatermarkId();
}
return true;
}
}

3
ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutInstruction.cs

@ -14,6 +14,9 @@ public class YamlPlayoutInstruction @@ -14,6 +14,9 @@ public class YamlPlayoutInstruction
[YamlMember(Alias = "custom_title", ApplyNamingConventions = false)]
public string CustomTitle { get; set; }
[YamlMember(Alias = "disable_watermarks", ApplyNamingConventions = false)]
public bool DisableWatermarks { get; set; } = false;
[YamlIgnore]
public string SequenceKey { get; set; }

11
ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutWatermarkInstruction.cs

@ -0,0 +1,11 @@ @@ -0,0 +1,11 @@
using YamlDotNet.Serialization;
namespace ErsatzTV.Core.Scheduling.YamlScheduling.Models;
public class YamlPlayoutWatermarkInstruction : YamlPlayoutInstruction
{
[YamlMember(Alias = "watermark", ApplyNamingConventions = false)]
public bool Watermark { get; set; }
public string Name { get; set; }
}

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

@ -17,6 +17,7 @@ public class YamlPlayoutBuilder( @@ -17,6 +17,7 @@ public class YamlPlayoutBuilder(
ILocalFileSystem localFileSystem,
IConfigElementRepository configElementRepository,
IMediaCollectionRepository mediaCollectionRepository,
IChannelRepository channelRepository,
ILogger<YamlPlayoutBuilder> logger)
: IYamlPlayoutBuilder
{
@ -274,7 +275,7 @@ public class YamlPlayoutBuilder( @@ -274,7 +275,7 @@ public class YamlPlayoutBuilder(
}
}
private static Option<IYamlPlayoutHandler> GetHandlerForInstruction(
private Option<IYamlPlayoutHandler> GetHandlerForInstruction(
Dictionary<YamlPlayoutInstruction, IYamlPlayoutHandler> handlers,
EnumeratorCache enumeratorCache,
YamlPlayoutInstruction instruction)
@ -289,6 +290,7 @@ public class YamlPlayoutBuilder( @@ -289,6 +290,7 @@ public class YamlPlayoutBuilder(
YamlPlayoutRepeatInstruction => new YamlPlayoutRepeatHandler(),
YamlPlayoutWaitUntilInstruction => new YamlPlayoutWaitUntilHandler(),
YamlPlayoutEpgGroupInstruction => new YamlPlayoutEpgGroupHandler(),
YamlPlayoutWatermarkInstruction => new YamlPlayoutWatermarkHandler(channelRepository),
YamlPlayoutShuffleSequenceInstruction => new YamlPlayoutShuffleSequenceHandler(),
YamlPlayoutSkipItemsInstruction => new YamlPlayoutSkipItemsHandler(enumeratorCache),
@ -341,6 +343,7 @@ public class YamlPlayoutBuilder( @@ -341,6 +343,7 @@ public class YamlPlayoutBuilder(
{ "count", typeof(YamlPlayoutCountInstruction) },
{ "duration", typeof(YamlPlayoutDurationInstruction) },
{ "epg_group", typeof(YamlPlayoutEpgGroupInstruction) },
{ "watermark", typeof(YamlPlayoutWatermarkInstruction) },
{ "pad_to_next", typeof(YamlPlayoutPadToNextInstruction) },
{ "pad_until", typeof(YamlPlayoutPadUntilInstruction) },
{ "repeat", typeof(YamlPlayoutRepeatInstruction) },

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

@ -9,6 +9,7 @@ public class YamlPlayoutContext(Playout playout, YamlPlayoutDefinition definitio @@ -9,6 +9,7 @@ public class YamlPlayoutContext(Playout playout, YamlPlayoutDefinition definitio
private int _guideGroup = guideGroup;
private bool _guideGroupLocked;
private int _instructionIndex;
private Option<int> _channelWatermarkId;
public Playout Playout { get; } = playout;
@ -69,4 +70,16 @@ public class YamlPlayoutContext(Playout playout, YamlPlayoutDefinition definitio @@ -69,4 +70,16 @@ public class YamlPlayoutContext(Playout playout, YamlPlayoutDefinition definitio
}
public void UnlockGuideGroup() => _guideGroupLocked = false;
public void SetChannelWatermarkId(int id)
{
_channelWatermarkId = id;
}
public void ClearChannelWatermarkId()
{
_channelWatermarkId = Option<int>.None;
}
public Option<int> GetChannelWatermarkId() => _channelWatermarkId;
}

13
ErsatzTV.Infrastructure/Data/Repositories/ChannelRepository.cs

@ -44,4 +44,17 @@ public class ChannelRepository : IChannelRepository @@ -44,4 +44,17 @@ public class ChannelRepository : IChannelRepository
.Include(c => c.Playouts)
.ToListAsync();
}
public async Task<Option<ChannelWatermark>> GetWatermarkByName(string name)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
List<ChannelWatermark> maybeWatermarks = await dbContext.ChannelWatermarks
.Where(cw => EF.Functions.Like(
EF.Functions.Collate(cw.Name, TvContext.CaseInsensitiveCollation),
$"%{name}%"))
.ToListAsync();
return maybeWatermarks.HeadOrNone();
}
}

Loading…
Cancel
Save